@hasna/todos 0.11.13 → 0.11.14
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 +4264 -3018
- package/dist/index.js +4922 -3918
- package/dist/mcp/index.js +4140 -3033
- package/package.json +1 -1
- package/dashboard/dist/assets/index-B-w1tUlm.js +0 -346
- package/dashboard/dist/assets/index-BXQ39iMX.css +0 -1
- package/dashboard/dist/index.html +0 -13
- package/dashboard/dist/logo.jpg +0 -0
- package/dist/cli/brains.d.ts +0 -3
- package/dist/cli/brains.d.ts.map +0 -1
- package/dist/cli/components/App.d.ts +0 -2
- package/dist/cli/components/App.d.ts.map +0 -1
- package/dist/cli/components/Dashboard.d.ts +0 -7
- package/dist/cli/components/Dashboard.d.ts.map +0 -1
- package/dist/cli/components/Header.d.ts +0 -8
- package/dist/cli/components/Header.d.ts.map +0 -1
- package/dist/cli/components/ProjectList.d.ts +0 -8
- package/dist/cli/components/ProjectList.d.ts.map +0 -1
- package/dist/cli/components/SearchView.d.ts +0 -10
- package/dist/cli/components/SearchView.d.ts.map +0 -1
- package/dist/cli/components/TaskDetail.d.ts +0 -7
- package/dist/cli/components/TaskDetail.d.ts.map +0 -1
- package/dist/cli/components/TaskForm.d.ts +0 -15
- package/dist/cli/components/TaskForm.d.ts.map +0 -1
- package/dist/cli/components/TaskList.d.ts +0 -8
- package/dist/cli/components/TaskList.d.ts.map +0 -1
- package/dist/cli/index.d.ts +0 -3
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/db/agent-metrics.d.ts +0 -34
- package/dist/db/agent-metrics.d.ts.map +0 -1
- package/dist/db/agents.d.ts +0 -82
- package/dist/db/agents.d.ts.map +0 -1
- package/dist/db/audit.d.ts +0 -52
- package/dist/db/audit.d.ts.map +0 -1
- package/dist/db/budgets.d.ts +0 -27
- package/dist/db/budgets.d.ts.map +0 -1
- package/dist/db/builtin-templates.d.ts +0 -22
- package/dist/db/builtin-templates.d.ts.map +0 -1
- package/dist/db/checklists.d.ts +0 -13
- package/dist/db/checklists.d.ts.map +0 -1
- package/dist/db/comments.d.ts +0 -8
- package/dist/db/comments.d.ts.map +0 -1
- package/dist/db/database.d.ts +0 -12
- package/dist/db/database.d.ts.map +0 -1
- package/dist/db/file-locks.d.ts +0 -43
- package/dist/db/file-locks.d.ts.map +0 -1
- package/dist/db/handoffs.d.ts +0 -25
- package/dist/db/handoffs.d.ts.map +0 -1
- package/dist/db/kg.d.ts +0 -70
- package/dist/db/kg.d.ts.map +0 -1
- package/dist/db/locks.d.ts +0 -14
- package/dist/db/locks.d.ts.map +0 -1
- package/dist/db/orgs.d.ts +0 -13
- package/dist/db/orgs.d.ts.map +0 -1
- package/dist/db/patrol.d.ts +0 -35
- package/dist/db/patrol.d.ts.map +0 -1
- package/dist/db/pg-migrate.d.ts +0 -14
- package/dist/db/pg-migrate.d.ts.map +0 -1
- package/dist/db/pg-migrations.d.ts +0 -8
- package/dist/db/pg-migrations.d.ts.map +0 -1
- package/dist/db/plans.d.ts +0 -8
- package/dist/db/plans.d.ts.map +0 -1
- package/dist/db/project-agent-roles.d.ts +0 -34
- package/dist/db/project-agent-roles.d.ts.map +0 -1
- package/dist/db/projects.d.ts +0 -16
- package/dist/db/projects.d.ts.map +0 -1
- package/dist/db/sessions.d.ts +0 -8
- package/dist/db/sessions.d.ts.map +0 -1
- package/dist/db/snapshots.d.ts +0 -37
- package/dist/db/snapshots.d.ts.map +0 -1
- package/dist/db/task-commits.d.ts +0 -31
- package/dist/db/task-commits.d.ts.map +0 -1
- package/dist/db/task-files.d.ts +0 -74
- package/dist/db/task-files.d.ts.map +0 -1
- package/dist/db/task-lists.d.ts +0 -10
- package/dist/db/task-lists.d.ts.map +0 -1
- package/dist/db/task-relationships.d.ts +0 -36
- package/dist/db/task-relationships.d.ts.map +0 -1
- package/dist/db/tasks.d.ts +0 -215
- package/dist/db/tasks.d.ts.map +0 -1
- package/dist/db/templates.d.ts +0 -98
- package/dist/db/templates.d.ts.map +0 -1
- package/dist/db/traces.d.ts +0 -38
- package/dist/db/traces.d.ts.map +0 -1
- package/dist/db/webhooks.d.ts +0 -19
- package/dist/db/webhooks.d.ts.map +0 -1
- package/dist/index.d.ts +0 -65
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/agent-tasks.d.ts +0 -11
- package/dist/lib/agent-tasks.d.ts.map +0 -1
- package/dist/lib/auto-assign.d.ts +0 -25
- package/dist/lib/auto-assign.d.ts.map +0 -1
- package/dist/lib/burndown.d.ts +0 -18
- package/dist/lib/burndown.d.ts.map +0 -1
- package/dist/lib/claude-tasks.d.ts +0 -20
- package/dist/lib/claude-tasks.d.ts.map +0 -1
- package/dist/lib/completion-guard.d.ts +0 -17
- package/dist/lib/completion-guard.d.ts.map +0 -1
- package/dist/lib/config.d.ts +0 -44
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/extract.d.ts +0 -51
- package/dist/lib/extract.d.ts.map +0 -1
- package/dist/lib/gatherer.d.ts +0 -16
- package/dist/lib/gatherer.d.ts.map +0 -1
- package/dist/lib/github.d.ts +0 -25
- package/dist/lib/github.d.ts.map +0 -1
- package/dist/lib/model-config.d.ts +0 -14
- package/dist/lib/model-config.d.ts.map +0 -1
- package/dist/lib/recurrence.d.ts +0 -10
- package/dist/lib/recurrence.d.ts.map +0 -1
- package/dist/lib/search.d.ts +0 -17
- package/dist/lib/search.d.ts.map +0 -1
- package/dist/lib/sync-types.d.ts +0 -16
- package/dist/lib/sync-types.d.ts.map +0 -1
- package/dist/lib/sync-utils.d.ts +0 -12
- package/dist/lib/sync-utils.d.ts.map +0 -1
- package/dist/lib/sync.d.ts +0 -9
- package/dist/lib/sync.d.ts.map +0 -1
- package/dist/mcp/index.d.ts +0 -3
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/sdk.d.ts +0 -186
- package/dist/sdk.d.ts.map +0 -1
- package/dist/server/index.d.ts +0 -9
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -4707
- package/dist/server/serve.d.ts +0 -10
- package/dist/server/serve.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -590
- package/dist/types/index.d.ts.map +0 -1
package/dist/server/index.js
DELETED
|
@@ -1,4707 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
// @bun
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __returnValue = (v) => v;
|
|
5
|
-
function __exportSetter(name, newValue) {
|
|
6
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
-
}
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, {
|
|
11
|
-
get: all[name],
|
|
12
|
-
enumerable: true,
|
|
13
|
-
configurable: true,
|
|
14
|
-
set: __exportSetter.bind(all, name)
|
|
15
|
-
});
|
|
16
|
-
};
|
|
17
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
-
var __require = import.meta.require;
|
|
19
|
-
|
|
20
|
-
// src/types/index.ts
|
|
21
|
-
var VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, DependencyCycleError, CompletionGuardError;
|
|
22
|
-
var init_types = __esm(() => {
|
|
23
|
-
VersionConflictError = class VersionConflictError extends Error {
|
|
24
|
-
taskId;
|
|
25
|
-
expectedVersion;
|
|
26
|
-
actualVersion;
|
|
27
|
-
static code = "VERSION_CONFLICT";
|
|
28
|
-
static suggestion = "Fetch the task with get_task to get the current version before updating.";
|
|
29
|
-
constructor(taskId, expectedVersion, actualVersion) {
|
|
30
|
-
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
31
|
-
this.taskId = taskId;
|
|
32
|
-
this.expectedVersion = expectedVersion;
|
|
33
|
-
this.actualVersion = actualVersion;
|
|
34
|
-
this.name = "VersionConflictError";
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
TaskNotFoundError = class TaskNotFoundError extends Error {
|
|
38
|
-
taskId;
|
|
39
|
-
static code = "TASK_NOT_FOUND";
|
|
40
|
-
static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
|
|
41
|
-
constructor(taskId) {
|
|
42
|
-
super(`Task not found: ${taskId}`);
|
|
43
|
-
this.taskId = taskId;
|
|
44
|
-
this.name = "TaskNotFoundError";
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
48
|
-
projectId;
|
|
49
|
-
static code = "PROJECT_NOT_FOUND";
|
|
50
|
-
static suggestion = "Use list_projects to see available projects.";
|
|
51
|
-
constructor(projectId) {
|
|
52
|
-
super(`Project not found: ${projectId}`);
|
|
53
|
-
this.projectId = projectId;
|
|
54
|
-
this.name = "ProjectNotFoundError";
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
58
|
-
planId;
|
|
59
|
-
static code = "PLAN_NOT_FOUND";
|
|
60
|
-
static suggestion = "Use list_plans to see available plans.";
|
|
61
|
-
constructor(planId) {
|
|
62
|
-
super(`Plan not found: ${planId}`);
|
|
63
|
-
this.planId = planId;
|
|
64
|
-
this.name = "PlanNotFoundError";
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
LockError = class LockError extends Error {
|
|
68
|
-
taskId;
|
|
69
|
-
lockedBy;
|
|
70
|
-
static code = "LOCK_ERROR";
|
|
71
|
-
static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
|
|
72
|
-
constructor(taskId, lockedBy) {
|
|
73
|
-
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
74
|
-
this.taskId = taskId;
|
|
75
|
-
this.lockedBy = lockedBy;
|
|
76
|
-
this.name = "LockError";
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
DependencyCycleError = class DependencyCycleError extends Error {
|
|
80
|
-
taskId;
|
|
81
|
-
dependsOn;
|
|
82
|
-
static code = "DEPENDENCY_CYCLE";
|
|
83
|
-
static suggestion = "Check the dependency chain with get_task to avoid circular references.";
|
|
84
|
-
constructor(taskId, dependsOn) {
|
|
85
|
-
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
86
|
-
this.taskId = taskId;
|
|
87
|
-
this.dependsOn = dependsOn;
|
|
88
|
-
this.name = "DependencyCycleError";
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
CompletionGuardError = class CompletionGuardError extends Error {
|
|
92
|
-
reason;
|
|
93
|
-
retryAfterSeconds;
|
|
94
|
-
static code = "COMPLETION_BLOCKED";
|
|
95
|
-
static suggestion = "Wait for the cooldown period, then retry.";
|
|
96
|
-
constructor(reason, retryAfterSeconds) {
|
|
97
|
-
super(reason);
|
|
98
|
-
this.reason = reason;
|
|
99
|
-
this.retryAfterSeconds = retryAfterSeconds;
|
|
100
|
-
this.name = "CompletionGuardError";
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// src/db/database.ts
|
|
106
|
-
import { Database } from "bun:sqlite";
|
|
107
|
-
import { existsSync, mkdirSync } from "fs";
|
|
108
|
-
import { dirname, join, resolve } from "path";
|
|
109
|
-
function isInMemoryDb(path) {
|
|
110
|
-
return path === ":memory:" || path.startsWith("file::memory:");
|
|
111
|
-
}
|
|
112
|
-
function findNearestTodosDb(startDir) {
|
|
113
|
-
let dir = resolve(startDir);
|
|
114
|
-
while (true) {
|
|
115
|
-
const candidate = join(dir, ".todos", "todos.db");
|
|
116
|
-
if (existsSync(candidate))
|
|
117
|
-
return candidate;
|
|
118
|
-
const parent = dirname(dir);
|
|
119
|
-
if (parent === dir)
|
|
120
|
-
break;
|
|
121
|
-
dir = parent;
|
|
122
|
-
}
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
function findGitRoot(startDir) {
|
|
126
|
-
let dir = resolve(startDir);
|
|
127
|
-
while (true) {
|
|
128
|
-
if (existsSync(join(dir, ".git")))
|
|
129
|
-
return dir;
|
|
130
|
-
const parent = dirname(dir);
|
|
131
|
-
if (parent === dir)
|
|
132
|
-
break;
|
|
133
|
-
dir = parent;
|
|
134
|
-
}
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
function getDbPath() {
|
|
138
|
-
if (process.env["HASNA_TODOS_DB_PATH"]) {
|
|
139
|
-
return process.env["HASNA_TODOS_DB_PATH"];
|
|
140
|
-
}
|
|
141
|
-
if (process.env["TODOS_DB_PATH"]) {
|
|
142
|
-
return process.env["TODOS_DB_PATH"];
|
|
143
|
-
}
|
|
144
|
-
const cwd = process.cwd();
|
|
145
|
-
const nearest = findNearestTodosDb(cwd);
|
|
146
|
-
if (nearest)
|
|
147
|
-
return nearest;
|
|
148
|
-
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
149
|
-
const gitRoot = findGitRoot(cwd);
|
|
150
|
-
if (gitRoot) {
|
|
151
|
-
return join(gitRoot, ".todos", "todos.db");
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
155
|
-
const newPath = join(home, ".hasna", "todos", "todos.db");
|
|
156
|
-
const legacyPath = join(home, ".todos", "todos.db");
|
|
157
|
-
if (!existsSync(newPath) && existsSync(legacyPath)) {
|
|
158
|
-
return legacyPath;
|
|
159
|
-
}
|
|
160
|
-
return newPath;
|
|
161
|
-
}
|
|
162
|
-
function ensureDir(filePath) {
|
|
163
|
-
if (isInMemoryDb(filePath))
|
|
164
|
-
return;
|
|
165
|
-
const dir = dirname(resolve(filePath));
|
|
166
|
-
if (!existsSync(dir)) {
|
|
167
|
-
mkdirSync(dir, { recursive: true });
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
function getDatabase(dbPath) {
|
|
171
|
-
if (_db)
|
|
172
|
-
return _db;
|
|
173
|
-
const path = dbPath || getDbPath();
|
|
174
|
-
ensureDir(path);
|
|
175
|
-
_db = new Database(path);
|
|
176
|
-
_db.run("PRAGMA journal_mode = WAL");
|
|
177
|
-
_db.run("PRAGMA busy_timeout = 5000");
|
|
178
|
-
_db.run("PRAGMA foreign_keys = ON");
|
|
179
|
-
runMigrations(_db);
|
|
180
|
-
backfillTaskTags(_db);
|
|
181
|
-
return _db;
|
|
182
|
-
}
|
|
183
|
-
function runMigrations(db) {
|
|
184
|
-
try {
|
|
185
|
-
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
186
|
-
const currentLevel = result?.max_id ?? 0;
|
|
187
|
-
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
188
|
-
try {
|
|
189
|
-
db.exec(MIGRATIONS[i]);
|
|
190
|
-
} catch {}
|
|
191
|
-
}
|
|
192
|
-
} catch {
|
|
193
|
-
for (const migration of MIGRATIONS) {
|
|
194
|
-
try {
|
|
195
|
-
db.exec(migration);
|
|
196
|
-
} catch {}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
ensureSchema(db);
|
|
200
|
-
}
|
|
201
|
-
function ensureSchema(db) {
|
|
202
|
-
const ensureColumn = (table, column, type) => {
|
|
203
|
-
try {
|
|
204
|
-
db.query(`SELECT ${column} FROM ${table} LIMIT 0`).get();
|
|
205
|
-
} catch {
|
|
206
|
-
try {
|
|
207
|
-
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
208
|
-
} catch {}
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
const ensureTable = (name, sql) => {
|
|
212
|
-
try {
|
|
213
|
-
const exists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(name);
|
|
214
|
-
if (!exists)
|
|
215
|
-
db.exec(sql);
|
|
216
|
-
} catch {}
|
|
217
|
-
};
|
|
218
|
-
const ensureIndex = (sql) => {
|
|
219
|
-
try {
|
|
220
|
-
db.exec(sql);
|
|
221
|
-
} catch {}
|
|
222
|
-
};
|
|
223
|
-
ensureTable("orgs", `
|
|
224
|
-
CREATE TABLE orgs (
|
|
225
|
-
id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
|
|
226
|
-
metadata TEXT DEFAULT '{}',
|
|
227
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
228
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
229
|
-
)`);
|
|
230
|
-
ensureTable("agents", `
|
|
231
|
-
CREATE TABLE agents (
|
|
232
|
-
id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
|
|
233
|
-
role TEXT DEFAULT 'agent', permissions TEXT DEFAULT '["*"]',
|
|
234
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived')),
|
|
235
|
-
metadata TEXT DEFAULT '{}',
|
|
236
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
237
|
-
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
238
|
-
)`);
|
|
239
|
-
ensureTable("task_lists", `
|
|
240
|
-
CREATE TABLE task_lists (
|
|
241
|
-
id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
242
|
-
slug TEXT NOT NULL, name TEXT NOT NULL, description TEXT,
|
|
243
|
-
metadata TEXT DEFAULT '{}',
|
|
244
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
245
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
246
|
-
UNIQUE(project_id, slug)
|
|
247
|
-
)`);
|
|
248
|
-
ensureTable("plans", `
|
|
249
|
-
CREATE TABLE plans (
|
|
250
|
-
id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
251
|
-
task_list_id TEXT, agent_id TEXT,
|
|
252
|
-
name TEXT NOT NULL, description TEXT,
|
|
253
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
254
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
255
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
256
|
-
)`);
|
|
257
|
-
ensureTable("task_tags", `
|
|
258
|
-
CREATE TABLE task_tags (
|
|
259
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
260
|
-
tag TEXT NOT NULL, PRIMARY KEY (task_id, tag)
|
|
261
|
-
)`);
|
|
262
|
-
ensureTable("task_history", `
|
|
263
|
-
CREATE TABLE task_history (
|
|
264
|
-
id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
265
|
-
action TEXT NOT NULL, field TEXT, old_value TEXT, new_value TEXT, agent_id TEXT,
|
|
266
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
267
|
-
)`);
|
|
268
|
-
ensureTable("webhooks", `
|
|
269
|
-
CREATE TABLE webhooks (
|
|
270
|
-
id TEXT PRIMARY KEY, url TEXT NOT NULL, events TEXT NOT NULL DEFAULT '[]',
|
|
271
|
-
secret TEXT, active INTEGER NOT NULL DEFAULT 1,
|
|
272
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
273
|
-
)`);
|
|
274
|
-
ensureTable("task_templates", `
|
|
275
|
-
CREATE TABLE task_templates (
|
|
276
|
-
id TEXT PRIMARY KEY, name TEXT NOT NULL, title_pattern TEXT NOT NULL,
|
|
277
|
-
description TEXT, priority TEXT DEFAULT 'medium', tags TEXT DEFAULT '[]',
|
|
278
|
-
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
279
|
-
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
280
|
-
metadata TEXT DEFAULT '{}',
|
|
281
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
282
|
-
)`);
|
|
283
|
-
ensureTable("template_tasks", `
|
|
284
|
-
CREATE TABLE template_tasks (
|
|
285
|
-
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
286
|
-
template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
|
|
287
|
-
position INTEGER NOT NULL,
|
|
288
|
-
title_pattern TEXT NOT NULL,
|
|
289
|
-
description TEXT,
|
|
290
|
-
priority TEXT DEFAULT 'medium',
|
|
291
|
-
tags TEXT DEFAULT '[]',
|
|
292
|
-
task_type TEXT,
|
|
293
|
-
depends_on_positions TEXT DEFAULT '[]',
|
|
294
|
-
metadata TEXT DEFAULT '{}',
|
|
295
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
296
|
-
)`);
|
|
297
|
-
ensureTable("task_checklists", `
|
|
298
|
-
CREATE TABLE task_checklists (
|
|
299
|
-
id TEXT PRIMARY KEY,
|
|
300
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
301
|
-
position INTEGER NOT NULL DEFAULT 0,
|
|
302
|
-
text TEXT NOT NULL,
|
|
303
|
-
checked INTEGER NOT NULL DEFAULT 0,
|
|
304
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
305
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
306
|
-
)`);
|
|
307
|
-
ensureTable("project_sources", `
|
|
308
|
-
CREATE TABLE project_sources (
|
|
309
|
-
id TEXT PRIMARY KEY,
|
|
310
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
311
|
-
type TEXT NOT NULL,
|
|
312
|
-
name TEXT NOT NULL,
|
|
313
|
-
uri TEXT NOT NULL,
|
|
314
|
-
description TEXT,
|
|
315
|
-
metadata TEXT DEFAULT '{}',
|
|
316
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
317
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
318
|
-
)`);
|
|
319
|
-
ensureTable("task_relationships", `
|
|
320
|
-
CREATE TABLE task_relationships (
|
|
321
|
-
id TEXT PRIMARY KEY,
|
|
322
|
-
source_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
323
|
-
target_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
324
|
-
relationship_type TEXT NOT NULL,
|
|
325
|
-
metadata TEXT DEFAULT '{}',
|
|
326
|
-
created_by TEXT,
|
|
327
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
328
|
-
CHECK (source_task_id != target_task_id)
|
|
329
|
-
)`);
|
|
330
|
-
ensureTable("kg_edges", `
|
|
331
|
-
CREATE TABLE kg_edges (
|
|
332
|
-
id TEXT PRIMARY KEY,
|
|
333
|
-
source_id TEXT NOT NULL,
|
|
334
|
-
source_type TEXT NOT NULL,
|
|
335
|
-
target_id TEXT NOT NULL,
|
|
336
|
-
target_type TEXT NOT NULL,
|
|
337
|
-
relation_type TEXT NOT NULL,
|
|
338
|
-
weight REAL NOT NULL DEFAULT 1.0,
|
|
339
|
-
metadata TEXT DEFAULT '{}',
|
|
340
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
341
|
-
UNIQUE(source_id, source_type, target_id, target_type, relation_type)
|
|
342
|
-
)`);
|
|
343
|
-
ensureColumn("projects", "task_list_id", "TEXT");
|
|
344
|
-
ensureColumn("projects", "task_prefix", "TEXT");
|
|
345
|
-
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
346
|
-
ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
|
|
347
|
-
ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
|
|
348
|
-
ensureColumn("tasks", "short_id", "TEXT");
|
|
349
|
-
ensureColumn("tasks", "due_at", "TEXT");
|
|
350
|
-
ensureColumn("tasks", "estimated_minutes", "INTEGER");
|
|
351
|
-
ensureColumn("tasks", "requires_approval", "INTEGER NOT NULL DEFAULT 0");
|
|
352
|
-
ensureColumn("tasks", "approved_by", "TEXT");
|
|
353
|
-
ensureColumn("tasks", "approved_at", "TEXT");
|
|
354
|
-
ensureColumn("tasks", "recurrence_rule", "TEXT");
|
|
355
|
-
ensureColumn("tasks", "recurrence_parent_id", "TEXT REFERENCES tasks(id) ON DELETE SET NULL");
|
|
356
|
-
ensureColumn("tasks", "confidence", "REAL");
|
|
357
|
-
ensureColumn("tasks", "reason", "TEXT");
|
|
358
|
-
ensureColumn("tasks", "spawned_from_session", "TEXT");
|
|
359
|
-
ensureColumn("tasks", "assigned_by", "TEXT");
|
|
360
|
-
ensureColumn("tasks", "assigned_from_project", "TEXT");
|
|
361
|
-
ensureColumn("tasks", "started_at", "TEXT");
|
|
362
|
-
ensureColumn("tasks", "task_type", "TEXT");
|
|
363
|
-
ensureColumn("tasks", "cost_tokens", "INTEGER DEFAULT 0");
|
|
364
|
-
ensureColumn("tasks", "cost_usd", "REAL DEFAULT 0");
|
|
365
|
-
ensureColumn("tasks", "delegated_from", "TEXT");
|
|
366
|
-
ensureColumn("tasks", "delegation_depth", "INTEGER DEFAULT 0");
|
|
367
|
-
ensureColumn("tasks", "retry_count", "INTEGER DEFAULT 0");
|
|
368
|
-
ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
|
|
369
|
-
ensureColumn("tasks", "retry_after", "TEXT");
|
|
370
|
-
ensureColumn("tasks", "sla_minutes", "INTEGER");
|
|
371
|
-
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
372
|
-
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
373
|
-
ensureColumn("agents", "reports_to", "TEXT");
|
|
374
|
-
ensureColumn("agents", "title", "TEXT");
|
|
375
|
-
ensureColumn("agents", "level", "TEXT");
|
|
376
|
-
ensureColumn("agents", "org_id", "TEXT");
|
|
377
|
-
ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
|
|
378
|
-
ensureColumn("projects", "org_id", "TEXT");
|
|
379
|
-
ensureColumn("plans", "task_list_id", "TEXT");
|
|
380
|
-
ensureColumn("plans", "agent_id", "TEXT");
|
|
381
|
-
ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
|
|
382
|
-
ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
|
|
383
|
-
ensureColumn("template_tasks", "condition", "TEXT");
|
|
384
|
-
ensureColumn("template_tasks", "include_template_id", "TEXT");
|
|
385
|
-
ensureTable("template_versions", `
|
|
386
|
-
CREATE TABLE template_versions (
|
|
387
|
-
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
388
|
-
template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
|
|
389
|
-
version INTEGER NOT NULL,
|
|
390
|
-
snapshot TEXT NOT NULL,
|
|
391
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
392
|
-
)`);
|
|
393
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id)");
|
|
394
|
-
ensureColumn("webhooks", "project_id", "TEXT");
|
|
395
|
-
ensureColumn("webhooks", "task_list_id", "TEXT");
|
|
396
|
-
ensureColumn("webhooks", "agent_id", "TEXT");
|
|
397
|
-
ensureColumn("webhooks", "task_id", "TEXT");
|
|
398
|
-
ensureTable("webhook_deliveries", `
|
|
399
|
-
CREATE TABLE webhook_deliveries (
|
|
400
|
-
id TEXT PRIMARY KEY,
|
|
401
|
-
webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
|
|
402
|
-
event TEXT NOT NULL,
|
|
403
|
-
payload TEXT NOT NULL,
|
|
404
|
-
status_code INTEGER,
|
|
405
|
-
response TEXT,
|
|
406
|
-
attempt INTEGER NOT NULL DEFAULT 1,
|
|
407
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
408
|
-
)`);
|
|
409
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id)");
|
|
410
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
|
|
411
|
-
ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
|
|
412
|
-
ensureColumn("task_comments", "progress_pct", "INTEGER");
|
|
413
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
|
|
414
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id)");
|
|
415
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at)");
|
|
416
|
-
ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL");
|
|
417
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name)");
|
|
418
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_project ON task_lists(project_id)");
|
|
419
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug)");
|
|
420
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag)");
|
|
421
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id)");
|
|
422
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
|
|
423
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
|
|
424
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
|
|
425
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
|
|
426
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
|
|
427
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(agent_id)");
|
|
428
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id)");
|
|
429
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL");
|
|
430
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_checklists_task ON task_checklists(task_id)");
|
|
431
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id)");
|
|
432
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type)");
|
|
433
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by)");
|
|
434
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id)");
|
|
435
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id)");
|
|
436
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type)");
|
|
437
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id)");
|
|
438
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type)");
|
|
439
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type)");
|
|
440
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type)");
|
|
441
|
-
}
|
|
442
|
-
function backfillTaskTags(db) {
|
|
443
|
-
try {
|
|
444
|
-
const count = db.query("SELECT COUNT(*) as count FROM task_tags").get();
|
|
445
|
-
if (count && count.count > 0)
|
|
446
|
-
return;
|
|
447
|
-
} catch {
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
try {
|
|
451
|
-
const rows = db.query("SELECT id, tags FROM tasks WHERE tags IS NOT NULL AND tags != '[]'").all();
|
|
452
|
-
if (rows.length === 0)
|
|
453
|
-
return;
|
|
454
|
-
const insert = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
455
|
-
for (const row of rows) {
|
|
456
|
-
if (!row.tags)
|
|
457
|
-
continue;
|
|
458
|
-
let tags = [];
|
|
459
|
-
try {
|
|
460
|
-
tags = JSON.parse(row.tags);
|
|
461
|
-
} catch {
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
for (const tag of tags) {
|
|
465
|
-
if (tag)
|
|
466
|
-
insert.run(row.id, tag);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
} catch {}
|
|
470
|
-
}
|
|
471
|
-
function now() {
|
|
472
|
-
return new Date().toISOString();
|
|
473
|
-
}
|
|
474
|
-
function uuid() {
|
|
475
|
-
return crypto.randomUUID();
|
|
476
|
-
}
|
|
477
|
-
function isLockExpired(lockedAt) {
|
|
478
|
-
if (!lockedAt)
|
|
479
|
-
return true;
|
|
480
|
-
const lockTime = new Date(lockedAt).getTime();
|
|
481
|
-
const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
|
|
482
|
-
return Date.now() - lockTime > expiryMs;
|
|
483
|
-
}
|
|
484
|
-
function lockExpiryCutoff(nowMs = Date.now()) {
|
|
485
|
-
const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
|
|
486
|
-
return new Date(nowMs - expiryMs).toISOString();
|
|
487
|
-
}
|
|
488
|
-
function clearExpiredLocks(db) {
|
|
489
|
-
const cutoff = lockExpiryCutoff();
|
|
490
|
-
db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
|
|
491
|
-
}
|
|
492
|
-
function resolvePartialId(db, table, partialId) {
|
|
493
|
-
if (partialId.length >= 36) {
|
|
494
|
-
const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
|
|
495
|
-
return row?.id ?? null;
|
|
496
|
-
}
|
|
497
|
-
const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
|
|
498
|
-
if (rows.length === 1) {
|
|
499
|
-
return rows[0].id;
|
|
500
|
-
}
|
|
501
|
-
if (rows.length > 1) {
|
|
502
|
-
return null;
|
|
503
|
-
}
|
|
504
|
-
if (table === "tasks") {
|
|
505
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
|
|
506
|
-
if (shortIdRows.length === 1) {
|
|
507
|
-
return shortIdRows[0].id;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
if (table === "task_lists") {
|
|
511
|
-
const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
|
|
512
|
-
if (slugRow)
|
|
513
|
-
return slugRow.id;
|
|
514
|
-
}
|
|
515
|
-
if (table === "projects") {
|
|
516
|
-
const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
|
|
517
|
-
if (nameRow)
|
|
518
|
-
return nameRow.id;
|
|
519
|
-
}
|
|
520
|
-
return null;
|
|
521
|
-
}
|
|
522
|
-
var LOCK_EXPIRY_MINUTES = 30, MIGRATIONS, _db = null;
|
|
523
|
-
var init_database = __esm(() => {
|
|
524
|
-
MIGRATIONS = [
|
|
525
|
-
`
|
|
526
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
527
|
-
id TEXT PRIMARY KEY,
|
|
528
|
-
name TEXT NOT NULL,
|
|
529
|
-
path TEXT UNIQUE NOT NULL,
|
|
530
|
-
description TEXT,
|
|
531
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
532
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
533
|
-
);
|
|
534
|
-
|
|
535
|
-
CREATE TABLE IF NOT EXISTS tasks (
|
|
536
|
-
id TEXT PRIMARY KEY,
|
|
537
|
-
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
538
|
-
parent_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
|
|
539
|
-
title TEXT NOT NULL,
|
|
540
|
-
description TEXT,
|
|
541
|
-
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'failed', 'cancelled')),
|
|
542
|
-
priority TEXT NOT NULL DEFAULT 'medium' CHECK(priority IN ('low', 'medium', 'high', 'critical')),
|
|
543
|
-
agent_id TEXT,
|
|
544
|
-
assigned_to TEXT,
|
|
545
|
-
session_id TEXT,
|
|
546
|
-
working_dir TEXT,
|
|
547
|
-
tags TEXT DEFAULT '[]',
|
|
548
|
-
metadata TEXT DEFAULT '{}',
|
|
549
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
550
|
-
locked_by TEXT,
|
|
551
|
-
locked_at TEXT,
|
|
552
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
553
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
554
|
-
completed_at TEXT
|
|
555
|
-
);
|
|
556
|
-
|
|
557
|
-
CREATE TABLE IF NOT EXISTS task_dependencies (
|
|
558
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
559
|
-
depends_on TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
560
|
-
PRIMARY KEY (task_id, depends_on),
|
|
561
|
-
CHECK (task_id != depends_on)
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
CREATE TABLE IF NOT EXISTS task_comments (
|
|
565
|
-
id TEXT PRIMARY KEY,
|
|
566
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
567
|
-
agent_id TEXT,
|
|
568
|
-
session_id TEXT,
|
|
569
|
-
content TEXT NOT NULL,
|
|
570
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
571
|
-
);
|
|
572
|
-
|
|
573
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
574
|
-
id TEXT PRIMARY KEY,
|
|
575
|
-
agent_id TEXT,
|
|
576
|
-
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
577
|
-
working_dir TEXT,
|
|
578
|
-
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
579
|
-
last_activity TEXT NOT NULL DEFAULT (datetime('now')),
|
|
580
|
-
metadata TEXT DEFAULT '{}'
|
|
581
|
-
);
|
|
582
|
-
|
|
583
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
|
584
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id);
|
|
585
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
586
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
|
|
587
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_assigned ON tasks(assigned_to);
|
|
588
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_agent ON tasks(agent_id);
|
|
589
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
|
|
590
|
-
CREATE INDEX IF NOT EXISTS idx_comments_task ON task_comments(task_id);
|
|
591
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id);
|
|
592
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
593
|
-
|
|
594
|
-
CREATE TABLE IF NOT EXISTS _migrations (
|
|
595
|
-
id INTEGER PRIMARY KEY,
|
|
596
|
-
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
597
|
-
);
|
|
598
|
-
|
|
599
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (1);
|
|
600
|
-
`,
|
|
601
|
-
`
|
|
602
|
-
ALTER TABLE projects ADD COLUMN task_list_id TEXT;
|
|
603
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (2);
|
|
604
|
-
`,
|
|
605
|
-
`
|
|
606
|
-
CREATE TABLE IF NOT EXISTS task_tags (
|
|
607
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
608
|
-
tag TEXT NOT NULL,
|
|
609
|
-
PRIMARY KEY (task_id, tag)
|
|
610
|
-
);
|
|
611
|
-
CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag);
|
|
612
|
-
CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id);
|
|
613
|
-
|
|
614
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (3);
|
|
615
|
-
`,
|
|
616
|
-
`
|
|
617
|
-
CREATE TABLE IF NOT EXISTS plans (
|
|
618
|
-
id TEXT PRIMARY KEY,
|
|
619
|
-
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
620
|
-
name TEXT NOT NULL,
|
|
621
|
-
description TEXT,
|
|
622
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
623
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
624
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
625
|
-
);
|
|
626
|
-
CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id);
|
|
627
|
-
CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status);
|
|
628
|
-
ALTER TABLE tasks ADD COLUMN plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL;
|
|
629
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id);
|
|
630
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (4);
|
|
631
|
-
`,
|
|
632
|
-
`
|
|
633
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
634
|
-
id TEXT PRIMARY KEY,
|
|
635
|
-
name TEXT NOT NULL UNIQUE,
|
|
636
|
-
description TEXT,
|
|
637
|
-
metadata TEXT DEFAULT '{}',
|
|
638
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
639
|
-
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
640
|
-
);
|
|
641
|
-
CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name);
|
|
642
|
-
|
|
643
|
-
CREATE TABLE IF NOT EXISTS task_lists (
|
|
644
|
-
id TEXT PRIMARY KEY,
|
|
645
|
-
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
646
|
-
slug TEXT NOT NULL,
|
|
647
|
-
name TEXT NOT NULL,
|
|
648
|
-
description TEXT,
|
|
649
|
-
metadata TEXT DEFAULT '{}',
|
|
650
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
651
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
652
|
-
UNIQUE(project_id, slug)
|
|
653
|
-
);
|
|
654
|
-
CREATE INDEX IF NOT EXISTS idx_task_lists_project ON task_lists(project_id);
|
|
655
|
-
CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug);
|
|
656
|
-
|
|
657
|
-
ALTER TABLE tasks ADD COLUMN task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL;
|
|
658
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id);
|
|
659
|
-
|
|
660
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (5);
|
|
661
|
-
`,
|
|
662
|
-
`
|
|
663
|
-
ALTER TABLE projects ADD COLUMN task_prefix TEXT;
|
|
664
|
-
ALTER TABLE projects ADD COLUMN task_counter INTEGER NOT NULL DEFAULT 0;
|
|
665
|
-
|
|
666
|
-
ALTER TABLE tasks ADD COLUMN short_id TEXT;
|
|
667
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL;
|
|
668
|
-
|
|
669
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (6);
|
|
670
|
-
`,
|
|
671
|
-
`
|
|
672
|
-
ALTER TABLE tasks ADD COLUMN due_at TEXT;
|
|
673
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at);
|
|
674
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (7);
|
|
675
|
-
`,
|
|
676
|
-
`
|
|
677
|
-
ALTER TABLE agents ADD COLUMN role TEXT DEFAULT 'agent';
|
|
678
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (8);
|
|
679
|
-
`,
|
|
680
|
-
`
|
|
681
|
-
ALTER TABLE plans ADD COLUMN task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL;
|
|
682
|
-
ALTER TABLE plans ADD COLUMN agent_id TEXT;
|
|
683
|
-
CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id);
|
|
684
|
-
CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id);
|
|
685
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (9);
|
|
686
|
-
`,
|
|
687
|
-
`
|
|
688
|
-
CREATE TABLE IF NOT EXISTS task_history (
|
|
689
|
-
id TEXT PRIMARY KEY,
|
|
690
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
691
|
-
action TEXT NOT NULL,
|
|
692
|
-
field TEXT,
|
|
693
|
-
old_value TEXT,
|
|
694
|
-
new_value TEXT,
|
|
695
|
-
agent_id TEXT,
|
|
696
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
697
|
-
);
|
|
698
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id);
|
|
699
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(agent_id);
|
|
700
|
-
|
|
701
|
-
CREATE TABLE IF NOT EXISTS webhooks (
|
|
702
|
-
id TEXT PRIMARY KEY,
|
|
703
|
-
url TEXT NOT NULL,
|
|
704
|
-
events TEXT NOT NULL DEFAULT '[]',
|
|
705
|
-
secret TEXT,
|
|
706
|
-
active INTEGER NOT NULL DEFAULT 1,
|
|
707
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
708
|
-
);
|
|
709
|
-
|
|
710
|
-
CREATE TABLE IF NOT EXISTS task_templates (
|
|
711
|
-
id TEXT PRIMARY KEY,
|
|
712
|
-
name TEXT NOT NULL,
|
|
713
|
-
title_pattern TEXT NOT NULL,
|
|
714
|
-
description TEXT,
|
|
715
|
-
priority TEXT DEFAULT 'medium',
|
|
716
|
-
tags TEXT DEFAULT '[]',
|
|
717
|
-
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
718
|
-
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
719
|
-
metadata TEXT DEFAULT '{}',
|
|
720
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
721
|
-
);
|
|
722
|
-
|
|
723
|
-
ALTER TABLE tasks ADD COLUMN estimated_minutes INTEGER;
|
|
724
|
-
ALTER TABLE tasks ADD COLUMN requires_approval INTEGER NOT NULL DEFAULT 0;
|
|
725
|
-
ALTER TABLE tasks ADD COLUMN approved_by TEXT;
|
|
726
|
-
ALTER TABLE tasks ADD COLUMN approved_at TEXT;
|
|
727
|
-
|
|
728
|
-
ALTER TABLE agents ADD COLUMN permissions TEXT DEFAULT '["*"]';
|
|
729
|
-
|
|
730
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (10);
|
|
731
|
-
`,
|
|
732
|
-
`
|
|
733
|
-
ALTER TABLE agents ADD COLUMN reports_to TEXT;
|
|
734
|
-
ALTER TABLE agents ADD COLUMN title TEXT;
|
|
735
|
-
ALTER TABLE agents ADD COLUMN level TEXT;
|
|
736
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (11);
|
|
737
|
-
`,
|
|
738
|
-
`
|
|
739
|
-
CREATE TABLE IF NOT EXISTS orgs (
|
|
740
|
-
id TEXT PRIMARY KEY,
|
|
741
|
-
name TEXT NOT NULL UNIQUE,
|
|
742
|
-
description TEXT,
|
|
743
|
-
metadata TEXT DEFAULT '{}',
|
|
744
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
745
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
746
|
-
);
|
|
747
|
-
ALTER TABLE agents ADD COLUMN org_id TEXT REFERENCES orgs(id) ON DELETE SET NULL;
|
|
748
|
-
ALTER TABLE projects ADD COLUMN org_id TEXT REFERENCES orgs(id) ON DELETE SET NULL;
|
|
749
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (12);
|
|
750
|
-
`,
|
|
751
|
-
`
|
|
752
|
-
ALTER TABLE tasks ADD COLUMN recurrence_rule TEXT;
|
|
753
|
-
ALTER TABLE tasks ADD COLUMN recurrence_parent_id TEXT REFERENCES tasks(id) ON DELETE SET NULL;
|
|
754
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id);
|
|
755
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL;
|
|
756
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (13);
|
|
757
|
-
`,
|
|
758
|
-
`
|
|
759
|
-
ALTER TABLE task_comments ADD COLUMN type TEXT DEFAULT 'comment' CHECK(type IN ('comment', 'progress', 'note'));
|
|
760
|
-
ALTER TABLE task_comments ADD COLUMN progress_pct INTEGER CHECK(progress_pct IS NULL OR (progress_pct >= 0 AND progress_pct <= 100));
|
|
761
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (14);
|
|
762
|
-
`,
|
|
763
|
-
`
|
|
764
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
|
|
765
|
-
task_id UNINDEXED,
|
|
766
|
-
title,
|
|
767
|
-
description,
|
|
768
|
-
tags,
|
|
769
|
-
tokenize='unicode61 remove_diacritics 2'
|
|
770
|
-
);
|
|
771
|
-
|
|
772
|
-
INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
|
|
773
|
-
SELECT t.rowid, t.id, t.title, COALESCE(t.description, ''),
|
|
774
|
-
COALESCE((SELECT GROUP_CONCAT(tag, ' ') FROM task_tags WHERE task_id = t.id), '')
|
|
775
|
-
FROM tasks t;
|
|
776
|
-
|
|
777
|
-
CREATE TRIGGER IF NOT EXISTS tasks_fts_ai AFTER INSERT ON tasks BEGIN
|
|
778
|
-
INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
|
|
779
|
-
VALUES (new.rowid, new.id, new.title, COALESCE(new.description, ''), '');
|
|
780
|
-
END;
|
|
781
|
-
|
|
782
|
-
CREATE TRIGGER IF NOT EXISTS tasks_fts_ad AFTER DELETE ON tasks BEGIN
|
|
783
|
-
DELETE FROM tasks_fts WHERE rowid = old.rowid;
|
|
784
|
-
END;
|
|
785
|
-
|
|
786
|
-
CREATE TRIGGER IF NOT EXISTS tasks_fts_au AFTER UPDATE OF title, description ON tasks BEGIN
|
|
787
|
-
DELETE FROM tasks_fts WHERE rowid = old.rowid;
|
|
788
|
-
INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
|
|
789
|
-
SELECT new.rowid, new.id, new.title, COALESCE(new.description, ''),
|
|
790
|
-
COALESCE((SELECT GROUP_CONCAT(tag, ' ') FROM task_tags WHERE task_id = new.id), '');
|
|
791
|
-
END;
|
|
792
|
-
|
|
793
|
-
CREATE TRIGGER IF NOT EXISTS task_tags_fts_ai AFTER INSERT ON task_tags BEGIN
|
|
794
|
-
DELETE FROM tasks_fts WHERE rowid = (SELECT rowid FROM tasks WHERE id = new.task_id);
|
|
795
|
-
INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
|
|
796
|
-
SELECT t.rowid, t.id, t.title, COALESCE(t.description, ''),
|
|
797
|
-
COALESCE((SELECT GROUP_CONCAT(tag, ' ') FROM task_tags WHERE task_id = t.id), '')
|
|
798
|
-
FROM tasks t WHERE t.id = new.task_id;
|
|
799
|
-
END;
|
|
800
|
-
|
|
801
|
-
CREATE TRIGGER IF NOT EXISTS task_tags_fts_ad AFTER DELETE ON task_tags BEGIN
|
|
802
|
-
DELETE FROM tasks_fts WHERE rowid = (SELECT rowid FROM tasks WHERE id = old.task_id);
|
|
803
|
-
INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
|
|
804
|
-
SELECT t.rowid, t.id, t.title, COALESCE(t.description, ''),
|
|
805
|
-
COALESCE((SELECT GROUP_CONCAT(tag, ' ') FROM task_tags WHERE task_id = t.id), '')
|
|
806
|
-
FROM tasks t WHERE t.id = old.task_id;
|
|
807
|
-
END;
|
|
808
|
-
|
|
809
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (15);
|
|
810
|
-
`,
|
|
811
|
-
`
|
|
812
|
-
ALTER TABLE tasks ADD COLUMN spawns_template_id TEXT REFERENCES task_templates(id) ON DELETE SET NULL;
|
|
813
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (16);
|
|
814
|
-
`,
|
|
815
|
-
`
|
|
816
|
-
ALTER TABLE agents ADD COLUMN session_id TEXT;
|
|
817
|
-
ALTER TABLE agents ADD COLUMN working_dir TEXT;
|
|
818
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (17);
|
|
819
|
-
`,
|
|
820
|
-
`
|
|
821
|
-
ALTER TABLE tasks ADD COLUMN confidence REAL;
|
|
822
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (18);
|
|
823
|
-
`,
|
|
824
|
-
`
|
|
825
|
-
ALTER TABLE tasks ADD COLUMN reason TEXT;
|
|
826
|
-
ALTER TABLE tasks ADD COLUMN spawned_from_session TEXT;
|
|
827
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (19);
|
|
828
|
-
`,
|
|
829
|
-
`
|
|
830
|
-
CREATE TABLE IF NOT EXISTS handoffs (
|
|
831
|
-
id TEXT PRIMARY KEY,
|
|
832
|
-
agent_id TEXT,
|
|
833
|
-
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
834
|
-
summary TEXT NOT NULL,
|
|
835
|
-
completed TEXT,
|
|
836
|
-
in_progress TEXT,
|
|
837
|
-
blockers TEXT,
|
|
838
|
-
next_steps TEXT,
|
|
839
|
-
created_at TEXT NOT NULL
|
|
840
|
-
);
|
|
841
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (20);
|
|
842
|
-
`,
|
|
843
|
-
`
|
|
844
|
-
CREATE TABLE IF NOT EXISTS task_checklists (
|
|
845
|
-
id TEXT PRIMARY KEY,
|
|
846
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
847
|
-
position INTEGER NOT NULL DEFAULT 0,
|
|
848
|
-
text TEXT NOT NULL,
|
|
849
|
-
checked INTEGER NOT NULL DEFAULT 0,
|
|
850
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
851
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
852
|
-
);
|
|
853
|
-
CREATE INDEX IF NOT EXISTS idx_task_checklists_task ON task_checklists(task_id);
|
|
854
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (21);
|
|
855
|
-
`,
|
|
856
|
-
`
|
|
857
|
-
CREATE TABLE IF NOT EXISTS project_sources (
|
|
858
|
-
id TEXT PRIMARY KEY,
|
|
859
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
860
|
-
type TEXT NOT NULL,
|
|
861
|
-
name TEXT NOT NULL,
|
|
862
|
-
uri TEXT NOT NULL,
|
|
863
|
-
description TEXT,
|
|
864
|
-
metadata TEXT DEFAULT '{}',
|
|
865
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
866
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
867
|
-
);
|
|
868
|
-
CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id);
|
|
869
|
-
CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type);
|
|
870
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (22);
|
|
871
|
-
`,
|
|
872
|
-
`
|
|
873
|
-
ALTER TABLE agents ADD COLUMN active_project_id TEXT;
|
|
874
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (23);
|
|
875
|
-
`,
|
|
876
|
-
`
|
|
877
|
-
CREATE TABLE IF NOT EXISTS resource_locks (
|
|
878
|
-
resource_type TEXT NOT NULL,
|
|
879
|
-
resource_id TEXT NOT NULL,
|
|
880
|
-
agent_id TEXT NOT NULL,
|
|
881
|
-
lock_type TEXT NOT NULL DEFAULT 'advisory',
|
|
882
|
-
locked_at TEXT NOT NULL,
|
|
883
|
-
expires_at TEXT NOT NULL,
|
|
884
|
-
UNIQUE(resource_type, resource_id, lock_type)
|
|
885
|
-
);
|
|
886
|
-
CREATE INDEX IF NOT EXISTS idx_resource_locks_type_id ON resource_locks(resource_type, resource_id);
|
|
887
|
-
CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
|
|
888
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (24);
|
|
889
|
-
`,
|
|
890
|
-
`
|
|
891
|
-
CREATE TABLE IF NOT EXISTS task_files (
|
|
892
|
-
id TEXT PRIMARY KEY,
|
|
893
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
894
|
-
path TEXT NOT NULL,
|
|
895
|
-
status TEXT NOT NULL DEFAULT 'active',
|
|
896
|
-
agent_id TEXT,
|
|
897
|
-
note TEXT,
|
|
898
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
899
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
900
|
-
);
|
|
901
|
-
CREATE INDEX IF NOT EXISTS idx_task_files_task ON task_files(task_id);
|
|
902
|
-
CREATE INDEX IF NOT EXISTS idx_task_files_path ON task_files(path);
|
|
903
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_files_task_path ON task_files(task_id, path);
|
|
904
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (25);
|
|
905
|
-
`,
|
|
906
|
-
`
|
|
907
|
-
ALTER TABLE tasks ADD COLUMN assigned_by TEXT;
|
|
908
|
-
ALTER TABLE tasks ADD COLUMN assigned_from_project TEXT;
|
|
909
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by);
|
|
910
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (26);
|
|
911
|
-
`,
|
|
912
|
-
`
|
|
913
|
-
CREATE TABLE IF NOT EXISTS task_relationships (
|
|
914
|
-
id TEXT PRIMARY KEY,
|
|
915
|
-
source_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
916
|
-
target_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
917
|
-
relationship_type TEXT NOT NULL CHECK(relationship_type IN ('related_to', 'conflicts_with', 'similar_to', 'duplicates', 'supersedes', 'modifies_same_file')),
|
|
918
|
-
metadata TEXT DEFAULT '{}',
|
|
919
|
-
created_by TEXT,
|
|
920
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
921
|
-
CHECK (source_task_id != target_task_id)
|
|
922
|
-
);
|
|
923
|
-
CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id);
|
|
924
|
-
CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id);
|
|
925
|
-
CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type);
|
|
926
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (27);
|
|
927
|
-
`,
|
|
928
|
-
`
|
|
929
|
-
CREATE TABLE IF NOT EXISTS kg_edges (
|
|
930
|
-
id TEXT PRIMARY KEY,
|
|
931
|
-
source_id TEXT NOT NULL,
|
|
932
|
-
source_type TEXT NOT NULL,
|
|
933
|
-
target_id TEXT NOT NULL,
|
|
934
|
-
target_type TEXT NOT NULL,
|
|
935
|
-
relation_type TEXT NOT NULL,
|
|
936
|
-
weight REAL NOT NULL DEFAULT 1.0,
|
|
937
|
-
metadata TEXT DEFAULT '{}',
|
|
938
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
939
|
-
UNIQUE(source_id, source_type, target_id, target_type, relation_type)
|
|
940
|
-
);
|
|
941
|
-
CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type);
|
|
942
|
-
CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type);
|
|
943
|
-
CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type);
|
|
944
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (28);
|
|
945
|
-
`,
|
|
946
|
-
`
|
|
947
|
-
ALTER TABLE agents ADD COLUMN capabilities TEXT DEFAULT '[]';
|
|
948
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (29);
|
|
949
|
-
`,
|
|
950
|
-
`
|
|
951
|
-
ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
|
|
952
|
-
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
|
|
953
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (30);
|
|
954
|
-
`,
|
|
955
|
-
`
|
|
956
|
-
CREATE TABLE IF NOT EXISTS project_agent_roles (
|
|
957
|
-
id TEXT PRIMARY KEY,
|
|
958
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
959
|
-
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
960
|
-
role TEXT NOT NULL,
|
|
961
|
-
is_lead INTEGER NOT NULL DEFAULT 0,
|
|
962
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
963
|
-
UNIQUE(project_id, agent_id, role)
|
|
964
|
-
);
|
|
965
|
-
CREATE INDEX IF NOT EXISTS idx_project_agent_roles_project ON project_agent_roles(project_id);
|
|
966
|
-
CREATE INDEX IF NOT EXISTS idx_project_agent_roles_agent ON project_agent_roles(agent_id);
|
|
967
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (31);
|
|
968
|
-
`,
|
|
969
|
-
`
|
|
970
|
-
CREATE TABLE IF NOT EXISTS task_commits (
|
|
971
|
-
id TEXT PRIMARY KEY,
|
|
972
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
973
|
-
sha TEXT NOT NULL,
|
|
974
|
-
message TEXT,
|
|
975
|
-
author TEXT,
|
|
976
|
-
files_changed TEXT,
|
|
977
|
-
committed_at TEXT,
|
|
978
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
979
|
-
UNIQUE(task_id, sha)
|
|
980
|
-
);
|
|
981
|
-
CREATE INDEX IF NOT EXISTS idx_task_commits_task ON task_commits(task_id);
|
|
982
|
-
CREATE INDEX IF NOT EXISTS idx_task_commits_sha ON task_commits(sha);
|
|
983
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (32);
|
|
984
|
-
`,
|
|
985
|
-
`
|
|
986
|
-
CREATE TABLE IF NOT EXISTS file_locks (
|
|
987
|
-
id TEXT PRIMARY KEY,
|
|
988
|
-
path TEXT NOT NULL UNIQUE,
|
|
989
|
-
agent_id TEXT NOT NULL,
|
|
990
|
-
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
991
|
-
expires_at TEXT NOT NULL,
|
|
992
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
993
|
-
);
|
|
994
|
-
CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
|
|
995
|
-
CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
|
|
996
|
-
CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
|
|
997
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (33);
|
|
998
|
-
`,
|
|
999
|
-
`
|
|
1000
|
-
ALTER TABLE tasks ADD COLUMN started_at TEXT;
|
|
1001
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (34);
|
|
1002
|
-
`,
|
|
1003
|
-
`
|
|
1004
|
-
ALTER TABLE tasks ADD COLUMN task_type TEXT;
|
|
1005
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type);
|
|
1006
|
-
ALTER TABLE tasks ADD COLUMN cost_tokens INTEGER DEFAULT 0;
|
|
1007
|
-
ALTER TABLE tasks ADD COLUMN cost_usd REAL DEFAULT 0;
|
|
1008
|
-
ALTER TABLE tasks ADD COLUMN delegated_from TEXT;
|
|
1009
|
-
ALTER TABLE tasks ADD COLUMN delegation_depth INTEGER DEFAULT 0;
|
|
1010
|
-
ALTER TABLE tasks ADD COLUMN retry_count INTEGER DEFAULT 0;
|
|
1011
|
-
ALTER TABLE tasks ADD COLUMN max_retries INTEGER DEFAULT 3;
|
|
1012
|
-
ALTER TABLE tasks ADD COLUMN retry_after TEXT;
|
|
1013
|
-
ALTER TABLE tasks ADD COLUMN sla_minutes INTEGER;
|
|
1014
|
-
|
|
1015
|
-
CREATE TABLE IF NOT EXISTS task_traces (
|
|
1016
|
-
id TEXT PRIMARY KEY,
|
|
1017
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1018
|
-
agent_id TEXT,
|
|
1019
|
-
trace_type TEXT NOT NULL CHECK(trace_type IN ('tool_call','llm_call','error','handoff','custom')),
|
|
1020
|
-
name TEXT,
|
|
1021
|
-
input_summary TEXT,
|
|
1022
|
-
output_summary TEXT,
|
|
1023
|
-
duration_ms INTEGER,
|
|
1024
|
-
tokens INTEGER,
|
|
1025
|
-
cost_usd REAL,
|
|
1026
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1027
|
-
);
|
|
1028
|
-
CREATE INDEX IF NOT EXISTS idx_task_traces_task ON task_traces(task_id);
|
|
1029
|
-
CREATE INDEX IF NOT EXISTS idx_task_traces_agent ON task_traces(agent_id);
|
|
1030
|
-
|
|
1031
|
-
CREATE TABLE IF NOT EXISTS context_snapshots (
|
|
1032
|
-
id TEXT PRIMARY KEY,
|
|
1033
|
-
agent_id TEXT,
|
|
1034
|
-
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1035
|
-
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1036
|
-
snapshot_type TEXT NOT NULL CHECK(snapshot_type IN ('interrupt','complete','handoff','checkpoint')),
|
|
1037
|
-
plan_summary TEXT,
|
|
1038
|
-
files_open TEXT DEFAULT '[]',
|
|
1039
|
-
attempts TEXT DEFAULT '[]',
|
|
1040
|
-
blockers TEXT DEFAULT '[]',
|
|
1041
|
-
next_steps TEXT,
|
|
1042
|
-
metadata TEXT DEFAULT '{}',
|
|
1043
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1044
|
-
);
|
|
1045
|
-
CREATE INDEX IF NOT EXISTS idx_snapshots_agent ON context_snapshots(agent_id);
|
|
1046
|
-
CREATE INDEX IF NOT EXISTS idx_snapshots_task ON context_snapshots(task_id);
|
|
1047
|
-
|
|
1048
|
-
CREATE TABLE IF NOT EXISTS agent_budgets (
|
|
1049
|
-
agent_id TEXT PRIMARY KEY,
|
|
1050
|
-
max_concurrent INTEGER DEFAULT 5,
|
|
1051
|
-
max_cost_usd REAL,
|
|
1052
|
-
max_task_minutes INTEGER,
|
|
1053
|
-
period_hours INTEGER DEFAULT 24,
|
|
1054
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1055
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1056
|
-
);
|
|
1057
|
-
|
|
1058
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (35);
|
|
1059
|
-
`,
|
|
1060
|
-
`
|
|
1061
|
-
CREATE TABLE IF NOT EXISTS feedback (
|
|
1062
|
-
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
1063
|
-
message TEXT NOT NULL,
|
|
1064
|
-
email TEXT,
|
|
1065
|
-
category TEXT DEFAULT 'general',
|
|
1066
|
-
version TEXT,
|
|
1067
|
-
machine_id TEXT,
|
|
1068
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1069
|
-
);
|
|
1070
|
-
|
|
1071
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (36);
|
|
1072
|
-
`,
|
|
1073
|
-
`
|
|
1074
|
-
CREATE TABLE IF NOT EXISTS template_tasks (
|
|
1075
|
-
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
1076
|
-
template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
|
|
1077
|
-
position INTEGER NOT NULL,
|
|
1078
|
-
title_pattern TEXT NOT NULL,
|
|
1079
|
-
description TEXT,
|
|
1080
|
-
priority TEXT DEFAULT 'medium',
|
|
1081
|
-
tags TEXT DEFAULT '[]',
|
|
1082
|
-
task_type TEXT,
|
|
1083
|
-
depends_on_positions TEXT DEFAULT '[]',
|
|
1084
|
-
metadata TEXT DEFAULT '{}',
|
|
1085
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1086
|
-
);
|
|
1087
|
-
CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id);
|
|
1088
|
-
|
|
1089
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (37);
|
|
1090
|
-
`,
|
|
1091
|
-
`
|
|
1092
|
-
ALTER TABLE task_templates ADD COLUMN variables TEXT DEFAULT '[]';
|
|
1093
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (38);
|
|
1094
|
-
`,
|
|
1095
|
-
`
|
|
1096
|
-
ALTER TABLE template_tasks ADD COLUMN condition TEXT;
|
|
1097
|
-
ALTER TABLE template_tasks ADD COLUMN include_template_id TEXT;
|
|
1098
|
-
ALTER TABLE task_templates ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
|
|
1099
|
-
|
|
1100
|
-
CREATE TABLE IF NOT EXISTS template_versions (
|
|
1101
|
-
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
1102
|
-
template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
|
|
1103
|
-
version INTEGER NOT NULL,
|
|
1104
|
-
snapshot TEXT NOT NULL,
|
|
1105
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1106
|
-
);
|
|
1107
|
-
CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id);
|
|
1108
|
-
|
|
1109
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (39);
|
|
1110
|
-
`
|
|
1111
|
-
];
|
|
1112
|
-
});
|
|
1113
|
-
|
|
1114
|
-
// src/db/projects.ts
|
|
1115
|
-
var exports_projects = {};
|
|
1116
|
-
__export(exports_projects, {
|
|
1117
|
-
updateProject: () => updateProject,
|
|
1118
|
-
slugify: () => slugify,
|
|
1119
|
-
removeProjectSource: () => removeProjectSource,
|
|
1120
|
-
nextTaskShortId: () => nextTaskShortId,
|
|
1121
|
-
listProjects: () => listProjects,
|
|
1122
|
-
listProjectSources: () => listProjectSources,
|
|
1123
|
-
getProjectWithSources: () => getProjectWithSources,
|
|
1124
|
-
getProjectByPath: () => getProjectByPath,
|
|
1125
|
-
getProject: () => getProject,
|
|
1126
|
-
ensureProject: () => ensureProject,
|
|
1127
|
-
deleteProject: () => deleteProject,
|
|
1128
|
-
createProject: () => createProject,
|
|
1129
|
-
addProjectSource: () => addProjectSource
|
|
1130
|
-
});
|
|
1131
|
-
function slugify(name) {
|
|
1132
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1133
|
-
}
|
|
1134
|
-
function generatePrefix(name, db) {
|
|
1135
|
-
const words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
|
|
1136
|
-
let prefix;
|
|
1137
|
-
if (words.length >= 3) {
|
|
1138
|
-
prefix = words.slice(0, 3).map((w) => w[0].toUpperCase()).join("");
|
|
1139
|
-
} else if (words.length === 2) {
|
|
1140
|
-
prefix = (words[0].slice(0, 2) + words[1][0]).toUpperCase();
|
|
1141
|
-
} else {
|
|
1142
|
-
prefix = words[0].slice(0, 3).toUpperCase();
|
|
1143
|
-
}
|
|
1144
|
-
let candidate = prefix;
|
|
1145
|
-
let suffix = 1;
|
|
1146
|
-
while (true) {
|
|
1147
|
-
const existing = db.query("SELECT id FROM projects WHERE task_prefix = ?").get(candidate);
|
|
1148
|
-
if (!existing)
|
|
1149
|
-
return candidate;
|
|
1150
|
-
suffix++;
|
|
1151
|
-
candidate = `${prefix}${suffix}`;
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
function createProject(input, db) {
|
|
1155
|
-
const d = db || getDatabase();
|
|
1156
|
-
const id = uuid();
|
|
1157
|
-
const timestamp = now();
|
|
1158
|
-
const taskListId = input.task_list_id ?? `todos-${slugify(input.name)}`;
|
|
1159
|
-
const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
|
|
1160
|
-
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at)
|
|
1161
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp]);
|
|
1162
|
-
return getProject(id, d);
|
|
1163
|
-
}
|
|
1164
|
-
function getProject(id, db) {
|
|
1165
|
-
const d = db || getDatabase();
|
|
1166
|
-
const row = d.query("SELECT * FROM projects WHERE id = ?").get(id);
|
|
1167
|
-
return row;
|
|
1168
|
-
}
|
|
1169
|
-
function getProjectByPath(path, db) {
|
|
1170
|
-
const d = db || getDatabase();
|
|
1171
|
-
const row = d.query("SELECT * FROM projects WHERE path = ?").get(path);
|
|
1172
|
-
return row;
|
|
1173
|
-
}
|
|
1174
|
-
function listProjects(db) {
|
|
1175
|
-
const d = db || getDatabase();
|
|
1176
|
-
return d.query("SELECT * FROM projects ORDER BY name").all();
|
|
1177
|
-
}
|
|
1178
|
-
function updateProject(id, input, db) {
|
|
1179
|
-
const d = db || getDatabase();
|
|
1180
|
-
const project = getProject(id, d);
|
|
1181
|
-
if (!project)
|
|
1182
|
-
throw new ProjectNotFoundError(id);
|
|
1183
|
-
const sets = ["updated_at = ?"];
|
|
1184
|
-
const params = [now()];
|
|
1185
|
-
if (input.name !== undefined) {
|
|
1186
|
-
sets.push("name = ?");
|
|
1187
|
-
params.push(input.name);
|
|
1188
|
-
}
|
|
1189
|
-
if (input.description !== undefined) {
|
|
1190
|
-
sets.push("description = ?");
|
|
1191
|
-
params.push(input.description);
|
|
1192
|
-
}
|
|
1193
|
-
if (input.task_list_id !== undefined) {
|
|
1194
|
-
sets.push("task_list_id = ?");
|
|
1195
|
-
params.push(input.task_list_id);
|
|
1196
|
-
}
|
|
1197
|
-
params.push(id);
|
|
1198
|
-
d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
1199
|
-
return getProject(id, d);
|
|
1200
|
-
}
|
|
1201
|
-
function deleteProject(id, db) {
|
|
1202
|
-
const d = db || getDatabase();
|
|
1203
|
-
const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
|
|
1204
|
-
return result.changes > 0;
|
|
1205
|
-
}
|
|
1206
|
-
function rowToSource(row) {
|
|
1207
|
-
return {
|
|
1208
|
-
...row,
|
|
1209
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : {}
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1212
|
-
function addProjectSource(input, db) {
|
|
1213
|
-
const d = db || getDatabase();
|
|
1214
|
-
const id = uuid();
|
|
1215
|
-
const timestamp = now();
|
|
1216
|
-
d.run(`INSERT INTO project_sources (id, project_id, type, name, uri, description, metadata, created_at, updated_at)
|
|
1217
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1218
|
-
id,
|
|
1219
|
-
input.project_id,
|
|
1220
|
-
input.type,
|
|
1221
|
-
input.name,
|
|
1222
|
-
input.uri,
|
|
1223
|
-
input.description || null,
|
|
1224
|
-
JSON.stringify(input.metadata || {}),
|
|
1225
|
-
timestamp,
|
|
1226
|
-
timestamp
|
|
1227
|
-
]);
|
|
1228
|
-
return rowToSource(d.query("SELECT * FROM project_sources WHERE id = ?").get(id));
|
|
1229
|
-
}
|
|
1230
|
-
function removeProjectSource(id, db) {
|
|
1231
|
-
const d = db || getDatabase();
|
|
1232
|
-
const result = d.run("DELETE FROM project_sources WHERE id = ?", [id]);
|
|
1233
|
-
return result.changes > 0;
|
|
1234
|
-
}
|
|
1235
|
-
function listProjectSources(projectId, db) {
|
|
1236
|
-
const d = db || getDatabase();
|
|
1237
|
-
const rows = d.query("SELECT * FROM project_sources WHERE project_id = ? ORDER BY name").all(projectId);
|
|
1238
|
-
return rows.map(rowToSource);
|
|
1239
|
-
}
|
|
1240
|
-
function getProjectWithSources(id, db) {
|
|
1241
|
-
const d = db || getDatabase();
|
|
1242
|
-
const project = getProject(id, d);
|
|
1243
|
-
if (!project)
|
|
1244
|
-
return null;
|
|
1245
|
-
project.sources = listProjectSources(id, d);
|
|
1246
|
-
return project;
|
|
1247
|
-
}
|
|
1248
|
-
function nextTaskShortId(projectId, db) {
|
|
1249
|
-
const d = db || getDatabase();
|
|
1250
|
-
const project = getProject(projectId, d);
|
|
1251
|
-
if (!project || !project.task_prefix)
|
|
1252
|
-
return null;
|
|
1253
|
-
d.run("UPDATE projects SET task_counter = task_counter + 1, updated_at = ? WHERE id = ?", [now(), projectId]);
|
|
1254
|
-
const updated = getProject(projectId, d);
|
|
1255
|
-
const padded = String(updated.task_counter).padStart(5, "0");
|
|
1256
|
-
return `${updated.task_prefix}-${padded}`;
|
|
1257
|
-
}
|
|
1258
|
-
function ensureProject(name, path, db) {
|
|
1259
|
-
const d = db || getDatabase();
|
|
1260
|
-
const existing = getProjectByPath(path, d);
|
|
1261
|
-
if (existing) {
|
|
1262
|
-
if (!existing.task_prefix) {
|
|
1263
|
-
const prefix = generatePrefix(existing.name, d);
|
|
1264
|
-
d.run("UPDATE projects SET task_prefix = ?, updated_at = ? WHERE id = ?", [prefix, now(), existing.id]);
|
|
1265
|
-
return getProject(existing.id, d);
|
|
1266
|
-
}
|
|
1267
|
-
return existing;
|
|
1268
|
-
}
|
|
1269
|
-
return createProject({ name, path }, d);
|
|
1270
|
-
}
|
|
1271
|
-
var init_projects = __esm(() => {
|
|
1272
|
-
init_types();
|
|
1273
|
-
init_database();
|
|
1274
|
-
});
|
|
1275
|
-
|
|
1276
|
-
// src/lib/sync-utils.ts
|
|
1277
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
1278
|
-
function readJsonFile(path) {
|
|
1279
|
-
try {
|
|
1280
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
1281
|
-
} catch {
|
|
1282
|
-
return null;
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
var HOME;
|
|
1286
|
-
var init_sync_utils = __esm(() => {
|
|
1287
|
-
HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
1288
|
-
});
|
|
1289
|
-
|
|
1290
|
-
// src/lib/config.ts
|
|
1291
|
-
import { existsSync as existsSync3 } from "fs";
|
|
1292
|
-
import { join as join2 } from "path";
|
|
1293
|
-
function getTodosGlobalDir() {
|
|
1294
|
-
const home = process.env["HOME"] || HOME;
|
|
1295
|
-
const newDir = join2(home, ".hasna", "todos");
|
|
1296
|
-
const legacyDir = join2(home, ".todos");
|
|
1297
|
-
if (!existsSync3(newDir) && existsSync3(legacyDir))
|
|
1298
|
-
return legacyDir;
|
|
1299
|
-
return newDir;
|
|
1300
|
-
}
|
|
1301
|
-
function getConfigPath() {
|
|
1302
|
-
return join2(getTodosGlobalDir(), "config.json");
|
|
1303
|
-
}
|
|
1304
|
-
function loadConfig() {
|
|
1305
|
-
if (cached)
|
|
1306
|
-
return cached;
|
|
1307
|
-
if (!existsSync3(getConfigPath())) {
|
|
1308
|
-
cached = {};
|
|
1309
|
-
return cached;
|
|
1310
|
-
}
|
|
1311
|
-
const config = readJsonFile(getConfigPath()) || {};
|
|
1312
|
-
if (typeof config.sync_agents === "string") {
|
|
1313
|
-
config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
|
|
1314
|
-
}
|
|
1315
|
-
cached = config;
|
|
1316
|
-
return cached;
|
|
1317
|
-
}
|
|
1318
|
-
function getCompletionGuardConfig(projectPath) {
|
|
1319
|
-
const config = loadConfig();
|
|
1320
|
-
const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
|
|
1321
|
-
if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
|
|
1322
|
-
return { ...global, ...config.project_overrides[projectPath].completion_guard };
|
|
1323
|
-
}
|
|
1324
|
-
return global;
|
|
1325
|
-
}
|
|
1326
|
-
var cached = null, GUARD_DEFAULTS;
|
|
1327
|
-
var init_config = __esm(() => {
|
|
1328
|
-
init_sync_utils();
|
|
1329
|
-
GUARD_DEFAULTS = {
|
|
1330
|
-
enabled: false,
|
|
1331
|
-
min_work_seconds: 30,
|
|
1332
|
-
max_completions_per_window: 5,
|
|
1333
|
-
window_minutes: 10,
|
|
1334
|
-
cooldown_seconds: 60
|
|
1335
|
-
};
|
|
1336
|
-
});
|
|
1337
|
-
|
|
1338
|
-
// src/lib/completion-guard.ts
|
|
1339
|
-
function checkCompletionGuard(task, agentId, db, configOverride) {
|
|
1340
|
-
let config;
|
|
1341
|
-
if (configOverride) {
|
|
1342
|
-
config = configOverride;
|
|
1343
|
-
} else {
|
|
1344
|
-
const projectPath = task.project_id ? getProject(task.project_id, db)?.path : null;
|
|
1345
|
-
config = getCompletionGuardConfig(projectPath);
|
|
1346
|
-
}
|
|
1347
|
-
if (!config.enabled)
|
|
1348
|
-
return;
|
|
1349
|
-
if (task.status !== "in_progress") {
|
|
1350
|
-
throw new CompletionGuardError(`Task must be in 'in_progress' status before completing (current: '${task.status}'). Use start_task first.`);
|
|
1351
|
-
}
|
|
1352
|
-
const agent = agentId || task.assigned_to || task.agent_id;
|
|
1353
|
-
if (config.min_work_seconds && task.locked_at) {
|
|
1354
|
-
const startedAt = new Date(task.locked_at).getTime();
|
|
1355
|
-
const elapsedSeconds = (Date.now() - startedAt) / 1000;
|
|
1356
|
-
if (elapsedSeconds < config.min_work_seconds) {
|
|
1357
|
-
const remaining = Math.ceil(config.min_work_seconds - elapsedSeconds);
|
|
1358
|
-
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);
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
if (agent && config.max_completions_per_window && config.window_minutes) {
|
|
1362
|
-
const windowStart = new Date(Date.now() - config.window_minutes * 60 * 1000).toISOString();
|
|
1363
|
-
const result = db.query(`SELECT COUNT(*) as count FROM tasks
|
|
1364
|
-
WHERE completed_at > ? AND (assigned_to = ? OR agent_id = ?)`).get(windowStart, agent, agent);
|
|
1365
|
-
if (result.count >= config.max_completions_per_window) {
|
|
1366
|
-
throw new CompletionGuardError(`Rate limit: ${result.count} tasks completed in the last ${config.window_minutes} minutes (max ${config.max_completions_per_window}). Slow down.`);
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
if (agent && config.cooldown_seconds) {
|
|
1370
|
-
const result = db.query(`SELECT MAX(completed_at) as last_completed FROM tasks
|
|
1371
|
-
WHERE completed_at IS NOT NULL AND (assigned_to = ? OR agent_id = ?) AND id != ?`).get(agent, agent, task.id);
|
|
1372
|
-
if (result.last_completed) {
|
|
1373
|
-
const elapsedSeconds = (Date.now() - new Date(result.last_completed).getTime()) / 1000;
|
|
1374
|
-
if (elapsedSeconds < config.cooldown_seconds) {
|
|
1375
|
-
const remaining = Math.ceil(config.cooldown_seconds - elapsedSeconds);
|
|
1376
|
-
throw new CompletionGuardError(`Cooldown: last completion was ${Math.floor(elapsedSeconds)}s ago. Wait ${remaining}s between completions.`, remaining);
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
var init_completion_guard = __esm(() => {
|
|
1382
|
-
init_types();
|
|
1383
|
-
init_config();
|
|
1384
|
-
init_projects();
|
|
1385
|
-
});
|
|
1386
|
-
|
|
1387
|
-
// src/db/audit.ts
|
|
1388
|
-
var exports_audit = {};
|
|
1389
|
-
__export(exports_audit, {
|
|
1390
|
-
logTaskChange: () => logTaskChange,
|
|
1391
|
-
getTaskHistory: () => getTaskHistory,
|
|
1392
|
-
getRecentActivity: () => getRecentActivity,
|
|
1393
|
-
getRecap: () => getRecap
|
|
1394
|
-
});
|
|
1395
|
-
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
1396
|
-
const d = db || getDatabase();
|
|
1397
|
-
const id = uuid();
|
|
1398
|
-
const timestamp = now();
|
|
1399
|
-
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
|
|
1400
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
|
|
1401
|
-
return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
|
|
1402
|
-
}
|
|
1403
|
-
function getTaskHistory(taskId, db) {
|
|
1404
|
-
const d = db || getDatabase();
|
|
1405
|
-
return d.query("SELECT * FROM task_history WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
|
|
1406
|
-
}
|
|
1407
|
-
function getRecentActivity(limit = 50, db) {
|
|
1408
|
-
const d = db || getDatabase();
|
|
1409
|
-
return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
1410
|
-
}
|
|
1411
|
-
function getRecap(hours = 8, projectId, db) {
|
|
1412
|
-
const d = db || getDatabase();
|
|
1413
|
-
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
1414
|
-
const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
1415
|
-
const pf = projectId ? " AND project_id = ?" : "";
|
|
1416
|
-
const tpf = projectId ? " AND t.project_id = ?" : "";
|
|
1417
|
-
const completed = projectId ? d.query(`SELECT id, short_id, title, assigned_to, completed_at, started_at FROM tasks WHERE status = 'completed' AND completed_at > ?${pf} ORDER BY completed_at DESC`).all(since, projectId) : d.query(`SELECT id, short_id, title, assigned_to, completed_at, started_at FROM tasks WHERE status = 'completed' AND completed_at > ? ORDER BY completed_at DESC`).all(since);
|
|
1418
|
-
const created = projectId ? d.query(`SELECT id, short_id, title, agent_id, created_at FROM tasks WHERE created_at > ?${pf} ORDER BY created_at DESC`).all(since, projectId) : d.query(`SELECT id, short_id, title, agent_id, created_at FROM tasks WHERE created_at > ? ORDER BY created_at DESC`).all(since);
|
|
1419
|
-
const in_progress = projectId ? d.query(`SELECT id, short_id, title, assigned_to, started_at FROM tasks WHERE status = 'in_progress' AND project_id = ? ORDER BY updated_at DESC`).all(projectId) : d.query(`SELECT id, short_id, title, assigned_to, started_at FROM tasks WHERE status = 'in_progress' ORDER BY updated_at DESC`).all();
|
|
1420
|
-
const blocked = projectId ? d.query(`SELECT DISTINCT t.id, t.short_id, t.title, t.assigned_to FROM tasks t JOIN task_dependencies td ON td.task_id = t.id JOIN tasks dep ON dep.id = td.depends_on AND dep.status NOT IN ('completed','cancelled') WHERE t.status = 'pending'${tpf}`).all(projectId) : d.query(`SELECT DISTINCT t.id, t.short_id, t.title, t.assigned_to FROM tasks t JOIN task_dependencies td ON td.task_id = t.id JOIN tasks dep ON dep.id = td.depends_on AND dep.status NOT IN ('completed','cancelled') WHERE t.status = 'pending'`).all();
|
|
1421
|
-
const stale = projectId ? d.query(`SELECT id, short_id, title, assigned_to, updated_at FROM tasks WHERE status = 'in_progress' AND updated_at < ? AND project_id = ? ORDER BY updated_at ASC`).all(staleWindow, projectId) : d.query(`SELECT id, short_id, title, assigned_to, updated_at FROM tasks WHERE status = 'in_progress' AND updated_at < ? ORDER BY updated_at ASC`).all(staleWindow);
|
|
1422
|
-
const agents = projectId ? d.query(`SELECT a.name, a.last_seen_at, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'completed' AND t.completed_at > ?${tpf}) as completed_count, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'in_progress'${tpf}) as in_progress_count FROM agents a WHERE a.status = 'active' AND a.last_seen_at > ? ORDER BY completed_count DESC`).all(since, projectId, projectId, since) : d.query(`SELECT a.name, a.last_seen_at, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'completed' AND t.completed_at > ?) as completed_count, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'in_progress') as in_progress_count FROM agents a WHERE a.status = 'active' AND a.last_seen_at > ? ORDER BY completed_count DESC`).all(since, since);
|
|
1423
|
-
return {
|
|
1424
|
-
hours,
|
|
1425
|
-
since,
|
|
1426
|
-
completed: completed.map((r) => ({
|
|
1427
|
-
...r,
|
|
1428
|
-
duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
|
|
1429
|
-
})),
|
|
1430
|
-
created,
|
|
1431
|
-
in_progress,
|
|
1432
|
-
blocked,
|
|
1433
|
-
stale,
|
|
1434
|
-
agents
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
var init_audit = __esm(() => {
|
|
1438
|
-
init_database();
|
|
1439
|
-
});
|
|
1440
|
-
|
|
1441
|
-
// src/lib/recurrence.ts
|
|
1442
|
-
function parseRecurrenceRule(rule) {
|
|
1443
|
-
const normalized = rule.trim().toLowerCase();
|
|
1444
|
-
if (normalized === "every weekday" || normalized === "every weekdays") {
|
|
1445
|
-
return { type: "specific_days", days: [1, 2, 3, 4, 5] };
|
|
1446
|
-
}
|
|
1447
|
-
if (normalized === "every day" || normalized === "daily") {
|
|
1448
|
-
return { type: "interval", interval: 1, unit: "day" };
|
|
1449
|
-
}
|
|
1450
|
-
if (normalized === "every week" || normalized === "weekly") {
|
|
1451
|
-
return { type: "interval", interval: 1, unit: "week" };
|
|
1452
|
-
}
|
|
1453
|
-
if (normalized === "every month" || normalized === "monthly") {
|
|
1454
|
-
return { type: "interval", interval: 1, unit: "month" };
|
|
1455
|
-
}
|
|
1456
|
-
const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
|
|
1457
|
-
if (intervalMatch) {
|
|
1458
|
-
return {
|
|
1459
|
-
type: "interval",
|
|
1460
|
-
interval: parseInt(intervalMatch[1], 10),
|
|
1461
|
-
unit: intervalMatch[2]
|
|
1462
|
-
};
|
|
1463
|
-
}
|
|
1464
|
-
const daysMatch = normalized.match(/^every\s+(.+)$/);
|
|
1465
|
-
if (daysMatch) {
|
|
1466
|
-
const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
|
|
1467
|
-
const days = [];
|
|
1468
|
-
for (const part of dayParts) {
|
|
1469
|
-
const dayNum = DAY_NAMES[part];
|
|
1470
|
-
if (dayNum !== undefined) {
|
|
1471
|
-
days.push(dayNum);
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
if (days.length > 0) {
|
|
1475
|
-
return { type: "specific_days", days: days.sort((a, b) => a - b) };
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
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"`);
|
|
1479
|
-
}
|
|
1480
|
-
function nextOccurrence(rule, from) {
|
|
1481
|
-
const parsed = parseRecurrenceRule(rule);
|
|
1482
|
-
const base = from || new Date;
|
|
1483
|
-
if (parsed.type === "interval") {
|
|
1484
|
-
const next = new Date(base);
|
|
1485
|
-
if (parsed.unit === "day") {
|
|
1486
|
-
next.setDate(next.getDate() + parsed.interval);
|
|
1487
|
-
} else if (parsed.unit === "week") {
|
|
1488
|
-
next.setDate(next.getDate() + parsed.interval * 7);
|
|
1489
|
-
} else if (parsed.unit === "month") {
|
|
1490
|
-
next.setMonth(next.getMonth() + parsed.interval);
|
|
1491
|
-
}
|
|
1492
|
-
return next.toISOString();
|
|
1493
|
-
}
|
|
1494
|
-
if (parsed.type === "specific_days") {
|
|
1495
|
-
const currentDay = base.getDay();
|
|
1496
|
-
const days = parsed.days;
|
|
1497
|
-
let daysToAdd = Infinity;
|
|
1498
|
-
for (const day of days) {
|
|
1499
|
-
let diff = day - currentDay;
|
|
1500
|
-
if (diff <= 0)
|
|
1501
|
-
diff += 7;
|
|
1502
|
-
if (diff < daysToAdd)
|
|
1503
|
-
daysToAdd = diff;
|
|
1504
|
-
}
|
|
1505
|
-
const next = new Date(base);
|
|
1506
|
-
next.setDate(next.getDate() + daysToAdd);
|
|
1507
|
-
return next.toISOString();
|
|
1508
|
-
}
|
|
1509
|
-
throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
|
|
1510
|
-
}
|
|
1511
|
-
var DAY_NAMES;
|
|
1512
|
-
var init_recurrence = __esm(() => {
|
|
1513
|
-
DAY_NAMES = {
|
|
1514
|
-
sunday: 0,
|
|
1515
|
-
sun: 0,
|
|
1516
|
-
monday: 1,
|
|
1517
|
-
mon: 1,
|
|
1518
|
-
tuesday: 2,
|
|
1519
|
-
tue: 2,
|
|
1520
|
-
wednesday: 3,
|
|
1521
|
-
wed: 3,
|
|
1522
|
-
thursday: 4,
|
|
1523
|
-
thu: 4,
|
|
1524
|
-
friday: 5,
|
|
1525
|
-
fri: 5,
|
|
1526
|
-
saturday: 6,
|
|
1527
|
-
sat: 6
|
|
1528
|
-
};
|
|
1529
|
-
});
|
|
1530
|
-
|
|
1531
|
-
// src/db/webhooks.ts
|
|
1532
|
-
var exports_webhooks = {};
|
|
1533
|
-
__export(exports_webhooks, {
|
|
1534
|
-
listWebhooks: () => listWebhooks,
|
|
1535
|
-
listDeliveries: () => listDeliveries,
|
|
1536
|
-
getWebhook: () => getWebhook,
|
|
1537
|
-
dispatchWebhook: () => dispatchWebhook,
|
|
1538
|
-
deleteWebhook: () => deleteWebhook,
|
|
1539
|
-
createWebhook: () => createWebhook
|
|
1540
|
-
});
|
|
1541
|
-
function rowToWebhook(row) {
|
|
1542
|
-
return {
|
|
1543
|
-
...row,
|
|
1544
|
-
events: JSON.parse(row.events || "[]"),
|
|
1545
|
-
active: !!row.active,
|
|
1546
|
-
project_id: row.project_id || null,
|
|
1547
|
-
task_list_id: row.task_list_id || null,
|
|
1548
|
-
agent_id: row.agent_id || null,
|
|
1549
|
-
task_id: row.task_id || null
|
|
1550
|
-
};
|
|
1551
|
-
}
|
|
1552
|
-
function createWebhook(input, db) {
|
|
1553
|
-
const d = db || getDatabase();
|
|
1554
|
-
const id = uuid();
|
|
1555
|
-
d.run(`INSERT INTO webhooks (id, url, events, secret, project_id, task_list_id, agent_id, task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1556
|
-
id,
|
|
1557
|
-
input.url,
|
|
1558
|
-
JSON.stringify(input.events || []),
|
|
1559
|
-
input.secret || null,
|
|
1560
|
-
input.project_id || null,
|
|
1561
|
-
input.task_list_id || null,
|
|
1562
|
-
input.agent_id || null,
|
|
1563
|
-
input.task_id || null,
|
|
1564
|
-
now()
|
|
1565
|
-
]);
|
|
1566
|
-
return getWebhook(id, d);
|
|
1567
|
-
}
|
|
1568
|
-
function getWebhook(id, db) {
|
|
1569
|
-
const d = db || getDatabase();
|
|
1570
|
-
const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
|
|
1571
|
-
return row ? rowToWebhook(row) : null;
|
|
1572
|
-
}
|
|
1573
|
-
function listWebhooks(db) {
|
|
1574
|
-
const d = db || getDatabase();
|
|
1575
|
-
return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
|
|
1576
|
-
}
|
|
1577
|
-
function deleteWebhook(id, db) {
|
|
1578
|
-
const d = db || getDatabase();
|
|
1579
|
-
return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
|
|
1580
|
-
}
|
|
1581
|
-
function listDeliveries(webhookId, limit = 50, db) {
|
|
1582
|
-
const d = db || getDatabase();
|
|
1583
|
-
if (webhookId) {
|
|
1584
|
-
return d.query("SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY created_at DESC LIMIT ?").all(webhookId, limit);
|
|
1585
|
-
}
|
|
1586
|
-
return d.query("SELECT * FROM webhook_deliveries ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
1587
|
-
}
|
|
1588
|
-
function logDelivery(d, webhookId, event, payload, statusCode, response, attempt) {
|
|
1589
|
-
const id = uuid();
|
|
1590
|
-
d.run(`INSERT INTO webhook_deliveries (id, webhook_id, event, payload, status_code, response, attempt, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, webhookId, event, payload, statusCode, response, attempt, now()]);
|
|
1591
|
-
}
|
|
1592
|
-
function matchesScope(wh, payload) {
|
|
1593
|
-
if (wh.project_id && payload.project_id !== wh.project_id)
|
|
1594
|
-
return false;
|
|
1595
|
-
if (wh.task_list_id && payload.task_list_id !== wh.task_list_id)
|
|
1596
|
-
return false;
|
|
1597
|
-
if (wh.agent_id && payload.agent_id !== wh.agent_id && payload.assigned_to !== wh.agent_id)
|
|
1598
|
-
return false;
|
|
1599
|
-
if (wh.task_id && payload.id !== wh.task_id)
|
|
1600
|
-
return false;
|
|
1601
|
-
return true;
|
|
1602
|
-
}
|
|
1603
|
-
async function deliverWebhook(wh, event, body, attempt, db) {
|
|
1604
|
-
try {
|
|
1605
|
-
const headers = { "Content-Type": "application/json" };
|
|
1606
|
-
if (wh.secret) {
|
|
1607
|
-
const encoder = new TextEncoder;
|
|
1608
|
-
const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
1609
|
-
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
1610
|
-
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1611
|
-
}
|
|
1612
|
-
const resp = await fetch(wh.url, { method: "POST", headers, body });
|
|
1613
|
-
const respText = await resp.text().catch(() => "");
|
|
1614
|
-
logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
|
|
1615
|
-
if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
|
|
1616
|
-
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
1617
|
-
setTimeout(() => {
|
|
1618
|
-
deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
|
|
1619
|
-
}, delay);
|
|
1620
|
-
}
|
|
1621
|
-
} catch (err) {
|
|
1622
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1623
|
-
logDelivery(db, wh.id, event, body, null, errorMsg.slice(0, 1000), attempt);
|
|
1624
|
-
if (attempt < MAX_RETRY_ATTEMPTS) {
|
|
1625
|
-
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
1626
|
-
setTimeout(() => {
|
|
1627
|
-
deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
|
|
1628
|
-
}, delay);
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
async function dispatchWebhook(event, payload, db) {
|
|
1633
|
-
const d = db || getDatabase();
|
|
1634
|
-
const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
|
|
1635
|
-
const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
|
|
1636
|
-
for (const wh of webhooks) {
|
|
1637
|
-
if (!matchesScope(wh, payloadObj))
|
|
1638
|
-
continue;
|
|
1639
|
-
const body = JSON.stringify({ event, payload, timestamp: now() });
|
|
1640
|
-
deliverWebhook(wh, event, body, 1, d).catch(() => {});
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
var MAX_RETRY_ATTEMPTS = 3, RETRY_BASE_DELAY_MS = 1000;
|
|
1644
|
-
var init_webhooks = __esm(() => {
|
|
1645
|
-
init_database();
|
|
1646
|
-
});
|
|
1647
|
-
|
|
1648
|
-
// src/db/templates.ts
|
|
1649
|
-
var exports_templates = {};
|
|
1650
|
-
__export(exports_templates, {
|
|
1651
|
-
updateTemplate: () => updateTemplate,
|
|
1652
|
-
tasksFromTemplate: () => tasksFromTemplate,
|
|
1653
|
-
taskFromTemplate: () => taskFromTemplate,
|
|
1654
|
-
resolveVariables: () => resolveVariables,
|
|
1655
|
-
previewTemplate: () => previewTemplate,
|
|
1656
|
-
listTemplates: () => listTemplates,
|
|
1657
|
-
listTemplateVersions: () => listTemplateVersions,
|
|
1658
|
-
importTemplate: () => importTemplate,
|
|
1659
|
-
getTemplateWithTasks: () => getTemplateWithTasks,
|
|
1660
|
-
getTemplateVersion: () => getTemplateVersion,
|
|
1661
|
-
getTemplateTasks: () => getTemplateTasks,
|
|
1662
|
-
getTemplate: () => getTemplate,
|
|
1663
|
-
exportTemplate: () => exportTemplate,
|
|
1664
|
-
evaluateCondition: () => evaluateCondition,
|
|
1665
|
-
deleteTemplate: () => deleteTemplate,
|
|
1666
|
-
createTemplate: () => createTemplate,
|
|
1667
|
-
addTemplateTasks: () => addTemplateTasks
|
|
1668
|
-
});
|
|
1669
|
-
function rowToTemplate(row) {
|
|
1670
|
-
return {
|
|
1671
|
-
...row,
|
|
1672
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
1673
|
-
variables: JSON.parse(row.variables || "[]"),
|
|
1674
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
1675
|
-
priority: row.priority || "medium",
|
|
1676
|
-
version: row.version ?? 1
|
|
1677
|
-
};
|
|
1678
|
-
}
|
|
1679
|
-
function rowToTemplateTask(row) {
|
|
1680
|
-
return {
|
|
1681
|
-
...row,
|
|
1682
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
1683
|
-
depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
|
|
1684
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
1685
|
-
priority: row.priority || "medium",
|
|
1686
|
-
condition: row.condition ?? null,
|
|
1687
|
-
include_template_id: row.include_template_id ?? null
|
|
1688
|
-
};
|
|
1689
|
-
}
|
|
1690
|
-
function resolveTemplateId(id, d) {
|
|
1691
|
-
return resolvePartialId(d, "task_templates", id);
|
|
1692
|
-
}
|
|
1693
|
-
function createTemplate(input, db) {
|
|
1694
|
-
const d = db || getDatabase();
|
|
1695
|
-
const id = uuid();
|
|
1696
|
-
d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
|
|
1697
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1698
|
-
id,
|
|
1699
|
-
input.name,
|
|
1700
|
-
input.title_pattern,
|
|
1701
|
-
input.description || null,
|
|
1702
|
-
input.priority || "medium",
|
|
1703
|
-
JSON.stringify(input.tags || []),
|
|
1704
|
-
JSON.stringify(input.variables || []),
|
|
1705
|
-
input.project_id || null,
|
|
1706
|
-
input.plan_id || null,
|
|
1707
|
-
JSON.stringify(input.metadata || {}),
|
|
1708
|
-
now()
|
|
1709
|
-
]);
|
|
1710
|
-
if (input.tasks && input.tasks.length > 0) {
|
|
1711
|
-
addTemplateTasks(id, input.tasks, d);
|
|
1712
|
-
}
|
|
1713
|
-
return getTemplate(id, d);
|
|
1714
|
-
}
|
|
1715
|
-
function getTemplate(id, db) {
|
|
1716
|
-
const d = db || getDatabase();
|
|
1717
|
-
const resolved = resolveTemplateId(id, d);
|
|
1718
|
-
if (!resolved)
|
|
1719
|
-
return null;
|
|
1720
|
-
const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
|
|
1721
|
-
return row ? rowToTemplate(row) : null;
|
|
1722
|
-
}
|
|
1723
|
-
function listTemplates(db) {
|
|
1724
|
-
const d = db || getDatabase();
|
|
1725
|
-
return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
|
|
1726
|
-
}
|
|
1727
|
-
function deleteTemplate(id, db) {
|
|
1728
|
-
const d = db || getDatabase();
|
|
1729
|
-
const resolved = resolveTemplateId(id, d);
|
|
1730
|
-
if (!resolved)
|
|
1731
|
-
return false;
|
|
1732
|
-
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
1733
|
-
}
|
|
1734
|
-
function updateTemplate(id, updates, db) {
|
|
1735
|
-
const d = db || getDatabase();
|
|
1736
|
-
const resolved = resolveTemplateId(id, d);
|
|
1737
|
-
if (!resolved)
|
|
1738
|
-
return null;
|
|
1739
|
-
const current = getTemplateWithTasks(resolved, d);
|
|
1740
|
-
if (current) {
|
|
1741
|
-
const snapshot = JSON.stringify({
|
|
1742
|
-
name: current.name,
|
|
1743
|
-
title_pattern: current.title_pattern,
|
|
1744
|
-
description: current.description,
|
|
1745
|
-
priority: current.priority,
|
|
1746
|
-
tags: current.tags,
|
|
1747
|
-
variables: current.variables,
|
|
1748
|
-
project_id: current.project_id,
|
|
1749
|
-
plan_id: current.plan_id,
|
|
1750
|
-
metadata: current.metadata,
|
|
1751
|
-
tasks: current.tasks
|
|
1752
|
-
});
|
|
1753
|
-
d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
|
|
1754
|
-
}
|
|
1755
|
-
const sets = ["version = version + 1"];
|
|
1756
|
-
const values = [];
|
|
1757
|
-
if (updates.name !== undefined) {
|
|
1758
|
-
sets.push("name = ?");
|
|
1759
|
-
values.push(updates.name);
|
|
1760
|
-
}
|
|
1761
|
-
if (updates.title_pattern !== undefined) {
|
|
1762
|
-
sets.push("title_pattern = ?");
|
|
1763
|
-
values.push(updates.title_pattern);
|
|
1764
|
-
}
|
|
1765
|
-
if (updates.description !== undefined) {
|
|
1766
|
-
sets.push("description = ?");
|
|
1767
|
-
values.push(updates.description);
|
|
1768
|
-
}
|
|
1769
|
-
if (updates.priority !== undefined) {
|
|
1770
|
-
sets.push("priority = ?");
|
|
1771
|
-
values.push(updates.priority);
|
|
1772
|
-
}
|
|
1773
|
-
if (updates.tags !== undefined) {
|
|
1774
|
-
sets.push("tags = ?");
|
|
1775
|
-
values.push(JSON.stringify(updates.tags));
|
|
1776
|
-
}
|
|
1777
|
-
if (updates.variables !== undefined) {
|
|
1778
|
-
sets.push("variables = ?");
|
|
1779
|
-
values.push(JSON.stringify(updates.variables));
|
|
1780
|
-
}
|
|
1781
|
-
if (updates.project_id !== undefined) {
|
|
1782
|
-
sets.push("project_id = ?");
|
|
1783
|
-
values.push(updates.project_id);
|
|
1784
|
-
}
|
|
1785
|
-
if (updates.plan_id !== undefined) {
|
|
1786
|
-
sets.push("plan_id = ?");
|
|
1787
|
-
values.push(updates.plan_id);
|
|
1788
|
-
}
|
|
1789
|
-
if (updates.metadata !== undefined) {
|
|
1790
|
-
sets.push("metadata = ?");
|
|
1791
|
-
values.push(JSON.stringify(updates.metadata));
|
|
1792
|
-
}
|
|
1793
|
-
values.push(resolved);
|
|
1794
|
-
d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
|
|
1795
|
-
return getTemplate(resolved, d);
|
|
1796
|
-
}
|
|
1797
|
-
function taskFromTemplate(templateId, overrides = {}, db) {
|
|
1798
|
-
const t = getTemplate(templateId, db);
|
|
1799
|
-
if (!t)
|
|
1800
|
-
throw new Error(`Template not found: ${templateId}`);
|
|
1801
|
-
return {
|
|
1802
|
-
title: overrides.title || t.title_pattern,
|
|
1803
|
-
description: overrides.description ?? t.description ?? undefined,
|
|
1804
|
-
priority: overrides.priority ?? t.priority,
|
|
1805
|
-
tags: overrides.tags ?? t.tags,
|
|
1806
|
-
project_id: overrides.project_id ?? t.project_id ?? undefined,
|
|
1807
|
-
plan_id: overrides.plan_id ?? t.plan_id ?? undefined,
|
|
1808
|
-
metadata: overrides.metadata ?? t.metadata,
|
|
1809
|
-
...overrides
|
|
1810
|
-
};
|
|
1811
|
-
}
|
|
1812
|
-
function addTemplateTasks(templateId, tasks, db) {
|
|
1813
|
-
const d = db || getDatabase();
|
|
1814
|
-
const template = getTemplate(templateId, d);
|
|
1815
|
-
if (!template)
|
|
1816
|
-
throw new Error(`Template not found: ${templateId}`);
|
|
1817
|
-
d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
|
|
1818
|
-
const results = [];
|
|
1819
|
-
for (let i = 0;i < tasks.length; i++) {
|
|
1820
|
-
const task = tasks[i];
|
|
1821
|
-
const id = uuid();
|
|
1822
|
-
d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, condition, include_template_id, depends_on_positions, metadata, created_at)
|
|
1823
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1824
|
-
id,
|
|
1825
|
-
templateId,
|
|
1826
|
-
i,
|
|
1827
|
-
task.title_pattern,
|
|
1828
|
-
task.description || null,
|
|
1829
|
-
task.priority || "medium",
|
|
1830
|
-
JSON.stringify(task.tags || []),
|
|
1831
|
-
task.task_type || null,
|
|
1832
|
-
task.condition || null,
|
|
1833
|
-
task.include_template_id || null,
|
|
1834
|
-
JSON.stringify(task.depends_on || []),
|
|
1835
|
-
JSON.stringify(task.metadata || {}),
|
|
1836
|
-
now()
|
|
1837
|
-
]);
|
|
1838
|
-
const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
|
|
1839
|
-
if (row)
|
|
1840
|
-
results.push(rowToTemplateTask(row));
|
|
1841
|
-
}
|
|
1842
|
-
return results;
|
|
1843
|
-
}
|
|
1844
|
-
function getTemplateWithTasks(id, db) {
|
|
1845
|
-
const d = db || getDatabase();
|
|
1846
|
-
const template = getTemplate(id, d);
|
|
1847
|
-
if (!template)
|
|
1848
|
-
return null;
|
|
1849
|
-
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
|
|
1850
|
-
const tasks = rows.map(rowToTemplateTask);
|
|
1851
|
-
return { ...template, tasks };
|
|
1852
|
-
}
|
|
1853
|
-
function getTemplateTasks(templateId, db) {
|
|
1854
|
-
const d = db || getDatabase();
|
|
1855
|
-
const resolved = resolveTemplateId(templateId, d);
|
|
1856
|
-
if (!resolved)
|
|
1857
|
-
return [];
|
|
1858
|
-
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
|
|
1859
|
-
return rows.map(rowToTemplateTask);
|
|
1860
|
-
}
|
|
1861
|
-
function evaluateCondition(condition, variables) {
|
|
1862
|
-
if (!condition || condition.trim() === "")
|
|
1863
|
-
return true;
|
|
1864
|
-
const trimmed = condition.trim();
|
|
1865
|
-
const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
1866
|
-
if (eqMatch) {
|
|
1867
|
-
const varName = eqMatch[1];
|
|
1868
|
-
const expected = eqMatch[2].trim();
|
|
1869
|
-
return (variables[varName] ?? "") === expected;
|
|
1870
|
-
}
|
|
1871
|
-
const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
1872
|
-
if (neqMatch) {
|
|
1873
|
-
const varName = neqMatch[1];
|
|
1874
|
-
const expected = neqMatch[2].trim();
|
|
1875
|
-
return (variables[varName] ?? "") !== expected;
|
|
1876
|
-
}
|
|
1877
|
-
const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
|
|
1878
|
-
if (falsyMatch) {
|
|
1879
|
-
const varName = falsyMatch[1];
|
|
1880
|
-
const val = variables[varName];
|
|
1881
|
-
return !val || val === "" || val === "false";
|
|
1882
|
-
}
|
|
1883
|
-
const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
|
|
1884
|
-
if (truthyMatch) {
|
|
1885
|
-
const varName = truthyMatch[1];
|
|
1886
|
-
const val = variables[varName];
|
|
1887
|
-
return !!val && val !== "" && val !== "false";
|
|
1888
|
-
}
|
|
1889
|
-
return true;
|
|
1890
|
-
}
|
|
1891
|
-
function exportTemplate(id, db) {
|
|
1892
|
-
const d = db || getDatabase();
|
|
1893
|
-
const template = getTemplateWithTasks(id, d);
|
|
1894
|
-
if (!template)
|
|
1895
|
-
throw new Error(`Template not found: ${id}`);
|
|
1896
|
-
return {
|
|
1897
|
-
name: template.name,
|
|
1898
|
-
title_pattern: template.title_pattern,
|
|
1899
|
-
description: template.description,
|
|
1900
|
-
priority: template.priority,
|
|
1901
|
-
tags: template.tags,
|
|
1902
|
-
variables: template.variables,
|
|
1903
|
-
project_id: template.project_id,
|
|
1904
|
-
plan_id: template.plan_id,
|
|
1905
|
-
metadata: template.metadata,
|
|
1906
|
-
tasks: template.tasks.map((t) => ({
|
|
1907
|
-
position: t.position,
|
|
1908
|
-
title_pattern: t.title_pattern,
|
|
1909
|
-
description: t.description,
|
|
1910
|
-
priority: t.priority,
|
|
1911
|
-
tags: t.tags,
|
|
1912
|
-
task_type: t.task_type,
|
|
1913
|
-
condition: t.condition,
|
|
1914
|
-
include_template_id: t.include_template_id,
|
|
1915
|
-
depends_on_positions: t.depends_on_positions,
|
|
1916
|
-
metadata: t.metadata
|
|
1917
|
-
}))
|
|
1918
|
-
};
|
|
1919
|
-
}
|
|
1920
|
-
function importTemplate(json, db) {
|
|
1921
|
-
const d = db || getDatabase();
|
|
1922
|
-
const taskInputs = (json.tasks || []).map((t) => ({
|
|
1923
|
-
title_pattern: t.title_pattern,
|
|
1924
|
-
description: t.description ?? undefined,
|
|
1925
|
-
priority: t.priority,
|
|
1926
|
-
tags: t.tags,
|
|
1927
|
-
task_type: t.task_type ?? undefined,
|
|
1928
|
-
condition: t.condition ?? undefined,
|
|
1929
|
-
include_template_id: t.include_template_id ?? undefined,
|
|
1930
|
-
depends_on: t.depends_on_positions,
|
|
1931
|
-
metadata: t.metadata
|
|
1932
|
-
}));
|
|
1933
|
-
return createTemplate({
|
|
1934
|
-
name: json.name,
|
|
1935
|
-
title_pattern: json.title_pattern,
|
|
1936
|
-
description: json.description ?? undefined,
|
|
1937
|
-
priority: json.priority,
|
|
1938
|
-
tags: json.tags,
|
|
1939
|
-
variables: json.variables,
|
|
1940
|
-
project_id: json.project_id ?? undefined,
|
|
1941
|
-
plan_id: json.plan_id ?? undefined,
|
|
1942
|
-
metadata: json.metadata,
|
|
1943
|
-
tasks: taskInputs
|
|
1944
|
-
}, d);
|
|
1945
|
-
}
|
|
1946
|
-
function getTemplateVersion(id, version, db) {
|
|
1947
|
-
const d = db || getDatabase();
|
|
1948
|
-
const resolved = resolveTemplateId(id, d);
|
|
1949
|
-
if (!resolved)
|
|
1950
|
-
return null;
|
|
1951
|
-
const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
|
|
1952
|
-
return row || null;
|
|
1953
|
-
}
|
|
1954
|
-
function listTemplateVersions(id, db) {
|
|
1955
|
-
const d = db || getDatabase();
|
|
1956
|
-
const resolved = resolveTemplateId(id, d);
|
|
1957
|
-
if (!resolved)
|
|
1958
|
-
return [];
|
|
1959
|
-
return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
|
|
1960
|
-
}
|
|
1961
|
-
function resolveVariables(templateVars, provided) {
|
|
1962
|
-
const merged = { ...provided };
|
|
1963
|
-
for (const v of templateVars) {
|
|
1964
|
-
if (merged[v.name] === undefined && v.default !== undefined) {
|
|
1965
|
-
merged[v.name] = v.default;
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
const missing = [];
|
|
1969
|
-
for (const v of templateVars) {
|
|
1970
|
-
if (v.required && merged[v.name] === undefined) {
|
|
1971
|
-
missing.push(v.name);
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
if (missing.length > 0) {
|
|
1975
|
-
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
1976
|
-
}
|
|
1977
|
-
return merged;
|
|
1978
|
-
}
|
|
1979
|
-
function substituteVars(text, variables) {
|
|
1980
|
-
let result = text;
|
|
1981
|
-
for (const [key, val] of Object.entries(variables)) {
|
|
1982
|
-
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
|
|
1983
|
-
}
|
|
1984
|
-
return result;
|
|
1985
|
-
}
|
|
1986
|
-
function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
|
|
1987
|
-
const d = db || getDatabase();
|
|
1988
|
-
const template = getTemplateWithTasks(templateId, d);
|
|
1989
|
-
if (!template)
|
|
1990
|
-
throw new Error(`Template not found: ${templateId}`);
|
|
1991
|
-
const visited = _visitedTemplateIds || new Set;
|
|
1992
|
-
if (visited.has(template.id)) {
|
|
1993
|
-
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
1994
|
-
}
|
|
1995
|
-
visited.add(template.id);
|
|
1996
|
-
const resolved = resolveVariables(template.variables, variables);
|
|
1997
|
-
if (template.tasks.length === 0) {
|
|
1998
|
-
const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
|
|
1999
|
-
const task = createTask(input, d);
|
|
2000
|
-
return [task];
|
|
2001
|
-
}
|
|
2002
|
-
const createdTasks = [];
|
|
2003
|
-
const positionToId = new Map;
|
|
2004
|
-
const skippedPositions = new Set;
|
|
2005
|
-
for (const tt of template.tasks) {
|
|
2006
|
-
if (tt.include_template_id) {
|
|
2007
|
-
const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
|
|
2008
|
-
createdTasks.push(...includedTasks);
|
|
2009
|
-
if (includedTasks.length > 0) {
|
|
2010
|
-
positionToId.set(tt.position, includedTasks[0].id);
|
|
2011
|
-
} else {
|
|
2012
|
-
skippedPositions.add(tt.position);
|
|
2013
|
-
}
|
|
2014
|
-
continue;
|
|
2015
|
-
}
|
|
2016
|
-
if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
|
|
2017
|
-
skippedPositions.add(tt.position);
|
|
2018
|
-
continue;
|
|
2019
|
-
}
|
|
2020
|
-
let title = tt.title_pattern;
|
|
2021
|
-
let desc = tt.description;
|
|
2022
|
-
title = substituteVars(title, resolved);
|
|
2023
|
-
if (desc)
|
|
2024
|
-
desc = substituteVars(desc, resolved);
|
|
2025
|
-
const task = createTask({
|
|
2026
|
-
title,
|
|
2027
|
-
description: desc ?? undefined,
|
|
2028
|
-
priority: tt.priority,
|
|
2029
|
-
tags: tt.tags,
|
|
2030
|
-
task_type: tt.task_type ?? undefined,
|
|
2031
|
-
project_id: projectId,
|
|
2032
|
-
task_list_id: taskListId,
|
|
2033
|
-
metadata: tt.metadata
|
|
2034
|
-
}, d);
|
|
2035
|
-
createdTasks.push(task);
|
|
2036
|
-
positionToId.set(tt.position, task.id);
|
|
2037
|
-
}
|
|
2038
|
-
for (const tt of template.tasks) {
|
|
2039
|
-
if (skippedPositions.has(tt.position))
|
|
2040
|
-
continue;
|
|
2041
|
-
if (tt.include_template_id)
|
|
2042
|
-
continue;
|
|
2043
|
-
const deps = tt.depends_on_positions;
|
|
2044
|
-
for (const depPos of deps) {
|
|
2045
|
-
if (skippedPositions.has(depPos))
|
|
2046
|
-
continue;
|
|
2047
|
-
const taskId = positionToId.get(tt.position);
|
|
2048
|
-
const depId = positionToId.get(depPos);
|
|
2049
|
-
if (taskId && depId) {
|
|
2050
|
-
addDependency(taskId, depId, d);
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
return createdTasks;
|
|
2055
|
-
}
|
|
2056
|
-
function previewTemplate(templateId, variables, db) {
|
|
2057
|
-
const d = db || getDatabase();
|
|
2058
|
-
const template = getTemplateWithTasks(templateId, d);
|
|
2059
|
-
if (!template)
|
|
2060
|
-
throw new Error(`Template not found: ${templateId}`);
|
|
2061
|
-
const resolved = resolveVariables(template.variables, variables);
|
|
2062
|
-
const tasks = [];
|
|
2063
|
-
if (template.tasks.length === 0) {
|
|
2064
|
-
tasks.push({
|
|
2065
|
-
position: 0,
|
|
2066
|
-
title: substituteVars(template.title_pattern, resolved),
|
|
2067
|
-
description: template.description ? substituteVars(template.description, resolved) : null,
|
|
2068
|
-
priority: template.priority,
|
|
2069
|
-
tags: template.tags,
|
|
2070
|
-
task_type: null,
|
|
2071
|
-
depends_on_positions: []
|
|
2072
|
-
});
|
|
2073
|
-
} else {
|
|
2074
|
-
for (const tt of template.tasks) {
|
|
2075
|
-
if (tt.condition && !evaluateCondition(tt.condition, resolved))
|
|
2076
|
-
continue;
|
|
2077
|
-
tasks.push({
|
|
2078
|
-
position: tt.position,
|
|
2079
|
-
title: substituteVars(tt.title_pattern, resolved),
|
|
2080
|
-
description: tt.description ? substituteVars(tt.description, resolved) : null,
|
|
2081
|
-
priority: tt.priority,
|
|
2082
|
-
tags: tt.tags,
|
|
2083
|
-
task_type: tt.task_type,
|
|
2084
|
-
depends_on_positions: tt.depends_on_positions
|
|
2085
|
-
});
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
return {
|
|
2089
|
-
template_id: template.id,
|
|
2090
|
-
template_name: template.name,
|
|
2091
|
-
description: template.description,
|
|
2092
|
-
variables: template.variables,
|
|
2093
|
-
resolved_variables: resolved,
|
|
2094
|
-
tasks
|
|
2095
|
-
};
|
|
2096
|
-
}
|
|
2097
|
-
var init_templates = __esm(() => {
|
|
2098
|
-
init_database();
|
|
2099
|
-
init_tasks();
|
|
2100
|
-
});
|
|
2101
|
-
|
|
2102
|
-
// src/db/checklists.ts
|
|
2103
|
-
function rowToItem(row) {
|
|
2104
|
-
return { ...row, checked: !!row.checked };
|
|
2105
|
-
}
|
|
2106
|
-
function getChecklist(taskId, db) {
|
|
2107
|
-
const d = db || getDatabase();
|
|
2108
|
-
const rows = d.query("SELECT * FROM task_checklists WHERE task_id = ? ORDER BY position, created_at").all(taskId);
|
|
2109
|
-
return rows.map(rowToItem);
|
|
2110
|
-
}
|
|
2111
|
-
var init_checklists = __esm(() => {
|
|
2112
|
-
init_database();
|
|
2113
|
-
});
|
|
2114
|
-
|
|
2115
|
-
// src/db/tasks.ts
|
|
2116
|
-
var exports_tasks = {};
|
|
2117
|
-
__export(exports_tasks, {
|
|
2118
|
-
updateTask: () => updateTask,
|
|
2119
|
-
unlockTask: () => unlockTask,
|
|
2120
|
-
stealTask: () => stealTask,
|
|
2121
|
-
startTask: () => startTask,
|
|
2122
|
-
setTaskStatus: () => setTaskStatus,
|
|
2123
|
-
setTaskPriority: () => setTaskPriority,
|
|
2124
|
-
removeDependency: () => removeDependency,
|
|
2125
|
-
redistributeStaleTasks: () => redistributeStaleTasks,
|
|
2126
|
-
moveTask: () => moveTask,
|
|
2127
|
-
logCost: () => logCost,
|
|
2128
|
-
lockTask: () => lockTask,
|
|
2129
|
-
listTasks: () => listTasks,
|
|
2130
|
-
getTasksChangedSince: () => getTasksChangedSince,
|
|
2131
|
-
getTaskWithRelations: () => getTaskWithRelations,
|
|
2132
|
-
getTaskStats: () => getTaskStats,
|
|
2133
|
-
getTaskGraph: () => getTaskGraph,
|
|
2134
|
-
getTaskDependents: () => getTaskDependents,
|
|
2135
|
-
getTaskDependencies: () => getTaskDependencies,
|
|
2136
|
-
getTask: () => getTask,
|
|
2137
|
-
getStatus: () => getStatus,
|
|
2138
|
-
getStaleTasks: () => getStaleTasks,
|
|
2139
|
-
getOverdueTasks: () => getOverdueTasks,
|
|
2140
|
-
getNextTask: () => getNextTask,
|
|
2141
|
-
getBlockingDeps: () => getBlockingDeps,
|
|
2142
|
-
getActiveWork: () => getActiveWork,
|
|
2143
|
-
failTask: () => failTask,
|
|
2144
|
-
deleteTask: () => deleteTask,
|
|
2145
|
-
decomposeTasks: () => decomposeTasks,
|
|
2146
|
-
createTask: () => createTask,
|
|
2147
|
-
countTasks: () => countTasks,
|
|
2148
|
-
completeTask: () => completeTask,
|
|
2149
|
-
cloneTask: () => cloneTask,
|
|
2150
|
-
claimOrSteal: () => claimOrSteal,
|
|
2151
|
-
claimNextTask: () => claimNextTask,
|
|
2152
|
-
bulkUpdateTasks: () => bulkUpdateTasks,
|
|
2153
|
-
bulkCreateTasks: () => bulkCreateTasks,
|
|
2154
|
-
addDependency: () => addDependency
|
|
2155
|
-
});
|
|
2156
|
-
function rowToTask(row) {
|
|
2157
|
-
return {
|
|
2158
|
-
...row,
|
|
2159
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
2160
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
2161
|
-
status: row.status,
|
|
2162
|
-
priority: row.priority,
|
|
2163
|
-
requires_approval: !!row.requires_approval
|
|
2164
|
-
};
|
|
2165
|
-
}
|
|
2166
|
-
function insertTaskTags(taskId, tags, db) {
|
|
2167
|
-
if (tags.length === 0)
|
|
2168
|
-
return;
|
|
2169
|
-
const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
2170
|
-
for (const tag of tags) {
|
|
2171
|
-
if (tag)
|
|
2172
|
-
stmt.run(taskId, tag);
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
function replaceTaskTags(taskId, tags, db) {
|
|
2176
|
-
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
2177
|
-
insertTaskTags(taskId, tags, db);
|
|
2178
|
-
}
|
|
2179
|
-
function createTask(input, db) {
|
|
2180
|
-
const d = db || getDatabase();
|
|
2181
|
-
const id = uuid();
|
|
2182
|
-
const timestamp = now();
|
|
2183
|
-
const tags = input.tags || [];
|
|
2184
|
-
const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
|
|
2185
|
-
const title = shortId ? `${shortId}: ${input.title}` : input.title;
|
|
2186
|
-
const assignedBy = input.assigned_by || input.agent_id;
|
|
2187
|
-
const assignedFromProject = input.assigned_from_project || null;
|
|
2188
|
-
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, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type)
|
|
2189
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2190
|
-
id,
|
|
2191
|
-
shortId,
|
|
2192
|
-
input.project_id || null,
|
|
2193
|
-
input.parent_id || null,
|
|
2194
|
-
input.plan_id || null,
|
|
2195
|
-
input.task_list_id || null,
|
|
2196
|
-
title,
|
|
2197
|
-
input.description || null,
|
|
2198
|
-
input.status || "pending",
|
|
2199
|
-
input.priority || "medium",
|
|
2200
|
-
input.agent_id || null,
|
|
2201
|
-
input.assigned_to || null,
|
|
2202
|
-
input.session_id || null,
|
|
2203
|
-
input.working_dir || null,
|
|
2204
|
-
JSON.stringify(tags),
|
|
2205
|
-
JSON.stringify(input.metadata || {}),
|
|
2206
|
-
timestamp,
|
|
2207
|
-
timestamp,
|
|
2208
|
-
input.due_at || null,
|
|
2209
|
-
input.estimated_minutes || null,
|
|
2210
|
-
input.requires_approval ? 1 : 0,
|
|
2211
|
-
null,
|
|
2212
|
-
null,
|
|
2213
|
-
input.recurrence_rule || null,
|
|
2214
|
-
input.recurrence_parent_id || null,
|
|
2215
|
-
input.spawns_template_id || null,
|
|
2216
|
-
input.reason || null,
|
|
2217
|
-
input.spawned_from_session || null,
|
|
2218
|
-
assignedBy || null,
|
|
2219
|
-
assignedFromProject || null,
|
|
2220
|
-
input.task_type || null
|
|
2221
|
-
]);
|
|
2222
|
-
if (tags.length > 0) {
|
|
2223
|
-
insertTaskTags(id, tags, d);
|
|
2224
|
-
}
|
|
2225
|
-
const task = getTask(id, d);
|
|
2226
|
-
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(() => {});
|
|
2227
|
-
return task;
|
|
2228
|
-
}
|
|
2229
|
-
function getTask(id, db) {
|
|
2230
|
-
const d = db || getDatabase();
|
|
2231
|
-
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
2232
|
-
if (!row)
|
|
2233
|
-
return null;
|
|
2234
|
-
return rowToTask(row);
|
|
2235
|
-
}
|
|
2236
|
-
function getTaskWithRelations(id, db) {
|
|
2237
|
-
const d = db || getDatabase();
|
|
2238
|
-
const task = getTask(id, d);
|
|
2239
|
-
if (!task)
|
|
2240
|
-
return null;
|
|
2241
|
-
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
2242
|
-
const subtasks = subtaskRows.map(rowToTask);
|
|
2243
|
-
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
2244
|
-
JOIN task_dependencies td ON td.depends_on = t.id
|
|
2245
|
-
WHERE td.task_id = ?`).all(id);
|
|
2246
|
-
const dependencies = depRows.map(rowToTask);
|
|
2247
|
-
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
2248
|
-
JOIN task_dependencies td ON td.task_id = t.id
|
|
2249
|
-
WHERE td.depends_on = ?`).all(id);
|
|
2250
|
-
const blocked_by = blockedByRows.map(rowToTask);
|
|
2251
|
-
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
2252
|
-
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
2253
|
-
const checklist = getChecklist(id, d);
|
|
2254
|
-
return {
|
|
2255
|
-
...task,
|
|
2256
|
-
subtasks,
|
|
2257
|
-
dependencies,
|
|
2258
|
-
blocked_by,
|
|
2259
|
-
comments,
|
|
2260
|
-
parent,
|
|
2261
|
-
checklist
|
|
2262
|
-
};
|
|
2263
|
-
}
|
|
2264
|
-
function listTasks(filter = {}, db) {
|
|
2265
|
-
const d = db || getDatabase();
|
|
2266
|
-
clearExpiredLocks(d);
|
|
2267
|
-
const conditions = [];
|
|
2268
|
-
const params = [];
|
|
2269
|
-
if (filter.project_id) {
|
|
2270
|
-
conditions.push("project_id = ?");
|
|
2271
|
-
params.push(filter.project_id);
|
|
2272
|
-
}
|
|
2273
|
-
if (filter.parent_id !== undefined) {
|
|
2274
|
-
if (filter.parent_id === null) {
|
|
2275
|
-
conditions.push("parent_id IS NULL");
|
|
2276
|
-
} else {
|
|
2277
|
-
conditions.push("parent_id = ?");
|
|
2278
|
-
params.push(filter.parent_id);
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
if (filter.status) {
|
|
2282
|
-
if (Array.isArray(filter.status)) {
|
|
2283
|
-
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
2284
|
-
params.push(...filter.status);
|
|
2285
|
-
} else {
|
|
2286
|
-
conditions.push("status = ?");
|
|
2287
|
-
params.push(filter.status);
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
if (filter.priority) {
|
|
2291
|
-
if (Array.isArray(filter.priority)) {
|
|
2292
|
-
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
2293
|
-
params.push(...filter.priority);
|
|
2294
|
-
} else {
|
|
2295
|
-
conditions.push("priority = ?");
|
|
2296
|
-
params.push(filter.priority);
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2299
|
-
if (filter.assigned_to) {
|
|
2300
|
-
conditions.push("assigned_to = ?");
|
|
2301
|
-
params.push(filter.assigned_to);
|
|
2302
|
-
}
|
|
2303
|
-
if (filter.agent_id) {
|
|
2304
|
-
conditions.push("agent_id = ?");
|
|
2305
|
-
params.push(filter.agent_id);
|
|
2306
|
-
}
|
|
2307
|
-
if (filter.session_id) {
|
|
2308
|
-
conditions.push("session_id = ?");
|
|
2309
|
-
params.push(filter.session_id);
|
|
2310
|
-
}
|
|
2311
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
2312
|
-
const placeholders = filter.tags.map(() => "?").join(",");
|
|
2313
|
-
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
2314
|
-
params.push(...filter.tags);
|
|
2315
|
-
}
|
|
2316
|
-
if (filter.plan_id) {
|
|
2317
|
-
conditions.push("plan_id = ?");
|
|
2318
|
-
params.push(filter.plan_id);
|
|
2319
|
-
}
|
|
2320
|
-
if (filter.task_list_id) {
|
|
2321
|
-
conditions.push("task_list_id = ?");
|
|
2322
|
-
params.push(filter.task_list_id);
|
|
2323
|
-
}
|
|
2324
|
-
if (filter.has_recurrence === true) {
|
|
2325
|
-
conditions.push("recurrence_rule IS NOT NULL");
|
|
2326
|
-
} else if (filter.has_recurrence === false) {
|
|
2327
|
-
conditions.push("recurrence_rule IS NULL");
|
|
2328
|
-
}
|
|
2329
|
-
if (filter.task_type) {
|
|
2330
|
-
if (Array.isArray(filter.task_type)) {
|
|
2331
|
-
conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
|
|
2332
|
-
params.push(...filter.task_type);
|
|
2333
|
-
} else {
|
|
2334
|
-
conditions.push("task_type = ?");
|
|
2335
|
-
params.push(filter.task_type);
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
2339
|
-
if (filter.cursor) {
|
|
2340
|
-
try {
|
|
2341
|
-
const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
|
|
2342
|
-
conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
|
|
2343
|
-
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
2344
|
-
} catch {}
|
|
2345
|
-
}
|
|
2346
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2347
|
-
let limitClause = "";
|
|
2348
|
-
if (filter.limit) {
|
|
2349
|
-
limitClause = " LIMIT ?";
|
|
2350
|
-
params.push(filter.limit);
|
|
2351
|
-
if (!filter.cursor && filter.offset) {
|
|
2352
|
-
limitClause += " OFFSET ?";
|
|
2353
|
-
params.push(filter.offset);
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
2357
|
-
return rows.map(rowToTask);
|
|
2358
|
-
}
|
|
2359
|
-
function countTasks(filter = {}, db) {
|
|
2360
|
-
const d = db || getDatabase();
|
|
2361
|
-
const conditions = [];
|
|
2362
|
-
const params = [];
|
|
2363
|
-
if (filter.project_id) {
|
|
2364
|
-
conditions.push("project_id = ?");
|
|
2365
|
-
params.push(filter.project_id);
|
|
2366
|
-
}
|
|
2367
|
-
if (filter.parent_id !== undefined) {
|
|
2368
|
-
if (filter.parent_id === null) {
|
|
2369
|
-
conditions.push("parent_id IS NULL");
|
|
2370
|
-
} else {
|
|
2371
|
-
conditions.push("parent_id = ?");
|
|
2372
|
-
params.push(filter.parent_id);
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
|
-
if (filter.status) {
|
|
2376
|
-
if (Array.isArray(filter.status)) {
|
|
2377
|
-
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
2378
|
-
params.push(...filter.status);
|
|
2379
|
-
} else {
|
|
2380
|
-
conditions.push("status = ?");
|
|
2381
|
-
params.push(filter.status);
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
|
-
if (filter.priority) {
|
|
2385
|
-
if (Array.isArray(filter.priority)) {
|
|
2386
|
-
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
2387
|
-
params.push(...filter.priority);
|
|
2388
|
-
} else {
|
|
2389
|
-
conditions.push("priority = ?");
|
|
2390
|
-
params.push(filter.priority);
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
if (filter.assigned_to) {
|
|
2394
|
-
conditions.push("assigned_to = ?");
|
|
2395
|
-
params.push(filter.assigned_to);
|
|
2396
|
-
}
|
|
2397
|
-
if (filter.agent_id) {
|
|
2398
|
-
conditions.push("agent_id = ?");
|
|
2399
|
-
params.push(filter.agent_id);
|
|
2400
|
-
}
|
|
2401
|
-
if (filter.session_id) {
|
|
2402
|
-
conditions.push("session_id = ?");
|
|
2403
|
-
params.push(filter.session_id);
|
|
2404
|
-
}
|
|
2405
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
2406
|
-
const placeholders = filter.tags.map(() => "?").join(",");
|
|
2407
|
-
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
2408
|
-
params.push(...filter.tags);
|
|
2409
|
-
}
|
|
2410
|
-
if (filter.plan_id) {
|
|
2411
|
-
conditions.push("plan_id = ?");
|
|
2412
|
-
params.push(filter.plan_id);
|
|
2413
|
-
}
|
|
2414
|
-
if (filter.task_list_id) {
|
|
2415
|
-
conditions.push("task_list_id = ?");
|
|
2416
|
-
params.push(filter.task_list_id);
|
|
2417
|
-
}
|
|
2418
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2419
|
-
const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
2420
|
-
return row.count;
|
|
2421
|
-
}
|
|
2422
|
-
function updateTask(id, input, db) {
|
|
2423
|
-
const d = db || getDatabase();
|
|
2424
|
-
const task = getTask(id, d);
|
|
2425
|
-
if (!task)
|
|
2426
|
-
throw new TaskNotFoundError(id);
|
|
2427
|
-
if (task.version !== input.version) {
|
|
2428
|
-
throw new VersionConflictError(id, input.version, task.version);
|
|
2429
|
-
}
|
|
2430
|
-
const sets = ["version = version + 1", "updated_at = ?"];
|
|
2431
|
-
const params = [now()];
|
|
2432
|
-
if (input.title !== undefined) {
|
|
2433
|
-
sets.push("title = ?");
|
|
2434
|
-
params.push(input.title);
|
|
2435
|
-
}
|
|
2436
|
-
if (input.description !== undefined) {
|
|
2437
|
-
sets.push("description = ?");
|
|
2438
|
-
params.push(input.description);
|
|
2439
|
-
}
|
|
2440
|
-
if (input.status !== undefined) {
|
|
2441
|
-
if (input.status === "completed") {
|
|
2442
|
-
checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
|
|
2443
|
-
}
|
|
2444
|
-
sets.push("status = ?");
|
|
2445
|
-
params.push(input.status);
|
|
2446
|
-
if (input.status === "completed") {
|
|
2447
|
-
sets.push("completed_at = ?");
|
|
2448
|
-
params.push(now());
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
if (input.priority !== undefined) {
|
|
2452
|
-
sets.push("priority = ?");
|
|
2453
|
-
params.push(input.priority);
|
|
2454
|
-
}
|
|
2455
|
-
if (input.assigned_to !== undefined) {
|
|
2456
|
-
sets.push("assigned_to = ?");
|
|
2457
|
-
params.push(input.assigned_to);
|
|
2458
|
-
}
|
|
2459
|
-
if (input.tags !== undefined) {
|
|
2460
|
-
sets.push("tags = ?");
|
|
2461
|
-
params.push(JSON.stringify(input.tags));
|
|
2462
|
-
}
|
|
2463
|
-
if (input.metadata !== undefined) {
|
|
2464
|
-
sets.push("metadata = ?");
|
|
2465
|
-
params.push(JSON.stringify(input.metadata));
|
|
2466
|
-
}
|
|
2467
|
-
if (input.plan_id !== undefined) {
|
|
2468
|
-
sets.push("plan_id = ?");
|
|
2469
|
-
params.push(input.plan_id);
|
|
2470
|
-
}
|
|
2471
|
-
if (input.task_list_id !== undefined) {
|
|
2472
|
-
sets.push("task_list_id = ?");
|
|
2473
|
-
params.push(input.task_list_id);
|
|
2474
|
-
}
|
|
2475
|
-
if (input.due_at !== undefined) {
|
|
2476
|
-
sets.push("due_at = ?");
|
|
2477
|
-
params.push(input.due_at);
|
|
2478
|
-
}
|
|
2479
|
-
if (input.estimated_minutes !== undefined) {
|
|
2480
|
-
sets.push("estimated_minutes = ?");
|
|
2481
|
-
params.push(input.estimated_minutes);
|
|
2482
|
-
}
|
|
2483
|
-
if (input.requires_approval !== undefined) {
|
|
2484
|
-
sets.push("requires_approval = ?");
|
|
2485
|
-
params.push(input.requires_approval ? 1 : 0);
|
|
2486
|
-
}
|
|
2487
|
-
if (input.approved_by !== undefined) {
|
|
2488
|
-
sets.push("approved_by = ?");
|
|
2489
|
-
params.push(input.approved_by);
|
|
2490
|
-
sets.push("approved_at = ?");
|
|
2491
|
-
params.push(now());
|
|
2492
|
-
}
|
|
2493
|
-
if (input.recurrence_rule !== undefined) {
|
|
2494
|
-
sets.push("recurrence_rule = ?");
|
|
2495
|
-
params.push(input.recurrence_rule);
|
|
2496
|
-
}
|
|
2497
|
-
if (input.task_type !== undefined) {
|
|
2498
|
-
sets.push("task_type = ?");
|
|
2499
|
-
params.push(input.task_type ?? null);
|
|
2500
|
-
}
|
|
2501
|
-
params.push(id, input.version);
|
|
2502
|
-
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
2503
|
-
if (result.changes === 0) {
|
|
2504
|
-
const current = getTask(id, d);
|
|
2505
|
-
throw new VersionConflictError(id, input.version, current?.version ?? -1);
|
|
2506
|
-
}
|
|
2507
|
-
if (input.tags !== undefined) {
|
|
2508
|
-
replaceTaskTags(id, input.tags, d);
|
|
2509
|
-
}
|
|
2510
|
-
const agentId = task.assigned_to || task.agent_id || null;
|
|
2511
|
-
if (input.status !== undefined && input.status !== task.status)
|
|
2512
|
-
logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
|
|
2513
|
-
if (input.priority !== undefined && input.priority !== task.priority)
|
|
2514
|
-
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
2515
|
-
if (input.title !== undefined && input.title !== task.title)
|
|
2516
|
-
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
2517
|
-
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
2518
|
-
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
2519
|
-
if (input.approved_by !== undefined)
|
|
2520
|
-
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
2521
|
-
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
2522
|
-
dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
|
|
2523
|
-
}
|
|
2524
|
-
if (input.status !== undefined && input.status !== task.status) {
|
|
2525
|
-
dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
|
|
2526
|
-
}
|
|
2527
|
-
return {
|
|
2528
|
-
...task,
|
|
2529
|
-
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
2530
|
-
tags: input.tags ?? task.tags,
|
|
2531
|
-
metadata: input.metadata ?? task.metadata,
|
|
2532
|
-
version: task.version + 1,
|
|
2533
|
-
updated_at: now(),
|
|
2534
|
-
completed_at: input.status === "completed" ? now() : task.completed_at,
|
|
2535
|
-
requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
|
|
2536
|
-
approved_by: input.approved_by ?? task.approved_by,
|
|
2537
|
-
approved_at: input.approved_by ? now() : task.approved_at
|
|
2538
|
-
};
|
|
2539
|
-
}
|
|
2540
|
-
function deleteTask(id, db) {
|
|
2541
|
-
const d = db || getDatabase();
|
|
2542
|
-
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
2543
|
-
return result.changes > 0;
|
|
2544
|
-
}
|
|
2545
|
-
function getBlockingDeps(id, db) {
|
|
2546
|
-
const d = db || getDatabase();
|
|
2547
|
-
const deps = getTaskDependencies(id, d);
|
|
2548
|
-
if (deps.length === 0)
|
|
2549
|
-
return [];
|
|
2550
|
-
const blocking = [];
|
|
2551
|
-
for (const dep of deps) {
|
|
2552
|
-
const task = getTask(dep.depends_on, d);
|
|
2553
|
-
if (task && task.status !== "completed")
|
|
2554
|
-
blocking.push(task);
|
|
2555
|
-
}
|
|
2556
|
-
return blocking;
|
|
2557
|
-
}
|
|
2558
|
-
function startTask(id, agentId, db) {
|
|
2559
|
-
const d = db || getDatabase();
|
|
2560
|
-
const task = getTask(id, d);
|
|
2561
|
-
if (!task)
|
|
2562
|
-
throw new TaskNotFoundError(id);
|
|
2563
|
-
const blocking = getBlockingDeps(id, d);
|
|
2564
|
-
if (blocking.length > 0) {
|
|
2565
|
-
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
2566
|
-
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
2567
|
-
}
|
|
2568
|
-
const cutoff = lockExpiryCutoff();
|
|
2569
|
-
const timestamp = now();
|
|
2570
|
-
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
|
|
2571
|
-
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
|
|
2572
|
-
if (result.changes === 0) {
|
|
2573
|
-
if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
2574
|
-
throw new LockError(id, task.locked_by);
|
|
2575
|
-
}
|
|
2576
|
-
}
|
|
2577
|
-
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
2578
|
-
dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
|
|
2579
|
-
return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
|
|
2580
|
-
}
|
|
2581
|
-
function completeTask(id, agentId, db, options) {
|
|
2582
|
-
const d = db || getDatabase();
|
|
2583
|
-
const task = getTask(id, d);
|
|
2584
|
-
if (!task)
|
|
2585
|
-
throw new TaskNotFoundError(id);
|
|
2586
|
-
if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
2587
|
-
throw new LockError(id, task.locked_by);
|
|
2588
|
-
}
|
|
2589
|
-
checkCompletionGuard(task, agentId || null, d);
|
|
2590
|
-
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;
|
|
2591
|
-
const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
|
|
2592
|
-
const completionMeta = {};
|
|
2593
|
-
if (hasEvidence)
|
|
2594
|
-
completionMeta._evidence = evidence;
|
|
2595
|
-
if (options?.confidence !== undefined) {
|
|
2596
|
-
completionMeta._completion = { confidence: options.confidence };
|
|
2597
|
-
}
|
|
2598
|
-
const hasMeta = Object.keys(completionMeta).length > 0;
|
|
2599
|
-
if (hasMeta) {
|
|
2600
|
-
const meta2 = { ...task.metadata, ...completionMeta };
|
|
2601
|
-
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
|
|
2602
|
-
}
|
|
2603
|
-
const timestamp = now();
|
|
2604
|
-
const confidence = options?.confidence !== undefined ? options.confidence : null;
|
|
2605
|
-
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
2606
|
-
WHERE id = ?`, [timestamp, confidence, timestamp, id]);
|
|
2607
|
-
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
2608
|
-
dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
|
|
2609
|
-
let spawnedTask = null;
|
|
2610
|
-
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
2611
|
-
spawnedTask = spawnNextRecurrence(task, d);
|
|
2612
|
-
}
|
|
2613
|
-
let spawnedFromTemplate = null;
|
|
2614
|
-
if (task.spawns_template_id) {
|
|
2615
|
-
try {
|
|
2616
|
-
const input = taskFromTemplate(task.spawns_template_id, {
|
|
2617
|
-
project_id: task.project_id ?? undefined,
|
|
2618
|
-
plan_id: task.plan_id ?? undefined,
|
|
2619
|
-
task_list_id: task.task_list_id ?? undefined,
|
|
2620
|
-
assigned_to: task.assigned_to ?? undefined
|
|
2621
|
-
}, d);
|
|
2622
|
-
spawnedFromTemplate = createTask(input, d);
|
|
2623
|
-
} catch {}
|
|
2624
|
-
}
|
|
2625
|
-
const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
|
|
2626
|
-
if (spawnedTask) {
|
|
2627
|
-
meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
|
|
2628
|
-
}
|
|
2629
|
-
if (spawnedFromTemplate) {
|
|
2630
|
-
meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
|
|
2631
|
-
}
|
|
2632
|
-
const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
|
|
2633
|
-
JOIN task_dependencies td ON td.task_id = t.id
|
|
2634
|
-
WHERE td.depends_on = ? AND t.status = 'pending'
|
|
2635
|
-
AND NOT EXISTS (
|
|
2636
|
-
SELECT 1 FROM task_dependencies td2
|
|
2637
|
-
JOIN tasks dep2 ON dep2.id = td2.depends_on
|
|
2638
|
-
WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
|
|
2639
|
-
)`).all(id, id);
|
|
2640
|
-
if (unblockedDeps.length > 0) {
|
|
2641
|
-
meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
|
|
2642
|
-
for (const dep of unblockedDeps) {
|
|
2643
|
-
dispatchWebhook("task.unblocked", { id: dep.id, unblocked_by: id, title: dep.title }, d).catch(() => {});
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
|
|
2647
|
-
}
|
|
2648
|
-
function lockTask(id, agentId, db) {
|
|
2649
|
-
const d = db || getDatabase();
|
|
2650
|
-
const task = getTask(id, d);
|
|
2651
|
-
if (!task)
|
|
2652
|
-
throw new TaskNotFoundError(id);
|
|
2653
|
-
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
2654
|
-
return { success: true, locked_by: agentId, locked_at: task.locked_at };
|
|
2655
|
-
}
|
|
2656
|
-
const cutoff = lockExpiryCutoff();
|
|
2657
|
-
const timestamp = now();
|
|
2658
|
-
const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
2659
|
-
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
2660
|
-
if (result.changes === 0) {
|
|
2661
|
-
const current = getTask(id, d);
|
|
2662
|
-
if (!current)
|
|
2663
|
-
throw new TaskNotFoundError(id);
|
|
2664
|
-
if (current.locked_by && !isLockExpired(current.locked_at)) {
|
|
2665
|
-
return {
|
|
2666
|
-
success: false,
|
|
2667
|
-
locked_by: current.locked_by,
|
|
2668
|
-
locked_at: current.locked_at,
|
|
2669
|
-
error: `Task is locked by ${current.locked_by}`
|
|
2670
|
-
};
|
|
2671
|
-
}
|
|
2672
|
-
}
|
|
2673
|
-
return { success: true, locked_by: agentId, locked_at: timestamp };
|
|
2674
|
-
}
|
|
2675
|
-
function unlockTask(id, agentId, db) {
|
|
2676
|
-
const d = db || getDatabase();
|
|
2677
|
-
const task = getTask(id, d);
|
|
2678
|
-
if (!task)
|
|
2679
|
-
throw new TaskNotFoundError(id);
|
|
2680
|
-
if (agentId && task.locked_by && task.locked_by !== agentId) {
|
|
2681
|
-
throw new LockError(id, task.locked_by);
|
|
2682
|
-
}
|
|
2683
|
-
const timestamp = now();
|
|
2684
|
-
d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
|
|
2685
|
-
WHERE id = ?`, [timestamp, id]);
|
|
2686
|
-
return true;
|
|
2687
|
-
}
|
|
2688
|
-
function addDependency(taskId, dependsOn, db) {
|
|
2689
|
-
const d = db || getDatabase();
|
|
2690
|
-
if (!getTask(taskId, d))
|
|
2691
|
-
throw new TaskNotFoundError(taskId);
|
|
2692
|
-
if (!getTask(dependsOn, d))
|
|
2693
|
-
throw new TaskNotFoundError(dependsOn);
|
|
2694
|
-
if (wouldCreateCycle(taskId, dependsOn, d)) {
|
|
2695
|
-
throw new DependencyCycleError(taskId, dependsOn);
|
|
2696
|
-
}
|
|
2697
|
-
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
|
|
2698
|
-
}
|
|
2699
|
-
function removeDependency(taskId, dependsOn, db) {
|
|
2700
|
-
const d = db || getDatabase();
|
|
2701
|
-
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
2702
|
-
return result.changes > 0;
|
|
2703
|
-
}
|
|
2704
|
-
function getTaskDependencies(taskId, db) {
|
|
2705
|
-
const d = db || getDatabase();
|
|
2706
|
-
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
2707
|
-
}
|
|
2708
|
-
function getTaskDependents(taskId, db) {
|
|
2709
|
-
const d = db || getDatabase();
|
|
2710
|
-
return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
|
|
2711
|
-
}
|
|
2712
|
-
function cloneTask(taskId, overrides, db) {
|
|
2713
|
-
const d = db || getDatabase();
|
|
2714
|
-
const source = getTask(taskId, d);
|
|
2715
|
-
if (!source)
|
|
2716
|
-
throw new TaskNotFoundError(taskId);
|
|
2717
|
-
const input = {
|
|
2718
|
-
title: overrides?.title ?? source.title,
|
|
2719
|
-
description: overrides?.description ?? source.description ?? undefined,
|
|
2720
|
-
priority: overrides?.priority ?? source.priority,
|
|
2721
|
-
project_id: overrides?.project_id ?? source.project_id ?? undefined,
|
|
2722
|
-
parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
|
|
2723
|
-
plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
|
|
2724
|
-
task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
|
|
2725
|
-
status: overrides?.status ?? "pending",
|
|
2726
|
-
agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
|
|
2727
|
-
assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
|
|
2728
|
-
tags: overrides?.tags ?? source.tags,
|
|
2729
|
-
metadata: overrides?.metadata ?? source.metadata,
|
|
2730
|
-
estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
|
|
2731
|
-
recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
|
|
2732
|
-
};
|
|
2733
|
-
return createTask(input, d);
|
|
2734
|
-
}
|
|
2735
|
-
function getTaskGraph(taskId, direction = "both", db) {
|
|
2736
|
-
const d = db || getDatabase();
|
|
2737
|
-
const task = getTask(taskId, d);
|
|
2738
|
-
if (!task)
|
|
2739
|
-
throw new TaskNotFoundError(taskId);
|
|
2740
|
-
function toNode(t) {
|
|
2741
|
-
const deps = getTaskDependencies(t.id, d);
|
|
2742
|
-
const hasUnfinishedDeps = deps.some((dep) => {
|
|
2743
|
-
const depTask = getTask(dep.depends_on, d);
|
|
2744
|
-
return depTask && depTask.status !== "completed";
|
|
2745
|
-
});
|
|
2746
|
-
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
2747
|
-
}
|
|
2748
|
-
function buildUp(id, visited) {
|
|
2749
|
-
if (visited.has(id))
|
|
2750
|
-
return [];
|
|
2751
|
-
visited.add(id);
|
|
2752
|
-
const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
|
|
2753
|
-
return deps.map((dep) => {
|
|
2754
|
-
const depTask = getTask(dep.depends_on, d);
|
|
2755
|
-
if (!depTask)
|
|
2756
|
-
return null;
|
|
2757
|
-
return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
|
|
2758
|
-
}).filter(Boolean);
|
|
2759
|
-
}
|
|
2760
|
-
function buildDown(id, visited) {
|
|
2761
|
-
if (visited.has(id))
|
|
2762
|
-
return [];
|
|
2763
|
-
visited.add(id);
|
|
2764
|
-
const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
|
|
2765
|
-
return dependents.map((dep) => {
|
|
2766
|
-
const depTask = getTask(dep.task_id, d);
|
|
2767
|
-
if (!depTask)
|
|
2768
|
-
return null;
|
|
2769
|
-
return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
|
|
2770
|
-
}).filter(Boolean);
|
|
2771
|
-
}
|
|
2772
|
-
const rootNode = toNode(task);
|
|
2773
|
-
const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
|
|
2774
|
-
const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
|
|
2775
|
-
return { task: rootNode, depends_on, blocks };
|
|
2776
|
-
}
|
|
2777
|
-
function moveTask(taskId, target, db) {
|
|
2778
|
-
const d = db || getDatabase();
|
|
2779
|
-
const task = getTask(taskId, d);
|
|
2780
|
-
if (!task)
|
|
2781
|
-
throw new TaskNotFoundError(taskId);
|
|
2782
|
-
const sets = ["updated_at = ?", "version = version + 1"];
|
|
2783
|
-
const params = [now()];
|
|
2784
|
-
if (target.task_list_id !== undefined) {
|
|
2785
|
-
sets.push("task_list_id = ?");
|
|
2786
|
-
params.push(target.task_list_id);
|
|
2787
|
-
}
|
|
2788
|
-
if (target.project_id !== undefined) {
|
|
2789
|
-
sets.push("project_id = ?");
|
|
2790
|
-
params.push(target.project_id);
|
|
2791
|
-
}
|
|
2792
|
-
if (target.plan_id !== undefined) {
|
|
2793
|
-
sets.push("plan_id = ?");
|
|
2794
|
-
params.push(target.plan_id);
|
|
2795
|
-
}
|
|
2796
|
-
params.push(taskId);
|
|
2797
|
-
d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
2798
|
-
return getTask(taskId, d);
|
|
2799
|
-
}
|
|
2800
|
-
function spawnNextRecurrence(completedTask, db) {
|
|
2801
|
-
const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
|
|
2802
|
-
let title = completedTask.title;
|
|
2803
|
-
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
2804
|
-
title = title.slice(completedTask.short_id.length + 2);
|
|
2805
|
-
}
|
|
2806
|
-
const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
|
|
2807
|
-
return createTask({
|
|
2808
|
-
title,
|
|
2809
|
-
description: completedTask.description ?? undefined,
|
|
2810
|
-
priority: completedTask.priority,
|
|
2811
|
-
project_id: completedTask.project_id ?? undefined,
|
|
2812
|
-
task_list_id: completedTask.task_list_id ?? undefined,
|
|
2813
|
-
plan_id: completedTask.plan_id ?? undefined,
|
|
2814
|
-
assigned_to: completedTask.assigned_to ?? undefined,
|
|
2815
|
-
tags: completedTask.tags,
|
|
2816
|
-
metadata: completedTask.metadata,
|
|
2817
|
-
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
2818
|
-
recurrence_rule: completedTask.recurrence_rule,
|
|
2819
|
-
recurrence_parent_id: recurrenceParentId,
|
|
2820
|
-
due_at: dueAt
|
|
2821
|
-
}, db);
|
|
2822
|
-
}
|
|
2823
|
-
function claimNextTask(agentId, filters, db) {
|
|
2824
|
-
const d = db || getDatabase();
|
|
2825
|
-
const tx = d.transaction(() => {
|
|
2826
|
-
const task = getNextTask(agentId, filters, d);
|
|
2827
|
-
if (!task)
|
|
2828
|
-
return null;
|
|
2829
|
-
return startTask(task.id, agentId, d);
|
|
2830
|
-
});
|
|
2831
|
-
return tx();
|
|
2832
|
-
}
|
|
2833
|
-
function getNextTask(agentId, filters, db) {
|
|
2834
|
-
const d = db || getDatabase();
|
|
2835
|
-
clearExpiredLocks(d);
|
|
2836
|
-
const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
|
|
2837
|
-
const params = [lockExpiryCutoff()];
|
|
2838
|
-
if (filters?.project_id) {
|
|
2839
|
-
conditions.push("project_id = ?");
|
|
2840
|
-
params.push(filters.project_id);
|
|
2841
|
-
}
|
|
2842
|
-
if (filters?.task_list_id) {
|
|
2843
|
-
conditions.push("task_list_id = ?");
|
|
2844
|
-
params.push(filters.task_list_id);
|
|
2845
|
-
}
|
|
2846
|
-
if (filters?.plan_id) {
|
|
2847
|
-
conditions.push("plan_id = ?");
|
|
2848
|
-
params.push(filters.plan_id);
|
|
2849
|
-
}
|
|
2850
|
-
if (filters?.tags && filters.tags.length > 0) {
|
|
2851
|
-
const placeholders = filters.tags.map(() => "?").join(",");
|
|
2852
|
-
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
2853
|
-
params.push(...filters.tags);
|
|
2854
|
-
}
|
|
2855
|
-
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')");
|
|
2856
|
-
const where = conditions.join(" AND ");
|
|
2857
|
-
let recentProjectIds = [];
|
|
2858
|
-
if (agentId) {
|
|
2859
|
-
const recentRows = d.query(`SELECT DISTINCT project_id FROM tasks WHERE assigned_to = ? AND status = 'completed' AND project_id IS NOT NULL ORDER BY completed_at DESC LIMIT 3`).all(agentId);
|
|
2860
|
-
recentProjectIds = recentRows.map((r) => r.project_id);
|
|
2861
|
-
}
|
|
2862
|
-
let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
|
|
2863
|
-
if (agentId) {
|
|
2864
|
-
sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
|
|
2865
|
-
params.push(agentId);
|
|
2866
|
-
}
|
|
2867
|
-
if (recentProjectIds.length > 0) {
|
|
2868
|
-
const placeholders = recentProjectIds.map(() => "?").join(",");
|
|
2869
|
-
sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
|
|
2870
|
-
params.push(...recentProjectIds);
|
|
2871
|
-
}
|
|
2872
|
-
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`;
|
|
2873
|
-
const row = d.query(sql).get(...params);
|
|
2874
|
-
return row ? rowToTask(row) : null;
|
|
2875
|
-
}
|
|
2876
|
-
function getActiveWork(filters, db) {
|
|
2877
|
-
const d = db || getDatabase();
|
|
2878
|
-
clearExpiredLocks(d);
|
|
2879
|
-
const conditions = ["status = 'in_progress'"];
|
|
2880
|
-
const params = [];
|
|
2881
|
-
if (filters?.project_id) {
|
|
2882
|
-
conditions.push("project_id = ?");
|
|
2883
|
-
params.push(filters.project_id);
|
|
2884
|
-
}
|
|
2885
|
-
if (filters?.task_list_id) {
|
|
2886
|
-
conditions.push("task_list_id = ?");
|
|
2887
|
-
params.push(filters.task_list_id);
|
|
2888
|
-
}
|
|
2889
|
-
const where = conditions.join(" AND ");
|
|
2890
|
-
const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
|
|
2891
|
-
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
2892
|
-
updated_at DESC`).all(...params);
|
|
2893
|
-
return rows;
|
|
2894
|
-
}
|
|
2895
|
-
function getTasksChangedSince(since, filters, db) {
|
|
2896
|
-
const d = db || getDatabase();
|
|
2897
|
-
const conditions = ["updated_at > ?"];
|
|
2898
|
-
const params = [since];
|
|
2899
|
-
if (filters?.project_id) {
|
|
2900
|
-
conditions.push("project_id = ?");
|
|
2901
|
-
params.push(filters.project_id);
|
|
2902
|
-
}
|
|
2903
|
-
if (filters?.task_list_id) {
|
|
2904
|
-
conditions.push("task_list_id = ?");
|
|
2905
|
-
params.push(filters.task_list_id);
|
|
2906
|
-
}
|
|
2907
|
-
const where = conditions.join(" AND ");
|
|
2908
|
-
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
|
|
2909
|
-
return rows.map(rowToTask);
|
|
2910
|
-
}
|
|
2911
|
-
function failTask(id, agentId, reason, options, db) {
|
|
2912
|
-
const d = db || getDatabase();
|
|
2913
|
-
const task = getTask(id, d);
|
|
2914
|
-
if (!task)
|
|
2915
|
-
throw new TaskNotFoundError(id);
|
|
2916
|
-
const meta = {
|
|
2917
|
-
...task.metadata,
|
|
2918
|
-
_failure: {
|
|
2919
|
-
reason: reason || "Unknown failure",
|
|
2920
|
-
error_code: options?.error_code || null,
|
|
2921
|
-
failed_by: agentId || null,
|
|
2922
|
-
failed_at: now(),
|
|
2923
|
-
retry_requested: options?.retry || false
|
|
2924
|
-
}
|
|
2925
|
-
};
|
|
2926
|
-
const timestamp = now();
|
|
2927
|
-
d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
2928
|
-
WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
|
|
2929
|
-
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
2930
|
-
dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
|
|
2931
|
-
const failedTask = {
|
|
2932
|
-
...task,
|
|
2933
|
-
status: "failed",
|
|
2934
|
-
locked_by: null,
|
|
2935
|
-
locked_at: null,
|
|
2936
|
-
metadata: meta,
|
|
2937
|
-
version: task.version + 1,
|
|
2938
|
-
updated_at: timestamp
|
|
2939
|
-
};
|
|
2940
|
-
let retryTask;
|
|
2941
|
-
if (options?.retry) {
|
|
2942
|
-
const retryCount = (task.retry_count || 0) + 1;
|
|
2943
|
-
const maxRetries = task.max_retries || 3;
|
|
2944
|
-
if (retryCount > maxRetries) {
|
|
2945
|
-
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
|
|
2946
|
-
JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
|
|
2947
|
-
id
|
|
2948
|
-
]);
|
|
2949
|
-
} else {
|
|
2950
|
-
const backoffMinutes = Math.pow(5, retryCount - 1);
|
|
2951
|
-
const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
|
|
2952
|
-
let title = task.title;
|
|
2953
|
-
if (task.short_id && title.startsWith(task.short_id + ": ")) {
|
|
2954
|
-
title = title.slice(task.short_id.length + 2);
|
|
2955
|
-
}
|
|
2956
|
-
retryTask = createTask({
|
|
2957
|
-
title,
|
|
2958
|
-
description: task.description ?? undefined,
|
|
2959
|
-
priority: task.priority,
|
|
2960
|
-
project_id: task.project_id ?? undefined,
|
|
2961
|
-
task_list_id: task.task_list_id ?? undefined,
|
|
2962
|
-
plan_id: task.plan_id ?? undefined,
|
|
2963
|
-
assigned_to: task.assigned_to ?? undefined,
|
|
2964
|
-
tags: task.tags,
|
|
2965
|
-
metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
|
|
2966
|
-
estimated_minutes: task.estimated_minutes ?? undefined,
|
|
2967
|
-
recurrence_rule: task.recurrence_rule ?? undefined,
|
|
2968
|
-
due_at: retryAfter
|
|
2969
|
-
}, d);
|
|
2970
|
-
d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
|
|
2971
|
-
}
|
|
2972
|
-
}
|
|
2973
|
-
return { task: failedTask, retryTask };
|
|
2974
|
-
}
|
|
2975
|
-
function getStaleTasks(staleMinutes = 30, filters, db) {
|
|
2976
|
-
const d = db || getDatabase();
|
|
2977
|
-
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
2978
|
-
const conditions = [
|
|
2979
|
-
"status = 'in_progress'",
|
|
2980
|
-
"(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
|
|
2981
|
-
];
|
|
2982
|
-
const params = [cutoff, cutoff];
|
|
2983
|
-
if (filters?.project_id) {
|
|
2984
|
-
conditions.push("project_id = ?");
|
|
2985
|
-
params.push(filters.project_id);
|
|
2986
|
-
}
|
|
2987
|
-
if (filters?.task_list_id) {
|
|
2988
|
-
conditions.push("task_list_id = ?");
|
|
2989
|
-
params.push(filters.task_list_id);
|
|
2990
|
-
}
|
|
2991
|
-
const where = conditions.join(" AND ");
|
|
2992
|
-
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
|
|
2993
|
-
return rows.map(rowToTask);
|
|
2994
|
-
}
|
|
2995
|
-
function logCost(taskId, tokens, usd, db) {
|
|
2996
|
-
const d = db || getDatabase();
|
|
2997
|
-
d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
|
|
2998
|
-
}
|
|
2999
|
-
function stealTask(agentId, opts, db) {
|
|
3000
|
-
const d = db || getDatabase();
|
|
3001
|
-
const staleMinutes = opts?.stale_minutes ?? 30;
|
|
3002
|
-
const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
|
|
3003
|
-
if (staleTasks.length === 0)
|
|
3004
|
-
return null;
|
|
3005
|
-
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
3006
|
-
staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
|
|
3007
|
-
const target = staleTasks[0];
|
|
3008
|
-
const timestamp = now();
|
|
3009
|
-
d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ?`, [agentId, agentId, timestamp, timestamp, target.id]);
|
|
3010
|
-
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
3011
|
-
dispatchWebhook("task.assigned", { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to }, d).catch(() => {});
|
|
3012
|
-
return { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
3013
|
-
}
|
|
3014
|
-
function claimOrSteal(agentId, filters, db) {
|
|
3015
|
-
const d = db || getDatabase();
|
|
3016
|
-
const tx = d.transaction(() => {
|
|
3017
|
-
const next = getNextTask(agentId, filters, d);
|
|
3018
|
-
if (next) {
|
|
3019
|
-
const started = startTask(next.id, agentId, d);
|
|
3020
|
-
return { task: started, stolen: false };
|
|
3021
|
-
}
|
|
3022
|
-
const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
|
|
3023
|
-
if (stolen)
|
|
3024
|
-
return { task: stolen, stolen: true };
|
|
3025
|
-
return null;
|
|
3026
|
-
});
|
|
3027
|
-
return tx();
|
|
3028
|
-
}
|
|
3029
|
-
function getStatus(filters, agentId, options, db) {
|
|
3030
|
-
const d = db || getDatabase();
|
|
3031
|
-
const pending = countTasks({ ...filters, status: "pending" }, d);
|
|
3032
|
-
const in_progress = countTasks({ ...filters, status: "in_progress" }, d);
|
|
3033
|
-
const completed = countTasks({ ...filters, status: "completed" }, d);
|
|
3034
|
-
const total = countTasks(filters || {}, d);
|
|
3035
|
-
const active_work = getActiveWork(filters, d);
|
|
3036
|
-
const next_task = getNextTask(agentId, filters, d);
|
|
3037
|
-
const stale = getStaleTasks(30, filters, d);
|
|
3038
|
-
const conditions = ["recurrence_rule IS NOT NULL", "status = 'pending'", "due_at < ?"];
|
|
3039
|
-
const params = [now()];
|
|
3040
|
-
if (filters?.project_id) {
|
|
3041
|
-
conditions.push("project_id = ?");
|
|
3042
|
-
params.push(filters.project_id);
|
|
3043
|
-
}
|
|
3044
|
-
if (filters?.task_list_id) {
|
|
3045
|
-
conditions.push("task_list_id = ?");
|
|
3046
|
-
params.push(filters.task_list_id);
|
|
3047
|
-
}
|
|
3048
|
-
const overdueRow = d.query(`SELECT COUNT(*) as count FROM tasks WHERE ${conditions.join(" AND ")}`).get(...params);
|
|
3049
|
-
const summary = {
|
|
3050
|
-
pending,
|
|
3051
|
-
in_progress,
|
|
3052
|
-
completed,
|
|
3053
|
-
total,
|
|
3054
|
-
active_work,
|
|
3055
|
-
next_task,
|
|
3056
|
-
stale_count: stale.length,
|
|
3057
|
-
overdue_recurring: overdueRow.count
|
|
3058
|
-
};
|
|
3059
|
-
if (options?.explain_blocked) {
|
|
3060
|
-
const pendingTasks = listTasks({ ...filters, status: "pending" }, d);
|
|
3061
|
-
const blockedTasks = [];
|
|
3062
|
-
for (const t of pendingTasks) {
|
|
3063
|
-
const blockingDeps = getBlockingDeps(t.id, d);
|
|
3064
|
-
if (blockingDeps.length > 0) {
|
|
3065
|
-
blockedTasks.push({
|
|
3066
|
-
id: t.id,
|
|
3067
|
-
short_id: t.short_id,
|
|
3068
|
-
title: t.title,
|
|
3069
|
-
blocked_by: blockingDeps.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
|
|
3070
|
-
});
|
|
3071
|
-
}
|
|
3072
|
-
}
|
|
3073
|
-
summary.blocked_tasks = blockedTasks;
|
|
3074
|
-
}
|
|
3075
|
-
return summary;
|
|
3076
|
-
}
|
|
3077
|
-
function decomposeTasks(parentId, subtasks, options, db) {
|
|
3078
|
-
const d = db || getDatabase();
|
|
3079
|
-
const parent = getTask(parentId, d);
|
|
3080
|
-
if (!parent)
|
|
3081
|
-
throw new TaskNotFoundError(parentId);
|
|
3082
|
-
const created = [];
|
|
3083
|
-
const tx = d.transaction(() => {
|
|
3084
|
-
for (const input of subtasks) {
|
|
3085
|
-
const task = createTask({
|
|
3086
|
-
title: input.title,
|
|
3087
|
-
description: input.description,
|
|
3088
|
-
priority: input.priority || parent.priority,
|
|
3089
|
-
parent_id: parentId,
|
|
3090
|
-
project_id: parent.project_id || undefined,
|
|
3091
|
-
plan_id: parent.plan_id || undefined,
|
|
3092
|
-
task_list_id: parent.task_list_id || undefined,
|
|
3093
|
-
assigned_to: input.assigned_to || parent.assigned_to || undefined,
|
|
3094
|
-
estimated_minutes: input.estimated_minutes,
|
|
3095
|
-
tags: input.tags
|
|
3096
|
-
}, d);
|
|
3097
|
-
if (options?.depends_on_prev && created.length > 0) {
|
|
3098
|
-
const prev = created[created.length - 1];
|
|
3099
|
-
addDependency(task.id, prev.id, d);
|
|
3100
|
-
}
|
|
3101
|
-
created.push(task);
|
|
3102
|
-
}
|
|
3103
|
-
});
|
|
3104
|
-
tx();
|
|
3105
|
-
return { parent, subtasks: created };
|
|
3106
|
-
}
|
|
3107
|
-
function setTaskStatus(id, status, _agentId, db) {
|
|
3108
|
-
const d = db || getDatabase();
|
|
3109
|
-
for (let attempt = 0;attempt < 3; attempt++) {
|
|
3110
|
-
const task = getTask(id, d);
|
|
3111
|
-
if (!task)
|
|
3112
|
-
throw new TaskNotFoundError(id);
|
|
3113
|
-
if (task.status === status)
|
|
3114
|
-
return task;
|
|
3115
|
-
try {
|
|
3116
|
-
return updateTask(id, { status, version: task.version }, d);
|
|
3117
|
-
} catch (e) {
|
|
3118
|
-
if (e instanceof VersionConflictError && attempt < 2)
|
|
3119
|
-
continue;
|
|
3120
|
-
throw e;
|
|
3121
|
-
}
|
|
3122
|
-
}
|
|
3123
|
-
throw new Error(`Failed to set status after 3 attempts`);
|
|
3124
|
-
}
|
|
3125
|
-
function setTaskPriority(id, priority, _agentId, db) {
|
|
3126
|
-
const d = db || getDatabase();
|
|
3127
|
-
for (let attempt = 0;attempt < 3; attempt++) {
|
|
3128
|
-
const task = getTask(id, d);
|
|
3129
|
-
if (!task)
|
|
3130
|
-
throw new TaskNotFoundError(id);
|
|
3131
|
-
if (task.priority === priority)
|
|
3132
|
-
return task;
|
|
3133
|
-
try {
|
|
3134
|
-
return updateTask(id, { priority, version: task.version }, d);
|
|
3135
|
-
} catch (e) {
|
|
3136
|
-
if (e instanceof VersionConflictError && attempt < 2)
|
|
3137
|
-
continue;
|
|
3138
|
-
throw e;
|
|
3139
|
-
}
|
|
3140
|
-
}
|
|
3141
|
-
throw new Error(`Failed to set priority after 3 attempts`);
|
|
3142
|
-
}
|
|
3143
|
-
function redistributeStaleTasks(agentId, options, db) {
|
|
3144
|
-
const d = db || getDatabase();
|
|
3145
|
-
const maxAge = options?.max_age_minutes ?? 60;
|
|
3146
|
-
const stale = getStaleTasks(maxAge, options?.project_id ? { project_id: options.project_id } : undefined, d);
|
|
3147
|
-
const limited = options?.limit ? stale.slice(0, options.limit) : stale;
|
|
3148
|
-
const timestamp = now();
|
|
3149
|
-
const released = [];
|
|
3150
|
-
for (const t of limited) {
|
|
3151
|
-
d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, status = 'pending', version = version + 1, updated_at = ? WHERE id = ?`, [timestamp, t.id]);
|
|
3152
|
-
released.push({ ...t, locked_by: null, locked_at: null, status: "pending" });
|
|
3153
|
-
}
|
|
3154
|
-
const claimed = released.length > 0 ? claimNextTask(agentId, options?.project_id ? { project_id: options.project_id } : undefined, d) : null;
|
|
3155
|
-
return { released, claimed };
|
|
3156
|
-
}
|
|
3157
|
-
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
3158
|
-
const visited = new Set;
|
|
3159
|
-
const queue = [dependsOn];
|
|
3160
|
-
while (queue.length > 0) {
|
|
3161
|
-
const current = queue.shift();
|
|
3162
|
-
if (current === taskId)
|
|
3163
|
-
return true;
|
|
3164
|
-
if (visited.has(current))
|
|
3165
|
-
continue;
|
|
3166
|
-
visited.add(current);
|
|
3167
|
-
const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
|
|
3168
|
-
for (const dep of deps) {
|
|
3169
|
-
queue.push(dep.depends_on);
|
|
3170
|
-
}
|
|
3171
|
-
}
|
|
3172
|
-
return false;
|
|
3173
|
-
}
|
|
3174
|
-
function getTaskStats(filters, db) {
|
|
3175
|
-
const d = db || getDatabase();
|
|
3176
|
-
const conditions = [];
|
|
3177
|
-
const params = [];
|
|
3178
|
-
if (filters?.project_id) {
|
|
3179
|
-
conditions.push("project_id = ?");
|
|
3180
|
-
params.push(filters.project_id);
|
|
3181
|
-
}
|
|
3182
|
-
if (filters?.task_list_id) {
|
|
3183
|
-
conditions.push("task_list_id = ?");
|
|
3184
|
-
params.push(filters.task_list_id);
|
|
3185
|
-
}
|
|
3186
|
-
if (filters?.agent_id) {
|
|
3187
|
-
conditions.push("(agent_id = ? OR assigned_to = ?)");
|
|
3188
|
-
params.push(filters.agent_id, filters.agent_id);
|
|
3189
|
-
}
|
|
3190
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3191
|
-
const totalRow = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
3192
|
-
const statusRows = d.query(`SELECT status, COUNT(*) as count FROM tasks ${where} GROUP BY status`).all(...params);
|
|
3193
|
-
const by_status = {};
|
|
3194
|
-
for (const r of statusRows)
|
|
3195
|
-
by_status[r.status] = r.count;
|
|
3196
|
-
const priorityRows = d.query(`SELECT priority, COUNT(*) as count FROM tasks ${where} GROUP BY priority`).all(...params);
|
|
3197
|
-
const by_priority = {};
|
|
3198
|
-
for (const r of priorityRows)
|
|
3199
|
-
by_priority[r.priority] = r.count;
|
|
3200
|
-
const agentRows = d.query(`SELECT COALESCE(assigned_to, agent_id, 'unassigned') as agent, COUNT(*) as count FROM tasks ${where} GROUP BY agent`).all(...params);
|
|
3201
|
-
const by_agent = {};
|
|
3202
|
-
for (const r of agentRows)
|
|
3203
|
-
by_agent[r.agent] = r.count;
|
|
3204
|
-
const completed = by_status["completed"] || 0;
|
|
3205
|
-
const completion_rate = totalRow.count > 0 ? Math.round(completed / totalRow.count * 100) : 0;
|
|
3206
|
-
return { total: totalRow.count, by_status, by_priority, completion_rate, by_agent };
|
|
3207
|
-
}
|
|
3208
|
-
function bulkCreateTasks(inputs, db) {
|
|
3209
|
-
const d = db || getDatabase();
|
|
3210
|
-
const tempIdToRealId = new Map;
|
|
3211
|
-
const created = [];
|
|
3212
|
-
const tx = d.transaction(() => {
|
|
3213
|
-
for (const input of inputs) {
|
|
3214
|
-
const { temp_id, depends_on_temp_ids: _deps, ...createInput } = input;
|
|
3215
|
-
const task = createTask(createInput, d);
|
|
3216
|
-
if (temp_id)
|
|
3217
|
-
tempIdToRealId.set(temp_id, task.id);
|
|
3218
|
-
created.push({ temp_id: temp_id || null, id: task.id, short_id: task.short_id, title: task.title });
|
|
3219
|
-
}
|
|
3220
|
-
for (const input of inputs) {
|
|
3221
|
-
if (input.depends_on_temp_ids && input.depends_on_temp_ids.length > 0) {
|
|
3222
|
-
const taskId = input.temp_id ? tempIdToRealId.get(input.temp_id) : null;
|
|
3223
|
-
if (!taskId)
|
|
3224
|
-
continue;
|
|
3225
|
-
for (const depTempId of input.depends_on_temp_ids) {
|
|
3226
|
-
const depRealId = tempIdToRealId.get(depTempId);
|
|
3227
|
-
if (depRealId) {
|
|
3228
|
-
addDependency(taskId, depRealId, d);
|
|
3229
|
-
}
|
|
3230
|
-
}
|
|
3231
|
-
}
|
|
3232
|
-
}
|
|
3233
|
-
});
|
|
3234
|
-
tx();
|
|
3235
|
-
return { created };
|
|
3236
|
-
}
|
|
3237
|
-
function bulkUpdateTasks(taskIds, updates, db) {
|
|
3238
|
-
const d = db || getDatabase();
|
|
3239
|
-
let updated = 0;
|
|
3240
|
-
const failed = [];
|
|
3241
|
-
const tx = d.transaction(() => {
|
|
3242
|
-
for (const id of taskIds) {
|
|
3243
|
-
try {
|
|
3244
|
-
const task = getTask(id, d);
|
|
3245
|
-
if (!task) {
|
|
3246
|
-
failed.push({ id, error: "Task not found" });
|
|
3247
|
-
continue;
|
|
3248
|
-
}
|
|
3249
|
-
updateTask(id, { ...updates, version: task.version }, d);
|
|
3250
|
-
updated++;
|
|
3251
|
-
} catch (e) {
|
|
3252
|
-
failed.push({ id, error: e instanceof Error ? e.message : String(e) });
|
|
3253
|
-
}
|
|
3254
|
-
}
|
|
3255
|
-
});
|
|
3256
|
-
tx();
|
|
3257
|
-
return { updated, failed };
|
|
3258
|
-
}
|
|
3259
|
-
function getOverdueTasks(projectId, db) {
|
|
3260
|
-
const d = db || getDatabase();
|
|
3261
|
-
const nowStr = new Date().toISOString();
|
|
3262
|
-
let query = `SELECT * FROM tasks WHERE due_at IS NOT NULL AND due_at < ? AND status NOT IN ('completed', 'cancelled', 'failed')`;
|
|
3263
|
-
const params = [nowStr];
|
|
3264
|
-
if (projectId) {
|
|
3265
|
-
query += ` AND project_id = ?`;
|
|
3266
|
-
params.push(projectId);
|
|
3267
|
-
}
|
|
3268
|
-
query += ` ORDER BY due_at ASC`;
|
|
3269
|
-
const rows = d.query(query).all(...params);
|
|
3270
|
-
return rows.map(rowToTask);
|
|
3271
|
-
}
|
|
3272
|
-
var init_tasks = __esm(() => {
|
|
3273
|
-
init_types();
|
|
3274
|
-
init_database();
|
|
3275
|
-
init_projects();
|
|
3276
|
-
init_completion_guard();
|
|
3277
|
-
init_audit();
|
|
3278
|
-
init_recurrence();
|
|
3279
|
-
init_webhooks();
|
|
3280
|
-
init_templates();
|
|
3281
|
-
init_checklists();
|
|
3282
|
-
});
|
|
3283
|
-
|
|
3284
|
-
// src/db/agents.ts
|
|
3285
|
-
var exports_agents = {};
|
|
3286
|
-
__export(exports_agents, {
|
|
3287
|
-
updateAgentActivity: () => updateAgentActivity,
|
|
3288
|
-
updateAgent: () => updateAgent,
|
|
3289
|
-
unarchiveAgent: () => unarchiveAgent,
|
|
3290
|
-
releaseAgent: () => releaseAgent,
|
|
3291
|
-
registerAgent: () => registerAgent,
|
|
3292
|
-
matchCapabilities: () => matchCapabilities,
|
|
3293
|
-
listAgents: () => listAgents,
|
|
3294
|
-
isAgentConflict: () => isAgentConflict,
|
|
3295
|
-
getOrgChart: () => getOrgChart,
|
|
3296
|
-
getDirectReports: () => getDirectReports,
|
|
3297
|
-
getCapableAgents: () => getCapableAgents,
|
|
3298
|
-
getAvailableNamesFromPool: () => getAvailableNamesFromPool,
|
|
3299
|
-
getAgentByName: () => getAgentByName,
|
|
3300
|
-
getAgent: () => getAgent,
|
|
3301
|
-
deleteAgent: () => deleteAgent,
|
|
3302
|
-
autoReleaseStaleAgents: () => autoReleaseStaleAgents,
|
|
3303
|
-
archiveAgent: () => archiveAgent
|
|
3304
|
-
});
|
|
3305
|
-
function getActiveWindowMs() {
|
|
3306
|
-
const env = process.env["TODOS_AGENT_TIMEOUT_MS"];
|
|
3307
|
-
if (env) {
|
|
3308
|
-
const parsed = parseInt(env, 10);
|
|
3309
|
-
if (!isNaN(parsed) && parsed > 0)
|
|
3310
|
-
return parsed;
|
|
3311
|
-
}
|
|
3312
|
-
return 30 * 60 * 1000;
|
|
3313
|
-
}
|
|
3314
|
-
function autoReleaseStaleAgents(db) {
|
|
3315
|
-
if (process.env["TODOS_AGENT_AUTO_RELEASE"] !== "true")
|
|
3316
|
-
return 0;
|
|
3317
|
-
const d = db || getDatabase();
|
|
3318
|
-
const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
|
|
3319
|
-
const result = d.run("UPDATE agents SET session_id = NULL WHERE status = 'active' AND session_id IS NOT NULL AND last_seen_at < ?", [cutoff]);
|
|
3320
|
-
return result.changes;
|
|
3321
|
-
}
|
|
3322
|
-
function getAvailableNamesFromPool(pool, db) {
|
|
3323
|
-
autoReleaseStaleAgents(db);
|
|
3324
|
-
const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
|
|
3325
|
-
const activeNames = new Set(db.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
|
|
3326
|
-
return pool.filter((name) => !activeNames.has(name.toLowerCase()));
|
|
3327
|
-
}
|
|
3328
|
-
function shortUuid() {
|
|
3329
|
-
return crypto.randomUUID().slice(0, 8);
|
|
3330
|
-
}
|
|
3331
|
-
function rowToAgent(row) {
|
|
3332
|
-
return {
|
|
3333
|
-
...row,
|
|
3334
|
-
permissions: JSON.parse(row.permissions || '["*"]'),
|
|
3335
|
-
capabilities: JSON.parse(row.capabilities || "[]"),
|
|
3336
|
-
status: row.status || "active",
|
|
3337
|
-
metadata: JSON.parse(row.metadata || "{}")
|
|
3338
|
-
};
|
|
3339
|
-
}
|
|
3340
|
-
function registerAgent(input, db) {
|
|
3341
|
-
const d = db || getDatabase();
|
|
3342
|
-
const normalizedName = input.name.trim().toLowerCase();
|
|
3343
|
-
const existing = getAgentByName(normalizedName, d);
|
|
3344
|
-
if (existing) {
|
|
3345
|
-
const lastSeenMs = new Date(existing.last_seen_at).getTime();
|
|
3346
|
-
const activeWindowMs = getActiveWindowMs();
|
|
3347
|
-
const isActive = Date.now() - lastSeenMs < activeWindowMs;
|
|
3348
|
-
const sameSession = input.session_id && existing.session_id && input.session_id === existing.session_id;
|
|
3349
|
-
const differentSession = input.session_id && existing.session_id && input.session_id !== existing.session_id;
|
|
3350
|
-
const callerHasNoSession = !input.session_id;
|
|
3351
|
-
const existingHasActiveSession = existing.session_id && isActive;
|
|
3352
|
-
if (!input.force) {
|
|
3353
|
-
if (isActive && differentSession) {
|
|
3354
|
-
return buildConflictError(existing, lastSeenMs, input.pool, d);
|
|
3355
|
-
}
|
|
3356
|
-
if (callerHasNoSession && existingHasActiveSession) {
|
|
3357
|
-
return buildConflictError(existing, lastSeenMs, input.pool, d);
|
|
3358
|
-
}
|
|
3359
|
-
}
|
|
3360
|
-
const updates = ["last_seen_at = ?", "status = 'active'"];
|
|
3361
|
-
const params = [now()];
|
|
3362
|
-
if (input.session_id && !sameSession) {
|
|
3363
|
-
updates.push("session_id = ?");
|
|
3364
|
-
params.push(input.session_id);
|
|
3365
|
-
}
|
|
3366
|
-
if (input.working_dir) {
|
|
3367
|
-
updates.push("working_dir = ?");
|
|
3368
|
-
params.push(input.working_dir);
|
|
3369
|
-
}
|
|
3370
|
-
if (input.description) {
|
|
3371
|
-
updates.push("description = ?");
|
|
3372
|
-
params.push(input.description);
|
|
3373
|
-
}
|
|
3374
|
-
params.push(existing.id);
|
|
3375
|
-
d.run(`UPDATE agents SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
3376
|
-
return getAgent(existing.id, d);
|
|
3377
|
-
}
|
|
3378
|
-
const id = shortUuid();
|
|
3379
|
-
const timestamp = now();
|
|
3380
|
-
d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir)
|
|
3381
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3382
|
-
id,
|
|
3383
|
-
normalizedName,
|
|
3384
|
-
input.description || null,
|
|
3385
|
-
input.role || "agent",
|
|
3386
|
-
input.title || null,
|
|
3387
|
-
input.level || null,
|
|
3388
|
-
JSON.stringify(input.permissions || ["*"]),
|
|
3389
|
-
JSON.stringify(input.capabilities || []),
|
|
3390
|
-
input.reports_to || null,
|
|
3391
|
-
input.org_id || null,
|
|
3392
|
-
JSON.stringify(input.metadata || {}),
|
|
3393
|
-
timestamp,
|
|
3394
|
-
timestamp,
|
|
3395
|
-
input.session_id || null,
|
|
3396
|
-
input.working_dir || null
|
|
3397
|
-
]);
|
|
3398
|
-
return getAgent(id, d);
|
|
3399
|
-
}
|
|
3400
|
-
function isAgentConflict(result) {
|
|
3401
|
-
return result.conflict === true;
|
|
3402
|
-
}
|
|
3403
|
-
function buildConflictError(existing, lastSeenMs, pool, d) {
|
|
3404
|
-
const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
|
|
3405
|
-
const suggestions = pool ? getAvailableNamesFromPool(pool, d) : [];
|
|
3406
|
-
return {
|
|
3407
|
-
conflict: true,
|
|
3408
|
-
existing_id: existing.id,
|
|
3409
|
-
existing_name: existing.name,
|
|
3410
|
-
last_seen_at: existing.last_seen_at,
|
|
3411
|
-
session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
|
|
3412
|
-
working_dir: existing.working_dir,
|
|
3413
|
-
suggestions: suggestions.slice(0, 5),
|
|
3414
|
-
message: `Agent "${existing.name}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id ? "\u2026" + existing.session_id.slice(-4) : "none"}, dir: ${existing.working_dir ?? "unknown"}). Pass force: true to take over, or choose a different name.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
|
|
3415
|
-
};
|
|
3416
|
-
}
|
|
3417
|
-
function releaseAgent(id, session_id, db) {
|
|
3418
|
-
const d = db || getDatabase();
|
|
3419
|
-
const agent = getAgent(id, d);
|
|
3420
|
-
if (!agent)
|
|
3421
|
-
return false;
|
|
3422
|
-
if (session_id && agent.session_id && agent.session_id !== session_id) {
|
|
3423
|
-
return false;
|
|
3424
|
-
}
|
|
3425
|
-
const epoch = new Date(0).toISOString();
|
|
3426
|
-
d.run("UPDATE agents SET session_id = NULL, last_seen_at = ? WHERE id = ?", [epoch, id]);
|
|
3427
|
-
return true;
|
|
3428
|
-
}
|
|
3429
|
-
function getAgent(id, db) {
|
|
3430
|
-
const d = db || getDatabase();
|
|
3431
|
-
const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
|
|
3432
|
-
return row ? rowToAgent(row) : null;
|
|
3433
|
-
}
|
|
3434
|
-
function getAgentByName(name, db) {
|
|
3435
|
-
const d = db || getDatabase();
|
|
3436
|
-
const normalizedName = name.trim().toLowerCase();
|
|
3437
|
-
const row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
|
|
3438
|
-
return row ? rowToAgent(row) : null;
|
|
3439
|
-
}
|
|
3440
|
-
function listAgents(opts, db) {
|
|
3441
|
-
let d;
|
|
3442
|
-
let includeArchived = false;
|
|
3443
|
-
if (opts && typeof opts === "object" && "query" in opts) {
|
|
3444
|
-
d = opts;
|
|
3445
|
-
} else {
|
|
3446
|
-
includeArchived = opts?.include_archived ?? false;
|
|
3447
|
-
d = db || getDatabase();
|
|
3448
|
-
}
|
|
3449
|
-
autoReleaseStaleAgents(d);
|
|
3450
|
-
if (includeArchived) {
|
|
3451
|
-
return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
|
|
3452
|
-
}
|
|
3453
|
-
return d.query("SELECT * FROM agents WHERE status = 'active' ORDER BY name").all().map(rowToAgent);
|
|
3454
|
-
}
|
|
3455
|
-
function updateAgentActivity(id, db) {
|
|
3456
|
-
const d = db || getDatabase();
|
|
3457
|
-
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), id]);
|
|
3458
|
-
}
|
|
3459
|
-
function updateAgent(id, input, db) {
|
|
3460
|
-
const d = db || getDatabase();
|
|
3461
|
-
const agent = getAgent(id, d);
|
|
3462
|
-
if (!agent)
|
|
3463
|
-
throw new Error(`Agent not found: ${id}`);
|
|
3464
|
-
const sets = ["last_seen_at = ?"];
|
|
3465
|
-
const params = [now()];
|
|
3466
|
-
if (input.name !== undefined) {
|
|
3467
|
-
const newName = input.name.trim().toLowerCase();
|
|
3468
|
-
const holder = getAgentByName(newName, d);
|
|
3469
|
-
if (holder && holder.id !== id) {
|
|
3470
|
-
const lastSeenMs = new Date(holder.last_seen_at).getTime();
|
|
3471
|
-
const isActive = Date.now() - lastSeenMs < getActiveWindowMs();
|
|
3472
|
-
if (isActive && holder.status === "active") {
|
|
3473
|
-
throw new Error(`Cannot rename: name "${newName}" is held by active agent ${holder.id} (last seen ${Math.round((Date.now() - lastSeenMs) / 60000)}m ago)`);
|
|
3474
|
-
}
|
|
3475
|
-
const evictedName = `${holder.name}__evicted_${holder.id}`;
|
|
3476
|
-
d.run("UPDATE agents SET name = ? WHERE id = ?", [evictedName, holder.id]);
|
|
3477
|
-
}
|
|
3478
|
-
sets.push("name = ?");
|
|
3479
|
-
params.push(newName);
|
|
3480
|
-
}
|
|
3481
|
-
if (input.description !== undefined) {
|
|
3482
|
-
sets.push("description = ?");
|
|
3483
|
-
params.push(input.description);
|
|
3484
|
-
}
|
|
3485
|
-
if (input.role !== undefined) {
|
|
3486
|
-
sets.push("role = ?");
|
|
3487
|
-
params.push(input.role);
|
|
3488
|
-
}
|
|
3489
|
-
if (input.permissions !== undefined) {
|
|
3490
|
-
sets.push("permissions = ?");
|
|
3491
|
-
params.push(JSON.stringify(input.permissions));
|
|
3492
|
-
}
|
|
3493
|
-
if (input.capabilities !== undefined) {
|
|
3494
|
-
sets.push("capabilities = ?");
|
|
3495
|
-
params.push(JSON.stringify(input.capabilities));
|
|
3496
|
-
}
|
|
3497
|
-
if (input.title !== undefined) {
|
|
3498
|
-
sets.push("title = ?");
|
|
3499
|
-
params.push(input.title);
|
|
3500
|
-
}
|
|
3501
|
-
if (input.level !== undefined) {
|
|
3502
|
-
sets.push("level = ?");
|
|
3503
|
-
params.push(input.level);
|
|
3504
|
-
}
|
|
3505
|
-
if (input.reports_to !== undefined) {
|
|
3506
|
-
sets.push("reports_to = ?");
|
|
3507
|
-
params.push(input.reports_to);
|
|
3508
|
-
}
|
|
3509
|
-
if (input.org_id !== undefined) {
|
|
3510
|
-
sets.push("org_id = ?");
|
|
3511
|
-
params.push(input.org_id);
|
|
3512
|
-
}
|
|
3513
|
-
if (input.metadata !== undefined) {
|
|
3514
|
-
sets.push("metadata = ?");
|
|
3515
|
-
params.push(JSON.stringify(input.metadata));
|
|
3516
|
-
}
|
|
3517
|
-
params.push(id);
|
|
3518
|
-
d.run(`UPDATE agents SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
3519
|
-
return getAgent(id, d);
|
|
3520
|
-
}
|
|
3521
|
-
function deleteAgent(id, db) {
|
|
3522
|
-
const d = db || getDatabase();
|
|
3523
|
-
return d.run("UPDATE agents SET status = 'archived', last_seen_at = ? WHERE id = ?", [now(), id]).changes > 0;
|
|
3524
|
-
}
|
|
3525
|
-
function archiveAgent(id, db) {
|
|
3526
|
-
const d = db || getDatabase();
|
|
3527
|
-
d.run("UPDATE agents SET status = 'archived', last_seen_at = ? WHERE id = ?", [now(), id]);
|
|
3528
|
-
return getAgent(id, d);
|
|
3529
|
-
}
|
|
3530
|
-
function unarchiveAgent(id, db) {
|
|
3531
|
-
const d = db || getDatabase();
|
|
3532
|
-
d.run("UPDATE agents SET status = 'active', last_seen_at = ? WHERE id = ?", [now(), id]);
|
|
3533
|
-
return getAgent(id, d);
|
|
3534
|
-
}
|
|
3535
|
-
function getDirectReports(agentId, db) {
|
|
3536
|
-
const d = db || getDatabase();
|
|
3537
|
-
return d.query("SELECT * FROM agents WHERE reports_to = ? ORDER BY name").all(agentId).map(rowToAgent);
|
|
3538
|
-
}
|
|
3539
|
-
function getOrgChart(db) {
|
|
3540
|
-
const agents = listAgents(db);
|
|
3541
|
-
const byManager = new Map;
|
|
3542
|
-
for (const a of agents) {
|
|
3543
|
-
const key = a.reports_to;
|
|
3544
|
-
if (!byManager.has(key))
|
|
3545
|
-
byManager.set(key, []);
|
|
3546
|
-
byManager.get(key).push(a);
|
|
3547
|
-
}
|
|
3548
|
-
function buildTree(parentId) {
|
|
3549
|
-
const children = byManager.get(parentId) || [];
|
|
3550
|
-
return children.map((a) => ({ agent: a, reports: buildTree(a.id) }));
|
|
3551
|
-
}
|
|
3552
|
-
return buildTree(null);
|
|
3553
|
-
}
|
|
3554
|
-
function matchCapabilities(agentCapabilities, requiredCapabilities) {
|
|
3555
|
-
if (requiredCapabilities.length === 0)
|
|
3556
|
-
return 1;
|
|
3557
|
-
if (agentCapabilities.length === 0)
|
|
3558
|
-
return 0;
|
|
3559
|
-
const agentSet = new Set(agentCapabilities.map((c) => c.toLowerCase()));
|
|
3560
|
-
let matches = 0;
|
|
3561
|
-
for (const req of requiredCapabilities) {
|
|
3562
|
-
if (agentSet.has(req.toLowerCase()))
|
|
3563
|
-
matches++;
|
|
3564
|
-
}
|
|
3565
|
-
return matches / requiredCapabilities.length;
|
|
3566
|
-
}
|
|
3567
|
-
function getCapableAgents(capabilities, opts, db) {
|
|
3568
|
-
const agents = listAgents(db);
|
|
3569
|
-
const minScore = opts?.min_score ?? 0.1;
|
|
3570
|
-
const scored = agents.map((agent) => ({
|
|
3571
|
-
agent,
|
|
3572
|
-
score: matchCapabilities(agent.capabilities, capabilities)
|
|
3573
|
-
})).filter((entry) => entry.score >= minScore).sort((a, b) => b.score - a.score);
|
|
3574
|
-
return opts?.limit ? scored.slice(0, opts.limit) : scored;
|
|
3575
|
-
}
|
|
3576
|
-
var init_agents = __esm(() => {
|
|
3577
|
-
init_database();
|
|
3578
|
-
});
|
|
3579
|
-
|
|
3580
|
-
// src/db/comments.ts
|
|
3581
|
-
var exports_comments = {};
|
|
3582
|
-
__export(exports_comments, {
|
|
3583
|
-
logProgress: () => logProgress,
|
|
3584
|
-
listComments: () => listComments,
|
|
3585
|
-
getComment: () => getComment,
|
|
3586
|
-
deleteComment: () => deleteComment,
|
|
3587
|
-
addComment: () => addComment
|
|
3588
|
-
});
|
|
3589
|
-
function addComment(input, db) {
|
|
3590
|
-
const d = db || getDatabase();
|
|
3591
|
-
if (!getTask(input.task_id, d)) {
|
|
3592
|
-
throw new TaskNotFoundError(input.task_id);
|
|
3593
|
-
}
|
|
3594
|
-
const id = uuid();
|
|
3595
|
-
const timestamp = now();
|
|
3596
|
-
d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, type, progress_pct, created_at)
|
|
3597
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3598
|
-
id,
|
|
3599
|
-
input.task_id,
|
|
3600
|
-
input.agent_id || null,
|
|
3601
|
-
input.session_id || null,
|
|
3602
|
-
input.content,
|
|
3603
|
-
input.type || "comment",
|
|
3604
|
-
input.progress_pct ?? null,
|
|
3605
|
-
timestamp
|
|
3606
|
-
]);
|
|
3607
|
-
return getComment(id, d);
|
|
3608
|
-
}
|
|
3609
|
-
function logProgress(taskId, message, pct, agentId, db) {
|
|
3610
|
-
return addComment({ task_id: taskId, content: message, type: "progress", progress_pct: pct, agent_id: agentId }, db);
|
|
3611
|
-
}
|
|
3612
|
-
function getComment(id, db) {
|
|
3613
|
-
const d = db || getDatabase();
|
|
3614
|
-
return d.query("SELECT * FROM task_comments WHERE id = ?").get(id);
|
|
3615
|
-
}
|
|
3616
|
-
function listComments(taskId, db) {
|
|
3617
|
-
const d = db || getDatabase();
|
|
3618
|
-
return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
|
|
3619
|
-
}
|
|
3620
|
-
function deleteComment(id, db) {
|
|
3621
|
-
const d = db || getDatabase();
|
|
3622
|
-
const result = d.run("DELETE FROM task_comments WHERE id = ?", [id]);
|
|
3623
|
-
return result.changes > 0;
|
|
3624
|
-
}
|
|
3625
|
-
var init_comments = __esm(() => {
|
|
3626
|
-
init_types();
|
|
3627
|
-
init_database();
|
|
3628
|
-
init_tasks();
|
|
3629
|
-
});
|
|
3630
|
-
|
|
3631
|
-
// src/db/orgs.ts
|
|
3632
|
-
var exports_orgs = {};
|
|
3633
|
-
__export(exports_orgs, {
|
|
3634
|
-
updateOrg: () => updateOrg,
|
|
3635
|
-
listOrgs: () => listOrgs,
|
|
3636
|
-
getOrgByName: () => getOrgByName,
|
|
3637
|
-
getOrg: () => getOrg,
|
|
3638
|
-
deleteOrg: () => deleteOrg,
|
|
3639
|
-
createOrg: () => createOrg
|
|
3640
|
-
});
|
|
3641
|
-
function rowToOrg(row) {
|
|
3642
|
-
return { ...row, metadata: JSON.parse(row.metadata || "{}") };
|
|
3643
|
-
}
|
|
3644
|
-
function createOrg(input, db) {
|
|
3645
|
-
const d = db || getDatabase();
|
|
3646
|
-
const id = uuid();
|
|
3647
|
-
const timestamp = now();
|
|
3648
|
-
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]);
|
|
3649
|
-
return getOrg(id, d);
|
|
3650
|
-
}
|
|
3651
|
-
function getOrg(id, db) {
|
|
3652
|
-
const d = db || getDatabase();
|
|
3653
|
-
const row = d.query("SELECT * FROM orgs WHERE id = ?").get(id);
|
|
3654
|
-
return row ? rowToOrg(row) : null;
|
|
3655
|
-
}
|
|
3656
|
-
function getOrgByName(name, db) {
|
|
3657
|
-
const d = db || getDatabase();
|
|
3658
|
-
const row = d.query("SELECT * FROM orgs WHERE name = ?").get(name);
|
|
3659
|
-
return row ? rowToOrg(row) : null;
|
|
3660
|
-
}
|
|
3661
|
-
function listOrgs(db) {
|
|
3662
|
-
const d = db || getDatabase();
|
|
3663
|
-
return d.query("SELECT * FROM orgs ORDER BY name").all().map(rowToOrg);
|
|
3664
|
-
}
|
|
3665
|
-
function updateOrg(id, input, db) {
|
|
3666
|
-
const d = db || getDatabase();
|
|
3667
|
-
const org = getOrg(id, d);
|
|
3668
|
-
if (!org)
|
|
3669
|
-
throw new Error(`Org not found: ${id}`);
|
|
3670
|
-
const sets = ["updated_at = ?"];
|
|
3671
|
-
const params = [now()];
|
|
3672
|
-
if (input.name !== undefined) {
|
|
3673
|
-
sets.push("name = ?");
|
|
3674
|
-
params.push(input.name);
|
|
3675
|
-
}
|
|
3676
|
-
if (input.description !== undefined) {
|
|
3677
|
-
sets.push("description = ?");
|
|
3678
|
-
params.push(input.description);
|
|
3679
|
-
}
|
|
3680
|
-
if (input.metadata !== undefined) {
|
|
3681
|
-
sets.push("metadata = ?");
|
|
3682
|
-
params.push(JSON.stringify(input.metadata));
|
|
3683
|
-
}
|
|
3684
|
-
params.push(id);
|
|
3685
|
-
d.run(`UPDATE orgs SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
3686
|
-
return getOrg(id, d);
|
|
3687
|
-
}
|
|
3688
|
-
function deleteOrg(id, db) {
|
|
3689
|
-
const d = db || getDatabase();
|
|
3690
|
-
return d.run("DELETE FROM orgs WHERE id = ?", [id]).changes > 0;
|
|
3691
|
-
}
|
|
3692
|
-
var init_orgs = __esm(() => {
|
|
3693
|
-
init_database();
|
|
3694
|
-
});
|
|
3695
|
-
|
|
3696
|
-
// src/server/serve.ts
|
|
3697
|
-
init_tasks();
|
|
3698
|
-
init_projects();
|
|
3699
|
-
init_agents();
|
|
3700
|
-
import { existsSync as existsSync4 } from "fs";
|
|
3701
|
-
import { join as join3, dirname as dirname2, extname, resolve as resolve2, sep } from "path";
|
|
3702
|
-
import { fileURLToPath } from "url";
|
|
3703
|
-
|
|
3704
|
-
// src/db/plans.ts
|
|
3705
|
-
init_types();
|
|
3706
|
-
init_database();
|
|
3707
|
-
function createPlan(input, db) {
|
|
3708
|
-
const d = db || getDatabase();
|
|
3709
|
-
const id = uuid();
|
|
3710
|
-
const timestamp = now();
|
|
3711
|
-
d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at)
|
|
3712
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3713
|
-
id,
|
|
3714
|
-
input.project_id || null,
|
|
3715
|
-
input.task_list_id || null,
|
|
3716
|
-
input.agent_id || null,
|
|
3717
|
-
input.name,
|
|
3718
|
-
input.description || null,
|
|
3719
|
-
input.status || "active",
|
|
3720
|
-
timestamp,
|
|
3721
|
-
timestamp
|
|
3722
|
-
]);
|
|
3723
|
-
return getPlan(id, d);
|
|
3724
|
-
}
|
|
3725
|
-
function getPlan(id, db) {
|
|
3726
|
-
const d = db || getDatabase();
|
|
3727
|
-
const row = d.query("SELECT * FROM plans WHERE id = ?").get(id);
|
|
3728
|
-
return row;
|
|
3729
|
-
}
|
|
3730
|
-
function listPlans(projectId, db) {
|
|
3731
|
-
const d = db || getDatabase();
|
|
3732
|
-
if (projectId) {
|
|
3733
|
-
return d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
|
|
3734
|
-
}
|
|
3735
|
-
return d.query("SELECT * FROM plans ORDER BY created_at DESC").all();
|
|
3736
|
-
}
|
|
3737
|
-
function updatePlan(id, input, db) {
|
|
3738
|
-
const d = db || getDatabase();
|
|
3739
|
-
const plan = getPlan(id, d);
|
|
3740
|
-
if (!plan)
|
|
3741
|
-
throw new PlanNotFoundError(id);
|
|
3742
|
-
const sets = ["updated_at = ?"];
|
|
3743
|
-
const params = [now()];
|
|
3744
|
-
if (input.name !== undefined) {
|
|
3745
|
-
sets.push("name = ?");
|
|
3746
|
-
params.push(input.name);
|
|
3747
|
-
}
|
|
3748
|
-
if (input.description !== undefined) {
|
|
3749
|
-
sets.push("description = ?");
|
|
3750
|
-
params.push(input.description);
|
|
3751
|
-
}
|
|
3752
|
-
if (input.status !== undefined) {
|
|
3753
|
-
sets.push("status = ?");
|
|
3754
|
-
params.push(input.status);
|
|
3755
|
-
}
|
|
3756
|
-
if (input.task_list_id !== undefined) {
|
|
3757
|
-
sets.push("task_list_id = ?");
|
|
3758
|
-
params.push(input.task_list_id);
|
|
3759
|
-
}
|
|
3760
|
-
if (input.agent_id !== undefined) {
|
|
3761
|
-
sets.push("agent_id = ?");
|
|
3762
|
-
params.push(input.agent_id);
|
|
3763
|
-
}
|
|
3764
|
-
params.push(id);
|
|
3765
|
-
d.run(`UPDATE plans SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
3766
|
-
return getPlan(id, d);
|
|
3767
|
-
}
|
|
3768
|
-
function deletePlan(id, db) {
|
|
3769
|
-
const d = db || getDatabase();
|
|
3770
|
-
const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
|
|
3771
|
-
return result.changes > 0;
|
|
3772
|
-
}
|
|
3773
|
-
|
|
3774
|
-
// src/server/serve.ts
|
|
3775
|
-
init_database();
|
|
3776
|
-
function resolveDashboardDir() {
|
|
3777
|
-
const candidates = [];
|
|
3778
|
-
try {
|
|
3779
|
-
const scriptDir = dirname2(fileURLToPath(import.meta.url));
|
|
3780
|
-
candidates.push(join3(scriptDir, "..", "dashboard", "dist"));
|
|
3781
|
-
candidates.push(join3(scriptDir, "..", "..", "dashboard", "dist"));
|
|
3782
|
-
} catch {}
|
|
3783
|
-
if (process.argv[1]) {
|
|
3784
|
-
const mainDir = dirname2(process.argv[1]);
|
|
3785
|
-
candidates.push(join3(mainDir, "..", "dashboard", "dist"));
|
|
3786
|
-
candidates.push(join3(mainDir, "..", "..", "dashboard", "dist"));
|
|
3787
|
-
}
|
|
3788
|
-
candidates.push(join3(process.cwd(), "dashboard", "dist"));
|
|
3789
|
-
for (const candidate of candidates) {
|
|
3790
|
-
if (existsSync4(candidate))
|
|
3791
|
-
return candidate;
|
|
3792
|
-
}
|
|
3793
|
-
return join3(process.cwd(), "dashboard", "dist");
|
|
3794
|
-
}
|
|
3795
|
-
var MIME_TYPES = {
|
|
3796
|
-
".html": "text/html; charset=utf-8",
|
|
3797
|
-
".js": "application/javascript",
|
|
3798
|
-
".css": "text/css",
|
|
3799
|
-
".json": "application/json",
|
|
3800
|
-
".png": "image/png",
|
|
3801
|
-
".jpg": "image/jpeg",
|
|
3802
|
-
".svg": "image/svg+xml",
|
|
3803
|
-
".ico": "image/x-icon",
|
|
3804
|
-
".woff": "font/woff",
|
|
3805
|
-
".woff2": "font/woff2"
|
|
3806
|
-
};
|
|
3807
|
-
var SECURITY_HEADERS = {
|
|
3808
|
-
"X-Content-Type-Options": "nosniff",
|
|
3809
|
-
"X-Frame-Options": "DENY"
|
|
3810
|
-
};
|
|
3811
|
-
function json(data, status = 200, port) {
|
|
3812
|
-
return new Response(JSON.stringify(data), {
|
|
3813
|
-
status,
|
|
3814
|
-
headers: {
|
|
3815
|
-
"Content-Type": "application/json",
|
|
3816
|
-
"Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
|
|
3817
|
-
...SECURITY_HEADERS
|
|
3818
|
-
}
|
|
3819
|
-
});
|
|
3820
|
-
}
|
|
3821
|
-
function serveStaticFile(filePath) {
|
|
3822
|
-
if (!existsSync4(filePath))
|
|
3823
|
-
return null;
|
|
3824
|
-
const ext = extname(filePath);
|
|
3825
|
-
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
3826
|
-
return new Response(Bun.file(filePath), {
|
|
3827
|
-
headers: { "Content-Type": contentType }
|
|
3828
|
-
});
|
|
3829
|
-
}
|
|
3830
|
-
function taskToSummary(task, fields) {
|
|
3831
|
-
const full = {
|
|
3832
|
-
id: task.id,
|
|
3833
|
-
short_id: task.short_id,
|
|
3834
|
-
title: task.title,
|
|
3835
|
-
description: task.description,
|
|
3836
|
-
status: task.status,
|
|
3837
|
-
priority: task.priority,
|
|
3838
|
-
project_id: task.project_id,
|
|
3839
|
-
plan_id: task.plan_id,
|
|
3840
|
-
task_list_id: task.task_list_id,
|
|
3841
|
-
agent_id: task.agent_id,
|
|
3842
|
-
assigned_to: task.assigned_to,
|
|
3843
|
-
locked_by: task.locked_by,
|
|
3844
|
-
tags: task.tags,
|
|
3845
|
-
version: task.version,
|
|
3846
|
-
created_at: task.created_at,
|
|
3847
|
-
updated_at: task.updated_at,
|
|
3848
|
-
completed_at: task.completed_at,
|
|
3849
|
-
due_at: task.due_at,
|
|
3850
|
-
recurrence_rule: task.recurrence_rule
|
|
3851
|
-
};
|
|
3852
|
-
if (!fields || fields.length === 0)
|
|
3853
|
-
return full;
|
|
3854
|
-
return Object.fromEntries(fields.map((f) => [f, full[f] ?? null]));
|
|
3855
|
-
}
|
|
3856
|
-
async function startServer(port, options) {
|
|
3857
|
-
const shouldOpen = options?.open ?? true;
|
|
3858
|
-
getDatabase();
|
|
3859
|
-
const sseClients = new Set;
|
|
3860
|
-
const filteredSseClients = new Set;
|
|
3861
|
-
function broadcastEvent(event) {
|
|
3862
|
-
const data = JSON.stringify({ ...event, timestamp: new Date().toISOString() });
|
|
3863
|
-
for (const controller of sseClients) {
|
|
3864
|
-
try {
|
|
3865
|
-
controller.enqueue(`data: ${data}
|
|
3866
|
-
|
|
3867
|
-
`);
|
|
3868
|
-
} catch {
|
|
3869
|
-
sseClients.delete(controller);
|
|
3870
|
-
}
|
|
3871
|
-
}
|
|
3872
|
-
const eventName = `task.${event.action}`;
|
|
3873
|
-
for (const client of filteredSseClients) {
|
|
3874
|
-
if (client.events && !client.events.has(eventName) && !client.events.has("*"))
|
|
3875
|
-
continue;
|
|
3876
|
-
if (client.agentId && event.agent_id !== client.agentId)
|
|
3877
|
-
continue;
|
|
3878
|
-
if (client.projectId && event.project_id !== client.projectId)
|
|
3879
|
-
continue;
|
|
3880
|
-
try {
|
|
3881
|
-
client.controller.enqueue(`event: ${eventName}
|
|
3882
|
-
data: ${data}
|
|
3883
|
-
|
|
3884
|
-
`);
|
|
3885
|
-
} catch {
|
|
3886
|
-
filteredSseClients.delete(client);
|
|
3887
|
-
}
|
|
3888
|
-
}
|
|
3889
|
-
}
|
|
3890
|
-
const dashboardDir = resolveDashboardDir();
|
|
3891
|
-
const dashboardExists = existsSync4(dashboardDir);
|
|
3892
|
-
if (!dashboardExists) {
|
|
3893
|
-
console.error(`
|
|
3894
|
-
Dashboard not found at: ${dashboardDir}`);
|
|
3895
|
-
console.error(`Run this to build it:
|
|
3896
|
-
`);
|
|
3897
|
-
console.error(` cd dashboard && bun install && bun run build
|
|
3898
|
-
`);
|
|
3899
|
-
}
|
|
3900
|
-
const hostname = options?.host || "127.0.0.1";
|
|
3901
|
-
const server = Bun.serve({
|
|
3902
|
-
port,
|
|
3903
|
-
hostname,
|
|
3904
|
-
async fetch(req) {
|
|
3905
|
-
const url = new URL(req.url);
|
|
3906
|
-
const path = url.pathname;
|
|
3907
|
-
const method = req.method;
|
|
3908
|
-
if (method === "OPTIONS") {
|
|
3909
|
-
return new Response(null, {
|
|
3910
|
-
headers: {
|
|
3911
|
-
"Access-Control-Allow-Origin": `http://localhost:${port}`,
|
|
3912
|
-
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
3913
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
3914
|
-
}
|
|
3915
|
-
});
|
|
3916
|
-
}
|
|
3917
|
-
if (path === "/api/events" && method === "GET") {
|
|
3918
|
-
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
3919
|
-
const projectId = url.searchParams.get("project_id") || undefined;
|
|
3920
|
-
if (agentId || projectId) {
|
|
3921
|
-
const client = { controller: null, agentId, projectId, events: undefined };
|
|
3922
|
-
const stream2 = new ReadableStream({
|
|
3923
|
-
start(controller) {
|
|
3924
|
-
client.controller = controller;
|
|
3925
|
-
filteredSseClients.add(client);
|
|
3926
|
-
controller.enqueue(`data: ${JSON.stringify({ type: "connected", agent_id: agentId, project_id: projectId, timestamp: new Date().toISOString() })}
|
|
3927
|
-
|
|
3928
|
-
`);
|
|
3929
|
-
},
|
|
3930
|
-
cancel() {
|
|
3931
|
-
filteredSseClients.delete(client);
|
|
3932
|
-
}
|
|
3933
|
-
});
|
|
3934
|
-
return new Response(stream2, {
|
|
3935
|
-
headers: {
|
|
3936
|
-
"Content-Type": "text/event-stream",
|
|
3937
|
-
"Cache-Control": "no-cache",
|
|
3938
|
-
Connection: "keep-alive",
|
|
3939
|
-
"Access-Control-Allow-Origin": "*"
|
|
3940
|
-
}
|
|
3941
|
-
});
|
|
3942
|
-
}
|
|
3943
|
-
const stream = new ReadableStream({
|
|
3944
|
-
start(controller) {
|
|
3945
|
-
sseClients.add(controller);
|
|
3946
|
-
controller.enqueue(`data: ${JSON.stringify({ type: "connected", timestamp: new Date().toISOString() })}
|
|
3947
|
-
|
|
3948
|
-
`);
|
|
3949
|
-
},
|
|
3950
|
-
cancel(controller) {
|
|
3951
|
-
sseClients.delete(controller);
|
|
3952
|
-
}
|
|
3953
|
-
});
|
|
3954
|
-
return new Response(stream, {
|
|
3955
|
-
headers: {
|
|
3956
|
-
"Content-Type": "text/event-stream",
|
|
3957
|
-
"Cache-Control": "no-cache",
|
|
3958
|
-
Connection: "keep-alive",
|
|
3959
|
-
"Access-Control-Allow-Origin": `http://localhost:${port}`
|
|
3960
|
-
}
|
|
3961
|
-
});
|
|
3962
|
-
}
|
|
3963
|
-
if (path === "/api/tasks/stream" && method === "GET") {
|
|
3964
|
-
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
3965
|
-
const projectId = url.searchParams.get("project_id") || undefined;
|
|
3966
|
-
const eventsParam = url.searchParams.get("events");
|
|
3967
|
-
const eventFilter = eventsParam ? new Set(eventsParam.split(",").map((e) => e.trim())) : undefined;
|
|
3968
|
-
const client = { controller: null, agentId, projectId, events: eventFilter };
|
|
3969
|
-
const stream = new ReadableStream({
|
|
3970
|
-
start(controller) {
|
|
3971
|
-
client.controller = controller;
|
|
3972
|
-
filteredSseClients.add(client);
|
|
3973
|
-
controller.enqueue(`: connected
|
|
3974
|
-
|
|
3975
|
-
data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Date().toISOString() })}
|
|
3976
|
-
|
|
3977
|
-
`);
|
|
3978
|
-
},
|
|
3979
|
-
cancel() {
|
|
3980
|
-
filteredSseClients.delete(client);
|
|
3981
|
-
}
|
|
3982
|
-
});
|
|
3983
|
-
return new Response(stream, {
|
|
3984
|
-
headers: {
|
|
3985
|
-
"Content-Type": "text/event-stream",
|
|
3986
|
-
"Cache-Control": "no-cache",
|
|
3987
|
-
Connection: "keep-alive",
|
|
3988
|
-
"Access-Control-Allow-Origin": "*"
|
|
3989
|
-
}
|
|
3990
|
-
});
|
|
3991
|
-
}
|
|
3992
|
-
if (path === "/api/health" && method === "GET") {
|
|
3993
|
-
const all = listTasks({ limit: 1e4 });
|
|
3994
|
-
const stale = all.filter((t) => t.status === "in_progress" && new Date(t.updated_at).getTime() < Date.now() - 30 * 60 * 1000);
|
|
3995
|
-
const overdue = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < new Date().toISOString());
|
|
3996
|
-
return json({ status: stale.length === 0 && overdue.length === 0 ? "ok" : "warn", tasks: all.length, stale: stale.length, overdue_recurring: overdue.length, timestamp: new Date().toISOString() }, 200, port);
|
|
3997
|
-
}
|
|
3998
|
-
if (path === "/api/stats" && method === "GET") {
|
|
3999
|
-
const all = listTasks({ limit: 1e4 });
|
|
4000
|
-
const projects = listProjects();
|
|
4001
|
-
const agents = listAgents();
|
|
4002
|
-
const { getStaleTasks: getStaleForStats } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
|
|
4003
|
-
const staleItems = getStaleForStats(30);
|
|
4004
|
-
const nowStr = new Date().toISOString();
|
|
4005
|
-
const overdueRecurring = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < nowStr).length;
|
|
4006
|
-
const recurringTasks = all.filter((t) => t.recurrence_rule).length;
|
|
4007
|
-
return json({
|
|
4008
|
-
total_tasks: all.length,
|
|
4009
|
-
pending: all.filter((t) => t.status === "pending").length,
|
|
4010
|
-
in_progress: all.filter((t) => t.status === "in_progress").length,
|
|
4011
|
-
completed: all.filter((t) => t.status === "completed").length,
|
|
4012
|
-
failed: all.filter((t) => t.status === "failed").length,
|
|
4013
|
-
cancelled: all.filter((t) => t.status === "cancelled").length,
|
|
4014
|
-
projects: projects.length,
|
|
4015
|
-
agents: agents.length,
|
|
4016
|
-
stale_count: staleItems.length,
|
|
4017
|
-
overdue_recurring: overdueRecurring,
|
|
4018
|
-
recurring_tasks: recurringTasks
|
|
4019
|
-
}, 200, port);
|
|
4020
|
-
}
|
|
4021
|
-
if (path === "/api/tasks" && method === "GET") {
|
|
4022
|
-
const status = url.searchParams.get("status") || undefined;
|
|
4023
|
-
const projectId = url.searchParams.get("project_id") || undefined;
|
|
4024
|
-
const sessionId = url.searchParams.get("session_id") || undefined;
|
|
4025
|
-
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
4026
|
-
const limitParam = url.searchParams.get("limit");
|
|
4027
|
-
const offsetParam = url.searchParams.get("offset");
|
|
4028
|
-
const fieldsParam = url.searchParams.get("fields");
|
|
4029
|
-
const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
4030
|
-
const tasks = listTasks({
|
|
4031
|
-
status,
|
|
4032
|
-
project_id: projectId,
|
|
4033
|
-
session_id: sessionId,
|
|
4034
|
-
agent_id: agentId,
|
|
4035
|
-
limit: limitParam ? parseInt(limitParam, 10) : undefined,
|
|
4036
|
-
offset: offsetParam ? parseInt(offsetParam, 10) : undefined
|
|
4037
|
-
});
|
|
4038
|
-
return json(tasks.map((t) => taskToSummary(t, fields)), 200, port);
|
|
4039
|
-
}
|
|
4040
|
-
if (path === "/api/tasks" && method === "POST") {
|
|
4041
|
-
try {
|
|
4042
|
-
const body = await req.json();
|
|
4043
|
-
if (!body.title)
|
|
4044
|
-
return json({ error: "Missing 'title'" }, 400, port);
|
|
4045
|
-
const task = createTask({
|
|
4046
|
-
title: body.title,
|
|
4047
|
-
description: body.description,
|
|
4048
|
-
priority: body.priority,
|
|
4049
|
-
project_id: body.project_id
|
|
4050
|
-
});
|
|
4051
|
-
broadcastEvent({ type: "task", task_id: task.id, action: "created", agent_id: task.agent_id, project_id: task.project_id });
|
|
4052
|
-
return json(taskToSummary(task), 201, port);
|
|
4053
|
-
} catch (e) {
|
|
4054
|
-
return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
|
|
4055
|
-
}
|
|
4056
|
-
}
|
|
4057
|
-
if (path === "/api/tasks/export" && method === "GET") {
|
|
4058
|
-
const format = url.searchParams.get("format") || "json";
|
|
4059
|
-
const status = url.searchParams.get("status") || undefined;
|
|
4060
|
-
const projectId = url.searchParams.get("project_id") || undefined;
|
|
4061
|
-
const tasks = listTasks({ status, project_id: projectId, limit: 1e4 });
|
|
4062
|
-
const summaries = tasks.map((t) => taskToSummary(t));
|
|
4063
|
-
if (format === "csv") {
|
|
4064
|
-
const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
|
|
4065
|
-
const rows = summaries.map((t) => headers.map((h) => {
|
|
4066
|
-
const val = t[h];
|
|
4067
|
-
if (val === null || val === undefined)
|
|
4068
|
-
return "";
|
|
4069
|
-
const str = String(val);
|
|
4070
|
-
return str.includes(",") || str.includes('"') || str.includes(`
|
|
4071
|
-
`) ? `"${str.replace(/"/g, '""')}"` : str;
|
|
4072
|
-
}).join(","));
|
|
4073
|
-
const csv = [headers.join(","), ...rows].join(`
|
|
4074
|
-
`);
|
|
4075
|
-
return new Response(csv, {
|
|
4076
|
-
headers: {
|
|
4077
|
-
"Content-Type": "text/csv",
|
|
4078
|
-
"Content-Disposition": "attachment; filename=tasks.csv",
|
|
4079
|
-
...SECURITY_HEADERS
|
|
4080
|
-
}
|
|
4081
|
-
});
|
|
4082
|
-
}
|
|
4083
|
-
return new Response(JSON.stringify(summaries, null, 2), {
|
|
4084
|
-
headers: {
|
|
4085
|
-
"Content-Type": "application/json",
|
|
4086
|
-
"Content-Disposition": "attachment; filename=tasks.json",
|
|
4087
|
-
...SECURITY_HEADERS
|
|
4088
|
-
}
|
|
4089
|
-
});
|
|
4090
|
-
}
|
|
4091
|
-
if (path === "/api/tasks/bulk" && method === "POST") {
|
|
4092
|
-
try {
|
|
4093
|
-
const body = await req.json();
|
|
4094
|
-
if (!body.ids?.length || !body.action)
|
|
4095
|
-
return json({ error: "Missing ids or action" }, 400, port);
|
|
4096
|
-
const results = [];
|
|
4097
|
-
for (const id of body.ids) {
|
|
4098
|
-
try {
|
|
4099
|
-
if (body.action === "delete") {
|
|
4100
|
-
deleteTask(id);
|
|
4101
|
-
results.push({ id, success: true });
|
|
4102
|
-
} else if (body.action === "start") {
|
|
4103
|
-
startTask(id, "dashboard");
|
|
4104
|
-
results.push({ id, success: true });
|
|
4105
|
-
} else if (body.action === "complete") {
|
|
4106
|
-
completeTask(id, "dashboard");
|
|
4107
|
-
results.push({ id, success: true });
|
|
4108
|
-
}
|
|
4109
|
-
} catch (e) {
|
|
4110
|
-
results.push({ id, success: false, error: e instanceof Error ? e.message : "Failed" });
|
|
4111
|
-
}
|
|
4112
|
-
}
|
|
4113
|
-
return json({ results, succeeded: results.filter((r) => r.success).length, failed: results.filter((r) => !r.success).length }, 200, port);
|
|
4114
|
-
} catch (e) {
|
|
4115
|
-
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
4116
|
-
}
|
|
4117
|
-
}
|
|
4118
|
-
if (path === "/api/tasks/status" && method === "GET") {
|
|
4119
|
-
try {
|
|
4120
|
-
const projectId = url.searchParams.get("project_id") || undefined;
|
|
4121
|
-
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
4122
|
-
const { getStatus: getStatus2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
|
|
4123
|
-
const status = getStatus2(projectId ? { project_id: projectId } : undefined, agentId);
|
|
4124
|
-
return json(status, 200, port);
|
|
4125
|
-
} catch (e) {
|
|
4126
|
-
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
4127
|
-
}
|
|
4128
|
-
}
|
|
4129
|
-
if (path === "/api/tasks/next" && method === "GET") {
|
|
4130
|
-
try {
|
|
4131
|
-
const projectId = url.searchParams.get("project_id") || undefined;
|
|
4132
|
-
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
4133
|
-
const { getNextTask: getNextTask2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
|
|
4134
|
-
const task = getNextTask2(agentId, projectId ? { project_id: projectId } : undefined);
|
|
4135
|
-
return json({ task: task ? taskToSummary(task) : null }, 200, port);
|
|
4136
|
-
} catch (e) {
|
|
4137
|
-
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
4138
|
-
}
|
|
4139
|
-
}
|
|
4140
|
-
if (path === "/api/tasks/active" && method === "GET") {
|
|
4141
|
-
try {
|
|
4142
|
-
const projectId = url.searchParams.get("project_id") || undefined;
|
|
4143
|
-
const { getActiveWork: getActiveWork2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
|
|
4144
|
-
const work = getActiveWork2(projectId ? { project_id: projectId } : undefined);
|
|
4145
|
-
return json({ active: work, count: work.length }, 200, port);
|
|
4146
|
-
} catch (e) {
|
|
4147
|
-
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
4148
|
-
}
|
|
4149
|
-
}
|
|
4150
|
-
if (path === "/api/tasks/stale" && method === "GET") {
|
|
4151
|
-
try {
|
|
4152
|
-
const projectId = url.searchParams.get("project_id") || undefined;
|
|
4153
|
-
const minutes = parseInt(url.searchParams.get("minutes") || "30", 10);
|
|
4154
|
-
const { getStaleTasks: getStaleTasks2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
|
|
4155
|
-
const tasks = getStaleTasks2(minutes, projectId ? { project_id: projectId } : undefined);
|
|
4156
|
-
return json({ tasks: tasks.map((t) => taskToSummary(t)), count: tasks.length }, 200, port);
|
|
4157
|
-
} catch (e) {
|
|
4158
|
-
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
4159
|
-
}
|
|
4160
|
-
}
|
|
4161
|
-
if (path === "/api/tasks/changed" && method === "GET") {
|
|
4162
|
-
try {
|
|
4163
|
-
const since = url.searchParams.get("since");
|
|
4164
|
-
if (!since)
|
|
4165
|
-
return json({ error: "since parameter required (ISO date string)" }, 400, port);
|
|
4166
|
-
const projectId = url.searchParams.get("project_id") || undefined;
|
|
4167
|
-
const { getTasksChangedSince: getTasksChangedSince2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
|
|
4168
|
-
const tasks = getTasksChangedSince2(since, projectId ? { project_id: projectId } : undefined);
|
|
4169
|
-
return json({ tasks: tasks.map((t) => taskToSummary(t)), count: tasks.length, since }, 200, port);
|
|
4170
|
-
} catch (e) {
|
|
4171
|
-
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
4172
|
-
}
|
|
4173
|
-
}
|
|
4174
|
-
if (path === "/api/tasks/context" && method === "GET") {
|
|
4175
|
-
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
4176
|
-
const projectId = url.searchParams.get("project_id") || undefined;
|
|
4177
|
-
const format = url.searchParams.get("format") || "text";
|
|
4178
|
-
const { getStatus: getStatus2, getNextTask: getNextTask2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
|
|
4179
|
-
const filters = projectId ? { project_id: projectId } : undefined;
|
|
4180
|
-
const status = getStatus2(filters, agentId);
|
|
4181
|
-
const next = getNextTask2(agentId, filters);
|
|
4182
|
-
if (format === "json") {
|
|
4183
|
-
return json({ status, next_task: next ? taskToSummary(next) : null }, 200, port);
|
|
4184
|
-
}
|
|
4185
|
-
const lines = [];
|
|
4186
|
-
lines.push(`Tasks: ${status.pending} pending | ${status.in_progress} active | ${status.completed} done`);
|
|
4187
|
-
if (status.stale_count > 0)
|
|
4188
|
-
lines.push(`\u26A0 ${status.stale_count} stale tasks stuck in-progress`);
|
|
4189
|
-
if (status.overdue_recurring > 0)
|
|
4190
|
-
lines.push(`\uD83D\uDD01 ${status.overdue_recurring} overdue recurring tasks`);
|
|
4191
|
-
if (status.active_work.length > 0) {
|
|
4192
|
-
lines.push(`Active: ${status.active_work.slice(0, 3).map((w) => `${w.short_id || w.id.slice(0, 8)} (${w.assigned_to || "?"})`).join(", ")}`);
|
|
4193
|
-
}
|
|
4194
|
-
if (next)
|
|
4195
|
-
lines.push(`Next up: ${next.short_id || next.id.slice(0, 8)} [${next.priority}] ${next.title}`);
|
|
4196
|
-
const text = lines.join(`
|
|
4197
|
-
`);
|
|
4198
|
-
return new Response(text, { headers: { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" } });
|
|
4199
|
-
}
|
|
4200
|
-
const attachmentsMatch = path.match(/^\/api\/tasks\/([^/]+)\/attachments$/);
|
|
4201
|
-
if (attachmentsMatch && method === "GET") {
|
|
4202
|
-
const id = attachmentsMatch[1];
|
|
4203
|
-
const task = getTask(id);
|
|
4204
|
-
if (!task)
|
|
4205
|
-
return json({ error: "Task not found" }, 404, port);
|
|
4206
|
-
const evidence = task.metadata?._evidence || {};
|
|
4207
|
-
const attachmentIds = evidence.attachments || [];
|
|
4208
|
-
return json({ task_id: id, short_id: task.short_id, attachment_ids: attachmentIds, count: attachmentIds.length, files_changed: evidence.files_changed, commit_hash: evidence.commit_hash, notes: evidence.notes }, 200, port);
|
|
4209
|
-
}
|
|
4210
|
-
const progressMatch = path.match(/^\/api\/tasks\/([^/]+)\/progress$/);
|
|
4211
|
-
if (progressMatch) {
|
|
4212
|
-
const id = progressMatch[1];
|
|
4213
|
-
const task = getTask(id);
|
|
4214
|
-
if (!task)
|
|
4215
|
-
return json({ error: "Task not found" }, 404, port);
|
|
4216
|
-
if (method === "GET") {
|
|
4217
|
-
const { listComments: listComments2 } = await Promise.resolve().then(() => (init_comments(), exports_comments));
|
|
4218
|
-
const all = listComments2(id);
|
|
4219
|
-
const progress = all.filter((c) => c.type === "progress");
|
|
4220
|
-
const latest = progress[progress.length - 1] || null;
|
|
4221
|
-
return json({ task_id: id, progress_entries: progress, latest, count: progress.length }, 200, port);
|
|
4222
|
-
}
|
|
4223
|
-
if (method === "POST") {
|
|
4224
|
-
try {
|
|
4225
|
-
const body = await req.json();
|
|
4226
|
-
if (!body.message)
|
|
4227
|
-
return json({ error: "message required" }, 400, port);
|
|
4228
|
-
const { logProgress: logProgress2 } = await Promise.resolve().then(() => (init_comments(), exports_comments));
|
|
4229
|
-
const comment = logProgress2(id, body.message, body.pct_complete, body.agent_id);
|
|
4230
|
-
return json(comment, 201, port);
|
|
4231
|
-
} catch (e) {
|
|
4232
|
-
return json({ error: e instanceof Error ? e.message : "Failed to log progress" }, 500, port);
|
|
4233
|
-
}
|
|
4234
|
-
}
|
|
4235
|
-
}
|
|
4236
|
-
const taskMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
|
|
4237
|
-
if (taskMatch) {
|
|
4238
|
-
const id = taskMatch[1];
|
|
4239
|
-
if (method === "GET") {
|
|
4240
|
-
const task = getTask(id);
|
|
4241
|
-
if (!task)
|
|
4242
|
-
return json({ error: "Task not found" }, 404, port);
|
|
4243
|
-
return json(taskToSummary(task), 200, port);
|
|
4244
|
-
}
|
|
4245
|
-
if (method === "PATCH") {
|
|
4246
|
-
try {
|
|
4247
|
-
const body = await req.json();
|
|
4248
|
-
const task = getTask(id);
|
|
4249
|
-
if (!task)
|
|
4250
|
-
return json({ error: "Task not found" }, 404, port);
|
|
4251
|
-
const updated = updateTask(id, {
|
|
4252
|
-
...body,
|
|
4253
|
-
version: task.version
|
|
4254
|
-
});
|
|
4255
|
-
return json(taskToSummary(updated), 200, port);
|
|
4256
|
-
} catch (e) {
|
|
4257
|
-
return json({ error: e instanceof Error ? e.message : "Failed to update task" }, 500, port);
|
|
4258
|
-
}
|
|
4259
|
-
}
|
|
4260
|
-
if (method === "DELETE") {
|
|
4261
|
-
const deleted = deleteTask(id);
|
|
4262
|
-
if (!deleted)
|
|
4263
|
-
return json({ error: "Task not found" }, 404, port);
|
|
4264
|
-
return json({ success: true }, 200, port);
|
|
4265
|
-
}
|
|
4266
|
-
}
|
|
4267
|
-
const startMatch = path.match(/^\/api\/tasks\/([^/]+)\/start$/);
|
|
4268
|
-
if (startMatch && method === "POST") {
|
|
4269
|
-
const id = startMatch[1];
|
|
4270
|
-
try {
|
|
4271
|
-
const task = startTask(id, "dashboard");
|
|
4272
|
-
broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard", project_id: task.project_id });
|
|
4273
|
-
return json(taskToSummary(task), 200, port);
|
|
4274
|
-
} catch (e) {
|
|
4275
|
-
return json({ error: e instanceof Error ? e.message : "Failed to start task" }, 500, port);
|
|
4276
|
-
}
|
|
4277
|
-
}
|
|
4278
|
-
const failMatch = path.match(/^\/api\/tasks\/([^/]+)\/fail$/);
|
|
4279
|
-
if (failMatch && method === "POST") {
|
|
4280
|
-
const id = failMatch[1];
|
|
4281
|
-
try {
|
|
4282
|
-
const body = await req.json().catch(() => ({}));
|
|
4283
|
-
const { failTask: failTask2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
|
|
4284
|
-
const result = failTask2(id, body.agent_id, body.reason, { retry: body.retry, error_code: body.error_code });
|
|
4285
|
-
broadcastEvent({ type: "task", task_id: id, action: "failed", agent_id: body.agent_id || null, project_id: result.task.project_id });
|
|
4286
|
-
return json({ task: taskToSummary(result.task), retry_task: result.retryTask ? taskToSummary(result.retryTask) : null }, 200, port);
|
|
4287
|
-
} catch (e) {
|
|
4288
|
-
return json({ error: e instanceof Error ? e.message : "Failed to fail task" }, 500, port);
|
|
4289
|
-
}
|
|
4290
|
-
}
|
|
4291
|
-
const completeMatch = path.match(/^\/api\/tasks\/([^/]+)\/complete$/);
|
|
4292
|
-
if (completeMatch && method === "POST") {
|
|
4293
|
-
const id = completeMatch[1];
|
|
4294
|
-
try {
|
|
4295
|
-
const task = completeTask(id, "dashboard");
|
|
4296
|
-
broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard", project_id: task.project_id });
|
|
4297
|
-
return json(taskToSummary(task), 200, port);
|
|
4298
|
-
} catch (e) {
|
|
4299
|
-
return json({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500, port);
|
|
4300
|
-
}
|
|
4301
|
-
}
|
|
4302
|
-
if (path === "/api/projects" && method === "GET") {
|
|
4303
|
-
const pFieldsParam = url.searchParams.get("fields");
|
|
4304
|
-
const pFields = pFieldsParam ? pFieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
4305
|
-
const projects = listProjects();
|
|
4306
|
-
return json(pFields ? projects.map((p) => Object.fromEntries(pFields.map((f) => [f, p[f] ?? null]))) : projects, 200, port);
|
|
4307
|
-
}
|
|
4308
|
-
if (path === "/api/agents/me" && method === "GET") {
|
|
4309
|
-
const name = url.searchParams.get("name");
|
|
4310
|
-
if (!name)
|
|
4311
|
-
return json({ error: "Missing name param" }, 400, port);
|
|
4312
|
-
const { registerAgent: registerAgent2, isAgentConflict: isAgentConflict2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
4313
|
-
const agentResult = registerAgent2({ name });
|
|
4314
|
-
if (isAgentConflict2(agentResult))
|
|
4315
|
-
return json({ error: agentResult.message, conflict: true }, 409, port);
|
|
4316
|
-
const agent = agentResult;
|
|
4317
|
-
const tasks = listTasks({ assigned_to: name });
|
|
4318
|
-
const agentIdTasks = listTasks({ agent_id: agent.id });
|
|
4319
|
-
const allTasks = [...tasks, ...agentIdTasks.filter((t) => !tasks.some((tt) => tt.id === t.id))];
|
|
4320
|
-
const pending = allTasks.filter((t) => t.status === "pending");
|
|
4321
|
-
const inProgress = allTasks.filter((t) => t.status === "in_progress");
|
|
4322
|
-
const completed = allTasks.filter((t) => t.status === "completed");
|
|
4323
|
-
return json({
|
|
4324
|
-
agent,
|
|
4325
|
-
pending_tasks: pending.map((t) => taskToSummary(t)),
|
|
4326
|
-
in_progress_tasks: inProgress.map((t) => taskToSummary(t)),
|
|
4327
|
-
stats: {
|
|
4328
|
-
total: allTasks.length,
|
|
4329
|
-
pending: pending.length,
|
|
4330
|
-
in_progress: inProgress.length,
|
|
4331
|
-
completed: completed.length,
|
|
4332
|
-
completion_rate: allTasks.length > 0 ? Math.round(completed.length / allTasks.length * 100) : 0
|
|
4333
|
-
}
|
|
4334
|
-
}, 200, port);
|
|
4335
|
-
}
|
|
4336
|
-
const queueMatch = path.match(/^\/api\/agents\/([^/]+)\/queue$/);
|
|
4337
|
-
if (queueMatch && method === "GET") {
|
|
4338
|
-
const agentId = decodeURIComponent(queueMatch[1]);
|
|
4339
|
-
const pending = listTasks({ status: "pending" });
|
|
4340
|
-
const queue = pending.filter((t) => t.assigned_to === agentId || t.agent_id === agentId || !t.assigned_to && !t.locked_by);
|
|
4341
|
-
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
4342
|
-
queue.sort((a, b) => (order[a.priority] ?? 4) - (order[b.priority] ?? 4) || new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
4343
|
-
return json(queue.map((t) => taskToSummary(t)), 200, port);
|
|
4344
|
-
}
|
|
4345
|
-
if (path === "/api/tasks/claim" && method === "POST") {
|
|
4346
|
-
try {
|
|
4347
|
-
const body = await req.json();
|
|
4348
|
-
const agentId = body.agent_id || "anonymous";
|
|
4349
|
-
const { claimNextTask: claimNextTask2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
|
|
4350
|
-
const task = claimNextTask2(agentId, body.project_id ? { project_id: body.project_id } : undefined);
|
|
4351
|
-
return json({ task: task ? taskToSummary(task) : null }, 200, port);
|
|
4352
|
-
} catch (e) {
|
|
4353
|
-
return json({ error: e instanceof Error ? e.message : "Failed to claim" }, 500, port);
|
|
4354
|
-
}
|
|
4355
|
-
}
|
|
4356
|
-
if (path === "/api/orgs" && method === "GET") {
|
|
4357
|
-
const { listOrgs: listOrgs2 } = await Promise.resolve().then(() => (init_orgs(), exports_orgs));
|
|
4358
|
-
return json(listOrgs2(), 200, port);
|
|
4359
|
-
}
|
|
4360
|
-
if (path === "/api/orgs" && method === "POST") {
|
|
4361
|
-
try {
|
|
4362
|
-
const body = await req.json();
|
|
4363
|
-
if (!body.name)
|
|
4364
|
-
return json({ error: "Missing name" }, 400, port);
|
|
4365
|
-
const { createOrg: createOrg2 } = await Promise.resolve().then(() => (init_orgs(), exports_orgs));
|
|
4366
|
-
return json(createOrg2(body), 201, port);
|
|
4367
|
-
} catch (e) {
|
|
4368
|
-
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
4369
|
-
}
|
|
4370
|
-
}
|
|
4371
|
-
const orgMatch = path.match(/^\/api\/orgs\/([^/]+)$/);
|
|
4372
|
-
if (orgMatch && method === "PATCH") {
|
|
4373
|
-
try {
|
|
4374
|
-
const body = await req.json();
|
|
4375
|
-
const { updateOrg: updateOrg2 } = await Promise.resolve().then(() => (init_orgs(), exports_orgs));
|
|
4376
|
-
return json(updateOrg2(orgMatch[1], body), 200, port);
|
|
4377
|
-
} catch (e) {
|
|
4378
|
-
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
4379
|
-
}
|
|
4380
|
-
}
|
|
4381
|
-
if (orgMatch && method === "DELETE") {
|
|
4382
|
-
const { deleteOrg: deleteOrg2 } = await Promise.resolve().then(() => (init_orgs(), exports_orgs));
|
|
4383
|
-
const deleted = deleteOrg2(orgMatch[1]);
|
|
4384
|
-
return json(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404, port);
|
|
4385
|
-
}
|
|
4386
|
-
if (path === "/api/org" && method === "GET") {
|
|
4387
|
-
const { getOrgChart: getOrgChart2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
4388
|
-
return json(getOrgChart2(), 200, port);
|
|
4389
|
-
}
|
|
4390
|
-
const teamMatch = path.match(/^\/api\/agents\/([^/]+)\/team$/);
|
|
4391
|
-
if (teamMatch && method === "GET") {
|
|
4392
|
-
const agentId = decodeURIComponent(teamMatch[1]);
|
|
4393
|
-
const { getDirectReports: getDirectReports2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
4394
|
-
return json(getDirectReports2(agentId), 200, port);
|
|
4395
|
-
}
|
|
4396
|
-
if (path === "/api/agents" && method === "GET") {
|
|
4397
|
-
const aFieldsParam = url.searchParams.get("fields");
|
|
4398
|
-
const aFields = aFieldsParam ? aFieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
4399
|
-
const agents = listAgents();
|
|
4400
|
-
return json(aFields ? agents.map((a) => Object.fromEntries(aFields.map((f) => [f, a[f] ?? null]))) : agents, 200, port);
|
|
4401
|
-
}
|
|
4402
|
-
if (path === "/api/projects" && method === "POST") {
|
|
4403
|
-
try {
|
|
4404
|
-
const body = await req.json();
|
|
4405
|
-
if (!body.name || !body.path)
|
|
4406
|
-
return json({ error: "Missing name or path" }, 400, port);
|
|
4407
|
-
const { createProject: createProject2 } = await Promise.resolve().then(() => (init_projects(), exports_projects));
|
|
4408
|
-
const project = createProject2({ name: body.name, path: body.path, description: body.description });
|
|
4409
|
-
return json(project, 201, port);
|
|
4410
|
-
} catch (e) {
|
|
4411
|
-
return json({ error: e instanceof Error ? e.message : "Failed to create project" }, 500, port);
|
|
4412
|
-
}
|
|
4413
|
-
}
|
|
4414
|
-
const projectDeleteMatch = path.match(/^\/api\/projects\/([^/]+)$/);
|
|
4415
|
-
if (projectDeleteMatch && method === "DELETE") {
|
|
4416
|
-
const id = projectDeleteMatch[1];
|
|
4417
|
-
const { deleteProject: deleteProject2 } = await Promise.resolve().then(() => (init_projects(), exports_projects));
|
|
4418
|
-
const deleted = deleteProject2(id);
|
|
4419
|
-
if (!deleted)
|
|
4420
|
-
return json({ error: "Project not found" }, 404, port);
|
|
4421
|
-
return json({ success: true }, 200, port);
|
|
4422
|
-
}
|
|
4423
|
-
if (path === "/api/agents" && method === "POST") {
|
|
4424
|
-
try {
|
|
4425
|
-
const body = await req.json();
|
|
4426
|
-
if (!body.name)
|
|
4427
|
-
return json({ error: "Missing name" }, 400, port);
|
|
4428
|
-
const { registerAgent: registerAgent2, isAgentConflict: isAgentConflict2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
4429
|
-
const result = registerAgent2({ name: body.name, description: body.description, session_id: body.session_id, working_dir: body.working_dir });
|
|
4430
|
-
if (isAgentConflict2(result))
|
|
4431
|
-
return json({ error: result.message, conflict: true }, 409, port);
|
|
4432
|
-
return json(result, 201, port);
|
|
4433
|
-
} catch (e) {
|
|
4434
|
-
return json({ error: e instanceof Error ? e.message : "Failed to register agent" }, 500, port);
|
|
4435
|
-
}
|
|
4436
|
-
}
|
|
4437
|
-
const agentMatch = path.match(/^\/api\/agents\/([^/]+)$/);
|
|
4438
|
-
if (agentMatch && method === "PATCH") {
|
|
4439
|
-
const id = agentMatch[1];
|
|
4440
|
-
try {
|
|
4441
|
-
const body = await req.json();
|
|
4442
|
-
const { updateAgent: updateAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
4443
|
-
const agent = updateAgent2(id, body);
|
|
4444
|
-
return json(agent, 200, port);
|
|
4445
|
-
} catch (e) {
|
|
4446
|
-
return json({ error: e instanceof Error ? e.message : "Failed to update agent" }, 500, port);
|
|
4447
|
-
}
|
|
4448
|
-
}
|
|
4449
|
-
if (agentMatch && method === "DELETE") {
|
|
4450
|
-
const id = agentMatch[1];
|
|
4451
|
-
const { deleteAgent: deleteAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
4452
|
-
const deleted = deleteAgent2(id);
|
|
4453
|
-
if (!deleted)
|
|
4454
|
-
return json({ error: "Agent not found" }, 404, port);
|
|
4455
|
-
return json({ success: true }, 200, port);
|
|
4456
|
-
}
|
|
4457
|
-
if (path === "/api/agents/bulk" && method === "POST") {
|
|
4458
|
-
try {
|
|
4459
|
-
const body = await req.json();
|
|
4460
|
-
if (!body.ids?.length || body.action !== "delete")
|
|
4461
|
-
return json({ error: "Missing ids or invalid action" }, 400, port);
|
|
4462
|
-
const { deleteAgent: deleteAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
4463
|
-
let succeeded = 0;
|
|
4464
|
-
for (const id of body.ids) {
|
|
4465
|
-
if (deleteAgent2(id))
|
|
4466
|
-
succeeded++;
|
|
4467
|
-
}
|
|
4468
|
-
return json({ succeeded, failed: body.ids.length - succeeded }, 200, port);
|
|
4469
|
-
} catch (e) {
|
|
4470
|
-
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
4471
|
-
}
|
|
4472
|
-
}
|
|
4473
|
-
if (path === "/api/projects/bulk" && method === "POST") {
|
|
4474
|
-
try {
|
|
4475
|
-
const body = await req.json();
|
|
4476
|
-
if (!body.ids?.length || body.action !== "delete")
|
|
4477
|
-
return json({ error: "Missing ids or invalid action" }, 400, port);
|
|
4478
|
-
const { deleteProject: deleteProject2 } = await Promise.resolve().then(() => (init_projects(), exports_projects));
|
|
4479
|
-
let succeeded = 0;
|
|
4480
|
-
for (const id of body.ids) {
|
|
4481
|
-
if (deleteProject2(id))
|
|
4482
|
-
succeeded++;
|
|
4483
|
-
}
|
|
4484
|
-
return json({ succeeded, failed: body.ids.length - succeeded }, 200, port);
|
|
4485
|
-
} catch (e) {
|
|
4486
|
-
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
4487
|
-
}
|
|
4488
|
-
}
|
|
4489
|
-
if (path === "/api/doctor" && method === "GET") {
|
|
4490
|
-
const issues = [];
|
|
4491
|
-
const { getStaleTasks: getStaleDiag } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
|
|
4492
|
-
const staleItems = getStaleDiag(30);
|
|
4493
|
-
if (staleItems.length > 0)
|
|
4494
|
-
issues.push({ severity: "warn", type: "stale_tasks", message: `${staleItems.length} tasks stuck in_progress >30min`, count: staleItems.length });
|
|
4495
|
-
const withParent = getDatabase().query("SELECT COUNT(*) as c FROM tasks t WHERE t.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = t.parent_id)").get();
|
|
4496
|
-
if (withParent.c > 0)
|
|
4497
|
-
issues.push({ severity: "error", type: "orphaned_parents", message: `${withParent.c} tasks reference non-existent parent IDs`, count: withParent.c });
|
|
4498
|
-
if (issues.length === 0)
|
|
4499
|
-
issues.push({ severity: "info", type: "healthy", message: "No issues found" });
|
|
4500
|
-
return json({ ok: !issues.some((i) => i.severity === "error"), issues }, 200, port);
|
|
4501
|
-
}
|
|
4502
|
-
if (path === "/api/report" && method === "GET") {
|
|
4503
|
-
const days = parseInt(url.searchParams.get("days") || "7", 10);
|
|
4504
|
-
const projectId = url.searchParams.get("project_id") || undefined;
|
|
4505
|
-
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
4506
|
-
const { getTasksChangedSince: getTasksChangedSince2, getTaskStats: getTaskStats2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
|
|
4507
|
-
const filters = projectId ? { project_id: projectId } : undefined;
|
|
4508
|
-
const changed = getTasksChangedSince2(since, filters);
|
|
4509
|
-
const all = listTasks(filters || {});
|
|
4510
|
-
const stats = getTaskStats2(filters);
|
|
4511
|
-
const completed = changed.filter((t) => t.status === "completed");
|
|
4512
|
-
const failed = changed.filter((t) => t.status === "failed");
|
|
4513
|
-
const byDay = {};
|
|
4514
|
-
for (const t of changed) {
|
|
4515
|
-
const day = t.updated_at.slice(0, 10);
|
|
4516
|
-
byDay[day] = (byDay[day] || 0) + 1;
|
|
4517
|
-
}
|
|
4518
|
-
const completionRate = changed.length > 0 ? Math.round(completed.length / changed.length * 100) : 0;
|
|
4519
|
-
return json({ days, period_since: since, total: all.length, stats, changed: changed.length, completed: completed.length, failed: failed.length, completion_rate: completionRate, by_day: byDay }, 200, port);
|
|
4520
|
-
}
|
|
4521
|
-
if (path === "/api/activity" && method === "GET") {
|
|
4522
|
-
const limit = parseInt(url.searchParams.get("limit") || "50", 10);
|
|
4523
|
-
const { getRecentActivity: getRecentActivity2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
4524
|
-
return json(getRecentActivity2(limit), 200, port);
|
|
4525
|
-
}
|
|
4526
|
-
const historyMatch = path.match(/^\/api\/tasks\/([^/]+)\/history$/);
|
|
4527
|
-
if (historyMatch && method === "GET") {
|
|
4528
|
-
const id = historyMatch[1];
|
|
4529
|
-
const { getTaskHistory: getTaskHistory2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
4530
|
-
return json(getTaskHistory2(id), 200, port);
|
|
4531
|
-
}
|
|
4532
|
-
if (path === "/api/webhooks" && method === "GET") {
|
|
4533
|
-
const { listWebhooks: listWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
4534
|
-
return json(listWebhooks2(), 200, port);
|
|
4535
|
-
}
|
|
4536
|
-
if (path === "/api/webhooks" && method === "POST") {
|
|
4537
|
-
try {
|
|
4538
|
-
const body = await req.json();
|
|
4539
|
-
if (!body.url)
|
|
4540
|
-
return json({ error: "Missing url" }, 400, port);
|
|
4541
|
-
const { createWebhook: createWebhook2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
4542
|
-
return json(createWebhook2(body), 201, port);
|
|
4543
|
-
} catch (e) {
|
|
4544
|
-
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
4545
|
-
}
|
|
4546
|
-
}
|
|
4547
|
-
const webhookMatch = path.match(/^\/api\/webhooks\/([^/]+)$/);
|
|
4548
|
-
if (webhookMatch && method === "DELETE") {
|
|
4549
|
-
const { deleteWebhook: deleteWebhook2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
4550
|
-
const deleted = deleteWebhook2(webhookMatch[1]);
|
|
4551
|
-
return json(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404, port);
|
|
4552
|
-
}
|
|
4553
|
-
if (path === "/api/templates" && method === "GET") {
|
|
4554
|
-
const { listTemplates: listTemplates2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
4555
|
-
return json(listTemplates2(), 200, port);
|
|
4556
|
-
}
|
|
4557
|
-
if (path === "/api/templates" && method === "POST") {
|
|
4558
|
-
try {
|
|
4559
|
-
const body = await req.json();
|
|
4560
|
-
if (!body.name || !body.title_pattern)
|
|
4561
|
-
return json({ error: "Missing name or title_pattern" }, 400, port);
|
|
4562
|
-
const { createTemplate: createTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
4563
|
-
return json(createTemplate2(body), 201, port);
|
|
4564
|
-
} catch (e) {
|
|
4565
|
-
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
4566
|
-
}
|
|
4567
|
-
}
|
|
4568
|
-
const templateMatch = path.match(/^\/api\/templates\/([^/]+)$/);
|
|
4569
|
-
if (templateMatch && method === "DELETE") {
|
|
4570
|
-
const { deleteTemplate: deleteTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
4571
|
-
const deleted = deleteTemplate2(templateMatch[1]);
|
|
4572
|
-
return json(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404, port);
|
|
4573
|
-
}
|
|
4574
|
-
if (path === "/api/plans" && method === "GET") {
|
|
4575
|
-
const projectId = url.searchParams.get("project_id") || undefined;
|
|
4576
|
-
const plans = listPlans(projectId);
|
|
4577
|
-
return json(plans, 200, port);
|
|
4578
|
-
}
|
|
4579
|
-
if (path === "/api/plans" && method === "POST") {
|
|
4580
|
-
try {
|
|
4581
|
-
const body = await req.json();
|
|
4582
|
-
if (!body.name)
|
|
4583
|
-
return json({ error: "Missing 'name'" }, 400, port);
|
|
4584
|
-
const plan = createPlan({
|
|
4585
|
-
name: body.name,
|
|
4586
|
-
description: body.description,
|
|
4587
|
-
project_id: body.project_id,
|
|
4588
|
-
task_list_id: body.task_list_id,
|
|
4589
|
-
agent_id: body.agent_id,
|
|
4590
|
-
status: body.status
|
|
4591
|
-
});
|
|
4592
|
-
return json(plan, 201, port);
|
|
4593
|
-
} catch (e) {
|
|
4594
|
-
return json({ error: e instanceof Error ? e.message : "Failed to create plan" }, 500, port);
|
|
4595
|
-
}
|
|
4596
|
-
}
|
|
4597
|
-
if (path === "/api/plans/bulk" && method === "POST") {
|
|
4598
|
-
try {
|
|
4599
|
-
const body = await req.json();
|
|
4600
|
-
if (!body.ids?.length || body.action !== "delete")
|
|
4601
|
-
return json({ error: "Missing ids or invalid action" }, 400, port);
|
|
4602
|
-
let succeeded = 0;
|
|
4603
|
-
for (const id of body.ids) {
|
|
4604
|
-
if (deletePlan(id))
|
|
4605
|
-
succeeded++;
|
|
4606
|
-
}
|
|
4607
|
-
return json({ succeeded, failed: body.ids.length - succeeded }, 200, port);
|
|
4608
|
-
} catch (e) {
|
|
4609
|
-
return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
|
|
4610
|
-
}
|
|
4611
|
-
}
|
|
4612
|
-
const planMatch = path.match(/^\/api\/plans\/([^/]+)$/);
|
|
4613
|
-
if (planMatch) {
|
|
4614
|
-
const id = planMatch[1];
|
|
4615
|
-
if (method === "GET") {
|
|
4616
|
-
const plan = getPlan(id);
|
|
4617
|
-
if (!plan)
|
|
4618
|
-
return json({ error: "Plan not found" }, 404, port);
|
|
4619
|
-
const tasks = listTasks({ plan_id: id });
|
|
4620
|
-
return json({ ...plan, tasks: tasks.map((t) => taskToSummary(t)) }, 200, port);
|
|
4621
|
-
}
|
|
4622
|
-
if (method === "PATCH") {
|
|
4623
|
-
try {
|
|
4624
|
-
const body = await req.json();
|
|
4625
|
-
const plan = updatePlan(id, body);
|
|
4626
|
-
return json(plan, 200, port);
|
|
4627
|
-
} catch (e) {
|
|
4628
|
-
return json({ error: e instanceof Error ? e.message : "Failed to update plan" }, 500, port);
|
|
4629
|
-
}
|
|
4630
|
-
}
|
|
4631
|
-
if (method === "DELETE") {
|
|
4632
|
-
const deleted = deletePlan(id);
|
|
4633
|
-
if (!deleted)
|
|
4634
|
-
return json({ error: "Plan not found" }, 404, port);
|
|
4635
|
-
return json({ success: true }, 200, port);
|
|
4636
|
-
}
|
|
4637
|
-
}
|
|
4638
|
-
if (dashboardExists && (method === "GET" || method === "HEAD")) {
|
|
4639
|
-
if (path !== "/") {
|
|
4640
|
-
const filePath = join3(dashboardDir, path);
|
|
4641
|
-
const resolvedFile = resolve2(filePath);
|
|
4642
|
-
const resolvedBase = resolve2(dashboardDir);
|
|
4643
|
-
if (!resolvedFile.startsWith(resolvedBase + sep) && resolvedFile !== resolvedBase) {
|
|
4644
|
-
return json({ error: "Forbidden" }, 403, port);
|
|
4645
|
-
}
|
|
4646
|
-
const res2 = serveStaticFile(filePath);
|
|
4647
|
-
if (res2)
|
|
4648
|
-
return res2;
|
|
4649
|
-
}
|
|
4650
|
-
const indexPath = join3(dashboardDir, "index.html");
|
|
4651
|
-
const res = serveStaticFile(indexPath);
|
|
4652
|
-
if (res)
|
|
4653
|
-
return res;
|
|
4654
|
-
}
|
|
4655
|
-
return json({ error: "Not found" }, 404, port);
|
|
4656
|
-
}
|
|
4657
|
-
});
|
|
4658
|
-
const shutdown = () => {
|
|
4659
|
-
server.stop();
|
|
4660
|
-
process.exit(0);
|
|
4661
|
-
};
|
|
4662
|
-
process.on("SIGINT", shutdown);
|
|
4663
|
-
process.on("SIGTERM", shutdown);
|
|
4664
|
-
const serverUrl = `http://localhost:${port}`;
|
|
4665
|
-
console.log(`Todos Dashboard running at ${serverUrl}`);
|
|
4666
|
-
if (shouldOpen) {
|
|
4667
|
-
try {
|
|
4668
|
-
const { exec } = await import("child_process");
|
|
4669
|
-
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
4670
|
-
exec(`${openCmd} ${serverUrl}`);
|
|
4671
|
-
} catch {}
|
|
4672
|
-
}
|
|
4673
|
-
}
|
|
4674
|
-
|
|
4675
|
-
// src/server/index.ts
|
|
4676
|
-
var DEFAULT_PORT = 19427;
|
|
4677
|
-
function parsePort() {
|
|
4678
|
-
const portArg = process.argv.find((a) => a === "--port" || a.startsWith("--port="));
|
|
4679
|
-
if (portArg) {
|
|
4680
|
-
if (portArg.includes("=")) {
|
|
4681
|
-
return parseInt(portArg.split("=")[1], 10) || DEFAULT_PORT;
|
|
4682
|
-
}
|
|
4683
|
-
const idx = process.argv.indexOf(portArg);
|
|
4684
|
-
return parseInt(process.argv[idx + 1], 10) || DEFAULT_PORT;
|
|
4685
|
-
}
|
|
4686
|
-
return DEFAULT_PORT;
|
|
4687
|
-
}
|
|
4688
|
-
async function findFreePort(start) {
|
|
4689
|
-
for (let port = start;port < start + 100; port++) {
|
|
4690
|
-
try {
|
|
4691
|
-
const server = Bun.serve({ port, fetch: () => new Response("") });
|
|
4692
|
-
server.stop(true);
|
|
4693
|
-
return port;
|
|
4694
|
-
} catch {}
|
|
4695
|
-
}
|
|
4696
|
-
return start;
|
|
4697
|
-
}
|
|
4698
|
-
async function main() {
|
|
4699
|
-
const requestedPort = parsePort();
|
|
4700
|
-
const port = await findFreePort(requestedPort);
|
|
4701
|
-
if (port !== requestedPort) {
|
|
4702
|
-
console.log(`Port ${requestedPort} in use, using ${port}`);
|
|
4703
|
-
}
|
|
4704
|
-
const noOpen = process.argv.includes("--no-open") || process.env["TODOS_NO_OPEN"] === "true";
|
|
4705
|
-
startServer(port, { open: !noOpen });
|
|
4706
|
-
}
|
|
4707
|
-
main();
|