@hasna/todos 0.11.29 → 0.11.30

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.
Files changed (67) hide show
  1. package/dist/cli/index.js +25237 -27283
  2. package/dist/db/checkpoints.d.ts +55 -0
  3. package/dist/db/checkpoints.d.ts.map +1 -0
  4. package/dist/db/database.d.ts.map +1 -1
  5. package/dist/db/migrations.d.ts +2 -0
  6. package/dist/db/migrations.d.ts.map +1 -0
  7. package/dist/db/pg-migrations.d.ts.map +1 -1
  8. package/dist/db/schema.d.ts +0 -1
  9. package/dist/db/schema.d.ts.map +1 -1
  10. package/dist/db/task-crud.d.ts +13 -0
  11. package/dist/db/task-crud.d.ts.map +1 -0
  12. package/dist/db/task-graph.d.ts +27 -0
  13. package/dist/db/task-graph.d.ts.map +1 -0
  14. package/dist/db/task-lifecycle.d.ts +80 -0
  15. package/dist/db/task-lifecycle.d.ts.map +1 -0
  16. package/dist/db/task-relations.d.ts +86 -0
  17. package/dist/db/task-relations.d.ts.map +1 -0
  18. package/dist/db/task-status.d.ts +65 -0
  19. package/dist/db/task-status.d.ts.map +1 -0
  20. package/dist/db/tasks.d.ts +10 -255
  21. package/dist/db/tasks.d.ts.map +1 -1
  22. package/dist/db/webhooks.d.ts +6 -1
  23. package/dist/db/webhooks.d.ts.map +1 -1
  24. package/dist/index.js +2381 -1788
  25. package/dist/lib/github.d.ts +1 -1
  26. package/dist/lib/github.d.ts.map +1 -1
  27. package/dist/lib/north-star.d.ts +33 -0
  28. package/dist/lib/north-star.d.ts.map +1 -0
  29. package/dist/lib/task-runner.d.ts +101 -0
  30. package/dist/lib/task-runner.d.ts.map +1 -0
  31. package/dist/mcp/index.d.ts.map +1 -1
  32. package/dist/mcp/index.js +21955 -25937
  33. package/dist/mcp/tools/cloud.d.ts.map +1 -1
  34. package/dist/mcp/tools/code-tools.d.ts +15 -0
  35. package/dist/mcp/tools/code-tools.d.ts.map +1 -0
  36. package/dist/mcp/tools/task-adv-tools.d.ts +20 -0
  37. package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -0
  38. package/dist/mcp/tools/task-auto-tools.d.ts +20 -0
  39. package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -0
  40. package/dist/mcp/tools/task-crud.d.ts +20 -0
  41. package/dist/mcp/tools/task-crud.d.ts.map +1 -0
  42. package/dist/mcp/tools/task-meta-tools.d.ts +12 -0
  43. package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -0
  44. package/dist/mcp/tools/task-project-tools.d.ts +20 -0
  45. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -0
  46. package/dist/mcp/tools/task-rel-tools.d.ts +19 -0
  47. package/dist/mcp/tools/task-rel-tools.d.ts.map +1 -0
  48. package/dist/mcp/tools/task-resources.d.ts +13 -0
  49. package/dist/mcp/tools/task-resources.d.ts.map +1 -0
  50. package/dist/mcp/tools/task-workflow-tools.d.ts +20 -0
  51. package/dist/mcp/tools/task-workflow-tools.d.ts.map +1 -0
  52. package/dist/sdk/client.d.ts +361 -0
  53. package/dist/sdk/client.d.ts.map +1 -0
  54. package/dist/sdk/index.d.ts +14 -0
  55. package/dist/sdk/index.d.ts.map +1 -0
  56. package/dist/sdk/types.d.ts +167 -0
  57. package/dist/sdk/types.d.ts.map +1 -0
  58. package/dist/sdk.d.ts +15 -182
  59. package/dist/sdk.d.ts.map +1 -1
  60. package/dist/server/index.js +1961 -2803
  61. package/dist/server/routes.d.ts +85 -0
  62. package/dist/server/routes.d.ts.map +1 -0
  63. package/dist/server/serve.d.ts +29 -0
  64. package/dist/server/serve.d.ts.map +1 -1
  65. package/dist/types/index.d.ts +77 -0
  66. package/dist/types/index.d.ts.map +1 -1
  67. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,419 +1,46 @@
1
1
  // @bun
2
2
  var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ function __accessProp(key) {
7
+ return this[key];
8
+ }
9
+ var __toCommonJS = (from) => {
10
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
11
+ if (entry)
12
+ return entry;
13
+ entry = __defProp({}, "__esModule", { value: true });
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (var key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(entry, key))
17
+ __defProp(entry, key, {
18
+ get: __accessProp.bind(from, key),
19
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
+ });
21
+ }
22
+ __moduleCache.set(from, entry);
23
+ return entry;
24
+ };
25
+ var __moduleCache;
26
+ var __returnValue = (v) => v;
27
+ function __exportSetter(name, newValue) {
28
+ this[name] = __returnValue.bind(null, newValue);
29
+ }
3
30
  var __export = (target, all) => {
4
31
  for (var name in all)
5
32
  __defProp(target, name, {
6
33
  get: all[name],
7
34
  enumerable: true,
8
35
  configurable: true,
9
- set: (newValue) => all[name] = () => newValue
36
+ set: __exportSetter.bind(all, name)
10
37
  });
11
38
  };
12
39
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
13
40
 
14
- // src/db/schema.ts
15
- function runMigrations(db) {
16
- try {
17
- const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
18
- const currentLevel = result?.max_id ?? 0;
19
- for (let i = currentLevel;i < MIGRATIONS.length; i++) {
20
- try {
21
- db.exec(MIGRATIONS[i]);
22
- } catch {}
23
- }
24
- } catch {
25
- for (const migration of MIGRATIONS) {
26
- try {
27
- db.exec(migration);
28
- } catch {}
29
- }
30
- }
31
- ensureSchema(db);
32
- }
33
- function ensureSchema(db) {
34
- const ensureColumn = (table, column, type) => {
35
- try {
36
- db.query(`SELECT ${column} FROM ${table} LIMIT 0`).get();
37
- } catch {
38
- try {
39
- db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
40
- } catch {}
41
- }
42
- };
43
- const ensureTable = (name, sql) => {
44
- try {
45
- const exists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(name);
46
- if (!exists)
47
- db.exec(sql);
48
- } catch {}
49
- };
50
- const ensureIndex = (sql) => {
51
- try {
52
- db.exec(sql);
53
- } catch {}
54
- };
55
- ensureTable("orgs", `
56
- CREATE TABLE orgs (
57
- id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
58
- metadata TEXT DEFAULT '{}',
59
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
60
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
61
- )`);
62
- ensureTable("agents", `
63
- CREATE TABLE agents (
64
- id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
65
- role TEXT DEFAULT 'agent', permissions TEXT DEFAULT '["*"]',
66
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived')),
67
- metadata TEXT DEFAULT '{}',
68
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
69
- last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
70
- )`);
71
- ensureTable("task_lists", `
72
- CREATE TABLE task_lists (
73
- id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
74
- slug TEXT NOT NULL, name TEXT NOT NULL, description TEXT,
75
- metadata TEXT DEFAULT '{}',
76
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
77
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
78
- UNIQUE(project_id, slug)
79
- )`);
80
- ensureTable("plans", `
81
- CREATE TABLE plans (
82
- id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
83
- task_list_id TEXT, agent_id TEXT,
84
- name TEXT NOT NULL, description TEXT,
85
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
86
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
87
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
88
- )`);
89
- ensureTable("task_tags", `
90
- CREATE TABLE task_tags (
91
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
92
- tag TEXT NOT NULL, PRIMARY KEY (task_id, tag)
93
- )`);
94
- ensureTable("task_history", `
95
- CREATE TABLE task_history (
96
- id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
97
- action TEXT NOT NULL, field TEXT, old_value TEXT, new_value TEXT, agent_id TEXT,
98
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
99
- )`);
100
- ensureTable("webhooks", `
101
- CREATE TABLE webhooks (
102
- id TEXT PRIMARY KEY, url TEXT NOT NULL, events TEXT NOT NULL DEFAULT '[]',
103
- secret TEXT, active INTEGER NOT NULL DEFAULT 1,
104
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
105
- )`);
106
- ensureTable("task_templates", `
107
- CREATE TABLE task_templates (
108
- id TEXT PRIMARY KEY, name TEXT NOT NULL, title_pattern TEXT NOT NULL,
109
- description TEXT, priority TEXT DEFAULT 'medium', tags TEXT DEFAULT '[]',
110
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
111
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
112
- metadata TEXT DEFAULT '{}',
113
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
114
- )`);
115
- ensureTable("template_tasks", `
116
- CREATE TABLE template_tasks (
117
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
118
- template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
119
- position INTEGER NOT NULL,
120
- title_pattern TEXT NOT NULL,
121
- description TEXT,
122
- priority TEXT DEFAULT 'medium',
123
- tags TEXT DEFAULT '[]',
124
- task_type TEXT,
125
- depends_on_positions TEXT DEFAULT '[]',
126
- metadata TEXT DEFAULT '{}',
127
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
128
- )`);
129
- ensureTable("task_checklists", `
130
- CREATE TABLE task_checklists (
131
- id TEXT PRIMARY KEY,
132
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
133
- position INTEGER NOT NULL DEFAULT 0,
134
- text TEXT NOT NULL,
135
- checked INTEGER NOT NULL DEFAULT 0,
136
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
137
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
138
- )`);
139
- ensureTable("project_sources", `
140
- CREATE TABLE project_sources (
141
- id TEXT PRIMARY KEY,
142
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
143
- type TEXT NOT NULL,
144
- name TEXT NOT NULL,
145
- uri TEXT NOT NULL,
146
- description TEXT,
147
- metadata TEXT DEFAULT '{}',
148
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
149
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
150
- )`);
151
- ensureTable("task_relationships", `
152
- CREATE TABLE task_relationships (
153
- id TEXT PRIMARY KEY,
154
- source_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
155
- target_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
156
- relationship_type TEXT NOT NULL,
157
- metadata TEXT DEFAULT '{}',
158
- created_by TEXT,
159
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
160
- CHECK (source_task_id != target_task_id)
161
- )`);
162
- ensureTable("kg_edges", `
163
- CREATE TABLE kg_edges (
164
- id TEXT PRIMARY KEY,
165
- source_id TEXT NOT NULL,
166
- source_type TEXT NOT NULL,
167
- target_id TEXT NOT NULL,
168
- target_type TEXT NOT NULL,
169
- relation_type TEXT NOT NULL,
170
- weight REAL NOT NULL DEFAULT 1.0,
171
- metadata TEXT DEFAULT '{}',
172
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
173
- UNIQUE(source_id, source_type, target_id, target_type, relation_type)
174
- )`);
175
- ensureTable("project_machine_paths", `
176
- CREATE TABLE project_machine_paths (
177
- id TEXT PRIMARY KEY,
178
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
179
- machine_id TEXT NOT NULL,
180
- path TEXT NOT NULL,
181
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
182
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
183
- UNIQUE(project_id, machine_id)
184
- )`);
185
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
186
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
187
- ensureTable("machines", `
188
- CREATE TABLE machines (
189
- id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, hostname TEXT, platform TEXT,
190
- last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
191
- metadata TEXT DEFAULT '{}',
192
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
193
- )`);
194
- ensureColumn("projects", "task_list_id", "TEXT");
195
- ensureColumn("projects", "task_prefix", "TEXT");
196
- ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
197
- ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
198
- ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
199
- ensureColumn("tasks", "short_id", "TEXT");
200
- ensureColumn("tasks", "due_at", "TEXT");
201
- ensureColumn("tasks", "estimated_minutes", "INTEGER");
202
- ensureColumn("tasks", "requires_approval", "INTEGER NOT NULL DEFAULT 0");
203
- ensureColumn("tasks", "approved_by", "TEXT");
204
- ensureColumn("tasks", "approved_at", "TEXT");
205
- ensureColumn("tasks", "recurrence_rule", "TEXT");
206
- ensureColumn("tasks", "recurrence_parent_id", "TEXT REFERENCES tasks(id) ON DELETE SET NULL");
207
- ensureColumn("tasks", "confidence", "REAL");
208
- ensureColumn("tasks", "reason", "TEXT");
209
- ensureColumn("tasks", "spawned_from_session", "TEXT");
210
- ensureColumn("tasks", "assigned_by", "TEXT");
211
- ensureColumn("tasks", "assigned_from_project", "TEXT");
212
- ensureColumn("tasks", "started_at", "TEXT");
213
- ensureColumn("tasks", "task_type", "TEXT");
214
- ensureColumn("tasks", "cost_tokens", "INTEGER DEFAULT 0");
215
- ensureColumn("tasks", "cost_usd", "REAL DEFAULT 0");
216
- ensureColumn("tasks", "delegated_from", "TEXT");
217
- ensureColumn("tasks", "delegation_depth", "INTEGER DEFAULT 0");
218
- ensureColumn("tasks", "retry_count", "INTEGER DEFAULT 0");
219
- ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
220
- ensureColumn("tasks", "retry_after", "TEXT");
221
- ensureColumn("tasks", "sla_minutes", "INTEGER");
222
- ensureColumn("tasks", "archived_at", "TEXT");
223
- ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
224
- ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
225
- ensureColumn("agents", "reports_to", "TEXT");
226
- ensureColumn("agents", "title", "TEXT");
227
- ensureColumn("agents", "level", "TEXT");
228
- ensureColumn("agents", "org_id", "TEXT");
229
- ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
230
- ensureColumn("projects", "org_id", "TEXT");
231
- ensureColumn("plans", "task_list_id", "TEXT");
232
- ensureColumn("plans", "agent_id", "TEXT");
233
- ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
234
- ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
235
- ensureColumn("template_tasks", "condition", "TEXT");
236
- ensureColumn("template_tasks", "include_template_id", "TEXT");
237
- ensureTable("template_versions", `
238
- CREATE TABLE template_versions (
239
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
240
- template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
241
- version INTEGER NOT NULL,
242
- snapshot TEXT NOT NULL,
243
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
244
- )`);
245
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id)");
246
- ensureTable("dispatches", `
247
- CREATE TABLE dispatches (
248
- id TEXT PRIMARY KEY,
249
- title TEXT,
250
- target_window TEXT NOT NULL,
251
- task_ids TEXT NOT NULL DEFAULT '[]',
252
- task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL,
253
- message TEXT,
254
- delay_ms INTEGER,
255
- scheduled_at TEXT,
256
- status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'sent', 'failed', 'cancelled')),
257
- error TEXT,
258
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
259
- sent_at TEXT
260
- )`);
261
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_status ON dispatches(status)");
262
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_scheduled ON dispatches(scheduled_at)");
263
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_task_list ON dispatches(task_list_id)");
264
- ensureTable("dispatch_logs", `
265
- CREATE TABLE dispatch_logs (
266
- id TEXT PRIMARY KEY,
267
- dispatch_id TEXT NOT NULL REFERENCES dispatches(id) ON DELETE CASCADE,
268
- target_window TEXT NOT NULL,
269
- message TEXT NOT NULL,
270
- delay_ms INTEGER NOT NULL,
271
- status TEXT NOT NULL CHECK(status IN ('sent', 'failed')),
272
- error TEXT,
273
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
274
- )`);
275
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatch_logs_dispatch ON dispatch_logs(dispatch_id)");
276
- ensureColumn("webhooks", "project_id", "TEXT");
277
- ensureColumn("webhooks", "task_list_id", "TEXT");
278
- ensureColumn("webhooks", "agent_id", "TEXT");
279
- ensureColumn("webhooks", "task_id", "TEXT");
280
- ensureTable("webhook_deliveries", `
281
- CREATE TABLE webhook_deliveries (
282
- id TEXT PRIMARY KEY,
283
- webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
284
- event TEXT NOT NULL,
285
- payload TEXT NOT NULL,
286
- status_code INTEGER,
287
- response TEXT,
288
- attempt INTEGER NOT NULL DEFAULT 1,
289
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
290
- )`);
291
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id)");
292
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
293
- ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
294
- ensureColumn("task_comments", "progress_pct", "INTEGER");
295
- ensureColumn("projects", "machine_id", "TEXT");
296
- ensureColumn("projects", "synced_at", "TEXT");
297
- ensureColumn("tasks", "machine_id", "TEXT");
298
- ensureColumn("tasks", "synced_at", "TEXT");
299
- ensureColumn("agents", "machine_id", "TEXT");
300
- ensureColumn("agents", "synced_at", "TEXT");
301
- ensureColumn("task_lists", "machine_id", "TEXT");
302
- ensureColumn("task_lists", "synced_at", "TEXT");
303
- ensureColumn("plans", "machine_id", "TEXT");
304
- ensureColumn("plans", "synced_at", "TEXT");
305
- ensureColumn("task_comments", "machine_id", "TEXT");
306
- ensureColumn("task_comments", "synced_at", "TEXT");
307
- ensureColumn("sessions", "machine_id", "TEXT");
308
- ensureColumn("sessions", "synced_at", "TEXT");
309
- ensureColumn("task_history", "machine_id", "TEXT");
310
- ensureColumn("webhooks", "machine_id", "TEXT");
311
- ensureColumn("webhooks", "synced_at", "TEXT");
312
- ensureColumn("task_templates", "machine_id", "TEXT");
313
- ensureColumn("task_templates", "synced_at", "TEXT");
314
- ensureColumn("orgs", "machine_id", "TEXT");
315
- ensureColumn("orgs", "synced_at", "TEXT");
316
- ensureColumn("handoffs", "machine_id", "TEXT");
317
- ensureColumn("handoffs", "synced_at", "TEXT");
318
- ensureColumn("task_checklists", "machine_id", "TEXT");
319
- ensureColumn("project_sources", "machine_id", "TEXT");
320
- ensureColumn("project_sources", "synced_at", "TEXT");
321
- ensureColumn("task_files", "machine_id", "TEXT");
322
- ensureColumn("task_relationships", "machine_id", "TEXT");
323
- ensureColumn("kg_edges", "machine_id", "TEXT");
324
- ensureColumn("project_agent_roles", "machine_id", "TEXT");
325
- ensureColumn("dispatches", "machine_id", "TEXT");
326
- ensureColumn("dispatches", "synced_at", "TEXT");
327
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
328
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id)");
329
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at)");
330
- ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL");
331
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name)");
332
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_project ON task_lists(project_id)");
333
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug)");
334
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag)");
335
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id)");
336
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
337
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
338
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
339
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
340
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
341
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(agent_id)");
342
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id)");
343
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL");
344
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_checklists_task ON task_checklists(task_id)");
345
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id)");
346
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type)");
347
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by)");
348
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id)");
349
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id)");
350
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type)");
351
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id)");
352
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type)");
353
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type)");
354
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type)");
355
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_machine ON tasks(machine_id)");
356
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_synced ON tasks(synced_at)");
357
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_machine ON projects(machine_id)");
358
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_machine ON agents(machine_id)");
359
- ensureTable("task_time_logs", `
360
- CREATE TABLE task_time_logs (
361
- id TEXT PRIMARY KEY,
362
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
363
- agent_id TEXT,
364
- started_at TEXT,
365
- ended_at TEXT,
366
- minutes INTEGER NOT NULL,
367
- notes TEXT,
368
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
369
- )`);
370
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_task ON task_time_logs(task_id)");
371
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_agent ON task_time_logs(agent_id)");
372
- ensureColumn("tasks", "actual_minutes", "INTEGER");
373
- ensureTable("task_watchers", `
374
- CREATE TABLE task_watchers (
375
- id TEXT PRIMARY KEY,
376
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
377
- agent_id TEXT NOT NULL,
378
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
379
- UNIQUE(task_id, agent_id)
380
- )`);
381
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_task ON task_watchers(task_id)");
382
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_agent ON task_watchers(agent_id)");
383
- ensureColumn("task_dependencies", "external_project_id", "TEXT");
384
- ensureColumn("task_dependencies", "external_task_id", "TEXT");
385
- }
386
- function backfillTaskTags(db) {
387
- try {
388
- const count = db.query("SELECT COUNT(*) as count FROM task_tags").get();
389
- if (count && count.count > 0)
390
- return;
391
- } catch {
392
- return;
393
- }
394
- try {
395
- const rows = db.query("SELECT id, tags FROM tasks WHERE tags IS NOT NULL AND tags != '[]'").all();
396
- if (rows.length === 0)
397
- return;
398
- const insert = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
399
- for (const row of rows) {
400
- if (!row.tags)
401
- continue;
402
- let tags = [];
403
- try {
404
- tags = JSON.parse(row.tags);
405
- } catch {
406
- continue;
407
- }
408
- for (const tag of tags) {
409
- if (tag)
410
- insert.run(row.id, tag);
411
- }
412
- }
413
- } catch {}
414
- }
41
+ // src/db/migrations.ts
415
42
  var MIGRATIONS;
416
- var init_schema = __esm(() => {
43
+ var init_migrations = __esm(() => {
417
44
  MIGRATIONS = [
418
45
  `
419
46
  CREATE TABLE IF NOT EXISTS projects (
@@ -557,7 +184,7 @@ var init_schema = __esm(() => {
557
184
  ALTER TABLE projects ADD COLUMN task_counter INTEGER NOT NULL DEFAULT 0;
558
185
 
559
186
  ALTER TABLE tasks ADD COLUMN short_id TEXT;
560
- CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL;
187
+ CREATE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL;
561
188
 
562
189
  INSERT OR IGNORE INTO _migrations (id) VALUES (6);
563
190
  `,
@@ -1142,10 +769,454 @@ var init_schema = __esm(() => {
1142
769
  ALTER TABLE task_dependencies ADD COLUMN external_project_id TEXT;
1143
770
  ALTER TABLE task_dependencies ADD COLUMN external_task_id TEXT;
1144
771
  INSERT OR IGNORE INTO _migrations (id) VALUES (47);
772
+ `,
773
+ `
774
+ CREATE TABLE IF NOT EXISTS task_checkpoints (
775
+ id TEXT PRIMARY KEY,
776
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
777
+ agent_id TEXT,
778
+ step TEXT NOT NULL,
779
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed', 'skipped')),
780
+ data TEXT DEFAULT '{}',
781
+ error TEXT,
782
+ attempt INTEGER NOT NULL DEFAULT 1,
783
+ max_attempts INTEGER NOT NULL DEFAULT 1,
784
+ started_at TEXT,
785
+ completed_at TEXT,
786
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
787
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
788
+ );
789
+ CREATE INDEX IF NOT EXISTS idx_task_checkpoints_task ON task_checkpoints(task_id);
790
+ CREATE INDEX IF NOT EXISTS idx_task_checkpoints_status ON task_checkpoints(status);
791
+
792
+ CREATE TABLE IF NOT EXISTS task_heartbeats (
793
+ id TEXT PRIMARY KEY,
794
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
795
+ agent_id TEXT,
796
+ step TEXT,
797
+ message TEXT,
798
+ progress REAL CHECK(progress >= 0 AND progress <= 1),
799
+ meta TEXT DEFAULT '{}',
800
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
801
+ );
802
+ CREATE INDEX IF NOT EXISTS idx_task_heartbeats_task ON task_heartbeats(task_id);
803
+ CREATE INDEX IF NOT EXISTS idx_task_heartbeats_agent ON task_heartbeats(agent_id);
804
+
805
+ ALTER TABLE tasks ADD COLUMN runner_id TEXT;
806
+ ALTER TABLE tasks ADD COLUMN runner_started_at TEXT;
807
+ ALTER TABLE tasks ADD COLUMN runner_completed_at TEXT;
808
+ ALTER TABLE tasks ADD COLUMN current_step TEXT;
809
+ ALTER TABLE tasks ADD COLUMN total_steps INTEGER;
810
+ INSERT OR IGNORE INTO _migrations (id) VALUES (48);
1145
811
  `
1146
812
  ];
1147
813
  });
1148
814
 
815
+ // src/db/schema.ts
816
+ function runMigrations(db) {
817
+ try {
818
+ const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
819
+ const currentLevel = result?.max_id ?? 0;
820
+ for (let i = currentLevel;i < MIGRATIONS.length; i++) {
821
+ try {
822
+ db.exec(MIGRATIONS[i]);
823
+ } catch {}
824
+ }
825
+ } catch {
826
+ for (const migration of MIGRATIONS) {
827
+ try {
828
+ db.exec(migration);
829
+ } catch {}
830
+ }
831
+ }
832
+ ensureSchema(db);
833
+ }
834
+ function ensureSchema(db) {
835
+ const ensureColumn = (table, column, type) => {
836
+ try {
837
+ db.query(`SELECT ${column} FROM ${table} LIMIT 0`).get();
838
+ } catch {
839
+ try {
840
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
841
+ } catch {}
842
+ }
843
+ };
844
+ const ensureTable = (name, sql) => {
845
+ try {
846
+ const exists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(name);
847
+ if (!exists)
848
+ db.exec(sql);
849
+ } catch {}
850
+ };
851
+ const ensureIndex = (sql) => {
852
+ try {
853
+ db.exec(sql);
854
+ } catch {}
855
+ };
856
+ ensureTable("orgs", `
857
+ CREATE TABLE orgs (
858
+ id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
859
+ metadata TEXT DEFAULT '{}',
860
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
861
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
862
+ )`);
863
+ ensureTable("agents", `
864
+ CREATE TABLE agents (
865
+ id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
866
+ role TEXT DEFAULT 'agent', permissions TEXT DEFAULT '["*"]',
867
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived')),
868
+ metadata TEXT DEFAULT '{}',
869
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
870
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
871
+ )`);
872
+ ensureTable("task_lists", `
873
+ CREATE TABLE task_lists (
874
+ id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
875
+ slug TEXT NOT NULL, name TEXT NOT NULL, description TEXT,
876
+ metadata TEXT DEFAULT '{}',
877
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
878
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
879
+ UNIQUE(project_id, slug)
880
+ )`);
881
+ ensureTable("plans", `
882
+ CREATE TABLE plans (
883
+ id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
884
+ task_list_id TEXT, agent_id TEXT,
885
+ name TEXT NOT NULL, description TEXT,
886
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
887
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
888
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
889
+ )`);
890
+ ensureTable("task_tags", `
891
+ CREATE TABLE task_tags (
892
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
893
+ tag TEXT NOT NULL, PRIMARY KEY (task_id, tag)
894
+ )`);
895
+ ensureTable("task_history", `
896
+ CREATE TABLE task_history (
897
+ id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
898
+ action TEXT NOT NULL, field TEXT, old_value TEXT, new_value TEXT, agent_id TEXT,
899
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
900
+ )`);
901
+ ensureTable("webhooks", `
902
+ CREATE TABLE webhooks (
903
+ id TEXT PRIMARY KEY, url TEXT NOT NULL, events TEXT NOT NULL DEFAULT '[]',
904
+ secret TEXT, active INTEGER NOT NULL DEFAULT 1,
905
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
906
+ )`);
907
+ ensureTable("task_templates", `
908
+ CREATE TABLE task_templates (
909
+ id TEXT PRIMARY KEY, name TEXT NOT NULL, title_pattern TEXT NOT NULL,
910
+ description TEXT, priority TEXT DEFAULT 'medium', tags TEXT DEFAULT '[]',
911
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
912
+ plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
913
+ metadata TEXT DEFAULT '{}',
914
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
915
+ )`);
916
+ ensureTable("template_tasks", `
917
+ CREATE TABLE template_tasks (
918
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
919
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
920
+ position INTEGER NOT NULL,
921
+ title_pattern TEXT NOT NULL,
922
+ description TEXT,
923
+ priority TEXT DEFAULT 'medium',
924
+ tags TEXT DEFAULT '[]',
925
+ task_type TEXT,
926
+ depends_on_positions TEXT DEFAULT '[]',
927
+ metadata TEXT DEFAULT '{}',
928
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
929
+ )`);
930
+ ensureTable("task_checklists", `
931
+ CREATE TABLE task_checklists (
932
+ id TEXT PRIMARY KEY,
933
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
934
+ position INTEGER NOT NULL DEFAULT 0,
935
+ text TEXT NOT NULL,
936
+ checked INTEGER NOT NULL DEFAULT 0,
937
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
938
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
939
+ )`);
940
+ ensureTable("project_sources", `
941
+ CREATE TABLE project_sources (
942
+ id TEXT PRIMARY KEY,
943
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
944
+ type TEXT NOT NULL,
945
+ name TEXT NOT NULL,
946
+ uri TEXT NOT NULL,
947
+ description TEXT,
948
+ metadata TEXT DEFAULT '{}',
949
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
950
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
951
+ )`);
952
+ ensureTable("task_relationships", `
953
+ CREATE TABLE task_relationships (
954
+ id TEXT PRIMARY KEY,
955
+ source_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
956
+ target_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
957
+ relationship_type TEXT NOT NULL,
958
+ metadata TEXT DEFAULT '{}',
959
+ created_by TEXT,
960
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
961
+ CHECK (source_task_id != target_task_id)
962
+ )`);
963
+ ensureTable("kg_edges", `
964
+ CREATE TABLE kg_edges (
965
+ id TEXT PRIMARY KEY,
966
+ source_id TEXT NOT NULL,
967
+ source_type TEXT NOT NULL,
968
+ target_id TEXT NOT NULL,
969
+ target_type TEXT NOT NULL,
970
+ relation_type TEXT NOT NULL,
971
+ weight REAL NOT NULL DEFAULT 1.0,
972
+ metadata TEXT DEFAULT '{}',
973
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
974
+ UNIQUE(source_id, source_type, target_id, target_type, relation_type)
975
+ )`);
976
+ ensureTable("project_machine_paths", `
977
+ CREATE TABLE project_machine_paths (
978
+ id TEXT PRIMARY KEY,
979
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
980
+ machine_id TEXT NOT NULL,
981
+ path TEXT NOT NULL,
982
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
983
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
984
+ UNIQUE(project_id, machine_id)
985
+ )`);
986
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
987
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
988
+ ensureTable("machines", `
989
+ CREATE TABLE machines (
990
+ id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, hostname TEXT, platform TEXT,
991
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
992
+ metadata TEXT DEFAULT '{}',
993
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
994
+ )`);
995
+ ensureColumn("projects", "task_list_id", "TEXT");
996
+ ensureColumn("projects", "task_prefix", "TEXT");
997
+ ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
998
+ ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
999
+ ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
1000
+ ensureColumn("tasks", "short_id", "TEXT");
1001
+ ensureColumn("tasks", "due_at", "TEXT");
1002
+ ensureColumn("tasks", "estimated_minutes", "INTEGER");
1003
+ ensureColumn("tasks", "requires_approval", "INTEGER NOT NULL DEFAULT 0");
1004
+ ensureColumn("tasks", "approved_by", "TEXT");
1005
+ ensureColumn("tasks", "approved_at", "TEXT");
1006
+ ensureColumn("tasks", "recurrence_rule", "TEXT");
1007
+ ensureColumn("tasks", "recurrence_parent_id", "TEXT REFERENCES tasks(id) ON DELETE SET NULL");
1008
+ ensureColumn("tasks", "confidence", "REAL");
1009
+ ensureColumn("tasks", "reason", "TEXT");
1010
+ ensureColumn("tasks", "spawned_from_session", "TEXT");
1011
+ ensureColumn("tasks", "assigned_by", "TEXT");
1012
+ ensureColumn("tasks", "assigned_from_project", "TEXT");
1013
+ ensureColumn("tasks", "started_at", "TEXT");
1014
+ ensureColumn("tasks", "task_type", "TEXT");
1015
+ ensureColumn("tasks", "cost_tokens", "INTEGER DEFAULT 0");
1016
+ ensureColumn("tasks", "cost_usd", "REAL DEFAULT 0");
1017
+ ensureColumn("tasks", "delegated_from", "TEXT");
1018
+ ensureColumn("tasks", "delegation_depth", "INTEGER DEFAULT 0");
1019
+ ensureColumn("tasks", "retry_count", "INTEGER DEFAULT 0");
1020
+ ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
1021
+ ensureColumn("tasks", "retry_after", "TEXT");
1022
+ ensureColumn("tasks", "sla_minutes", "INTEGER");
1023
+ ensureColumn("tasks", "archived_at", "TEXT");
1024
+ ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
1025
+ ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
1026
+ ensureColumn("agents", "reports_to", "TEXT");
1027
+ ensureColumn("agents", "title", "TEXT");
1028
+ ensureColumn("agents", "level", "TEXT");
1029
+ ensureColumn("agents", "org_id", "TEXT");
1030
+ ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
1031
+ ensureColumn("projects", "org_id", "TEXT");
1032
+ ensureColumn("plans", "task_list_id", "TEXT");
1033
+ ensureColumn("plans", "agent_id", "TEXT");
1034
+ ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
1035
+ ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
1036
+ ensureColumn("template_tasks", "condition", "TEXT");
1037
+ ensureColumn("template_tasks", "include_template_id", "TEXT");
1038
+ ensureTable("template_versions", `
1039
+ CREATE TABLE template_versions (
1040
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
1041
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
1042
+ version INTEGER NOT NULL,
1043
+ snapshot TEXT NOT NULL,
1044
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1045
+ )`);
1046
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id)");
1047
+ ensureTable("dispatches", `
1048
+ CREATE TABLE dispatches (
1049
+ id TEXT PRIMARY KEY,
1050
+ title TEXT,
1051
+ target_window TEXT NOT NULL,
1052
+ task_ids TEXT NOT NULL DEFAULT '[]',
1053
+ task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL,
1054
+ message TEXT,
1055
+ delay_ms INTEGER,
1056
+ scheduled_at TEXT,
1057
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'sent', 'failed', 'cancelled')),
1058
+ error TEXT,
1059
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1060
+ sent_at TEXT
1061
+ )`);
1062
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_status ON dispatches(status)");
1063
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_scheduled ON dispatches(scheduled_at)");
1064
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_task_list ON dispatches(task_list_id)");
1065
+ ensureTable("dispatch_logs", `
1066
+ CREATE TABLE dispatch_logs (
1067
+ id TEXT PRIMARY KEY,
1068
+ dispatch_id TEXT NOT NULL REFERENCES dispatches(id) ON DELETE CASCADE,
1069
+ target_window TEXT NOT NULL,
1070
+ message TEXT NOT NULL,
1071
+ delay_ms INTEGER NOT NULL,
1072
+ status TEXT NOT NULL CHECK(status IN ('sent', 'failed')),
1073
+ error TEXT,
1074
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1075
+ )`);
1076
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatch_logs_dispatch ON dispatch_logs(dispatch_id)");
1077
+ ensureColumn("webhooks", "project_id", "TEXT");
1078
+ ensureColumn("webhooks", "task_list_id", "TEXT");
1079
+ ensureColumn("webhooks", "agent_id", "TEXT");
1080
+ ensureColumn("webhooks", "task_id", "TEXT");
1081
+ ensureTable("webhook_deliveries", `
1082
+ CREATE TABLE webhook_deliveries (
1083
+ id TEXT PRIMARY KEY,
1084
+ webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
1085
+ event TEXT NOT NULL,
1086
+ payload TEXT NOT NULL,
1087
+ status_code INTEGER,
1088
+ response TEXT,
1089
+ attempt INTEGER NOT NULL DEFAULT 1,
1090
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1091
+ )`);
1092
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id)");
1093
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
1094
+ ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
1095
+ ensureColumn("task_comments", "progress_pct", "INTEGER");
1096
+ ensureColumn("projects", "machine_id", "TEXT");
1097
+ ensureColumn("projects", "synced_at", "TEXT");
1098
+ ensureColumn("tasks", "machine_id", "TEXT");
1099
+ ensureColumn("tasks", "synced_at", "TEXT");
1100
+ ensureColumn("agents", "machine_id", "TEXT");
1101
+ ensureColumn("agents", "synced_at", "TEXT");
1102
+ ensureColumn("task_lists", "machine_id", "TEXT");
1103
+ ensureColumn("task_lists", "synced_at", "TEXT");
1104
+ ensureColumn("plans", "machine_id", "TEXT");
1105
+ ensureColumn("plans", "synced_at", "TEXT");
1106
+ ensureColumn("task_comments", "machine_id", "TEXT");
1107
+ ensureColumn("task_comments", "synced_at", "TEXT");
1108
+ ensureColumn("sessions", "machine_id", "TEXT");
1109
+ ensureColumn("sessions", "synced_at", "TEXT");
1110
+ ensureColumn("task_history", "machine_id", "TEXT");
1111
+ ensureColumn("webhooks", "machine_id", "TEXT");
1112
+ ensureColumn("webhooks", "synced_at", "TEXT");
1113
+ ensureColumn("task_templates", "machine_id", "TEXT");
1114
+ ensureColumn("task_templates", "synced_at", "TEXT");
1115
+ ensureColumn("orgs", "machine_id", "TEXT");
1116
+ ensureColumn("orgs", "synced_at", "TEXT");
1117
+ ensureColumn("handoffs", "machine_id", "TEXT");
1118
+ ensureColumn("handoffs", "synced_at", "TEXT");
1119
+ ensureColumn("task_checklists", "machine_id", "TEXT");
1120
+ ensureColumn("project_sources", "machine_id", "TEXT");
1121
+ ensureColumn("project_sources", "synced_at", "TEXT");
1122
+ ensureColumn("task_files", "machine_id", "TEXT");
1123
+ ensureColumn("task_relationships", "machine_id", "TEXT");
1124
+ ensureColumn("kg_edges", "machine_id", "TEXT");
1125
+ ensureColumn("project_agent_roles", "machine_id", "TEXT");
1126
+ ensureColumn("dispatches", "machine_id", "TEXT");
1127
+ ensureColumn("dispatches", "synced_at", "TEXT");
1128
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
1129
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id)");
1130
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at)");
1131
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL");
1132
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name)");
1133
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_project ON task_lists(project_id)");
1134
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug)");
1135
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag)");
1136
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id)");
1137
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
1138
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
1139
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
1140
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
1141
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
1142
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(task_id)");
1143
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id)");
1144
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL");
1145
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_checklists_task ON task_checklists(task_id)");
1146
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id)");
1147
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type)");
1148
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by)");
1149
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id)");
1150
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id)");
1151
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type)");
1152
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id)");
1153
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type)");
1154
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type)");
1155
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type)");
1156
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_machine ON tasks(machine_id)");
1157
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_synced ON tasks(synced_at)");
1158
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_machine ON projects(machine_id)");
1159
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_machine ON agents(machine_id)");
1160
+ ensureTable("task_time_logs", `
1161
+ CREATE TABLE task_time_logs (
1162
+ id TEXT PRIMARY KEY,
1163
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1164
+ agent_id TEXT,
1165
+ started_at TEXT,
1166
+ ended_at TEXT,
1167
+ minutes INTEGER NOT NULL,
1168
+ notes TEXT,
1169
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1170
+ )`);
1171
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_task ON task_time_logs(task_id)");
1172
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_agent ON task_time_logs(agent_id)");
1173
+ ensureColumn("tasks", "actual_minutes", "INTEGER");
1174
+ ensureTable("task_watchers", `
1175
+ CREATE TABLE task_watchers (
1176
+ id TEXT PRIMARY KEY,
1177
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1178
+ agent_id TEXT NOT NULL,
1179
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1180
+ UNIQUE(task_id, agent_id)
1181
+ )`);
1182
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_task ON task_watchers(task_id)");
1183
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_agent ON task_watchers(agent_id)");
1184
+ ensureColumn("task_dependencies", "external_project_id", "TEXT");
1185
+ ensureColumn("task_dependencies", "external_task_id", "TEXT");
1186
+ }
1187
+ function backfillTaskTags(db) {
1188
+ try {
1189
+ const count = db.query("SELECT COUNT(*) as count FROM task_tags").get();
1190
+ if (count && count.count > 0)
1191
+ return;
1192
+ } catch {
1193
+ return;
1194
+ }
1195
+ try {
1196
+ const rows = db.query("SELECT id, tags FROM tasks WHERE tags IS NOT NULL AND tags != '[]'").all();
1197
+ if (rows.length === 0)
1198
+ return;
1199
+ const insert = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
1200
+ for (const row of rows) {
1201
+ if (!row.tags)
1202
+ continue;
1203
+ let tags = [];
1204
+ try {
1205
+ tags = JSON.parse(row.tags);
1206
+ } catch {
1207
+ continue;
1208
+ }
1209
+ for (const tag of tags) {
1210
+ if (tag)
1211
+ insert.run(row.id, tag);
1212
+ }
1213
+ }
1214
+ } catch {}
1215
+ }
1216
+ var init_schema = __esm(() => {
1217
+ init_migrations();
1218
+ });
1219
+
1149
1220
  // src/db/machines.ts
1150
1221
  import { hostname as osHostname, platform as osPlatform } from "os";
1151
1222
  function rowToMachine(row) {
@@ -1238,6 +1309,19 @@ var init_machines = __esm(() => {
1238
1309
  });
1239
1310
 
1240
1311
  // src/db/database.ts
1312
+ var exports_database = {};
1313
+ __export(exports_database, {
1314
+ uuid: () => uuid,
1315
+ resolvePartialId: () => resolvePartialId,
1316
+ resetDatabase: () => resetDatabase,
1317
+ now: () => now,
1318
+ lockExpiryCutoff: () => lockExpiryCutoff,
1319
+ isLockExpired: () => isLockExpired,
1320
+ getDatabase: () => getDatabase,
1321
+ closeDatabase: () => closeDatabase,
1322
+ clearExpiredLocks: () => clearExpiredLocks,
1323
+ LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
1324
+ });
1241
1325
  import { Database } from "bun:sqlite";
1242
1326
  import { existsSync, mkdirSync } from "fs";
1243
1327
  import { dirname, join, resolve } from "path";
@@ -1347,6 +1431,9 @@ function clearExpiredLocks(db) {
1347
1431
  db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
1348
1432
  }
1349
1433
  function resolvePartialId(db, table, partialId) {
1434
+ if (!ALLOWED_TABLES.has(table)) {
1435
+ throw new Error(`Invalid table name: ${table}`);
1436
+ }
1350
1437
  if (partialId.length >= 36) {
1351
1438
  const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
1352
1439
  return row?.id ?? null;
@@ -1376,10 +1463,11 @@ function resolvePartialId(db, table, partialId) {
1376
1463
  }
1377
1464
  return null;
1378
1465
  }
1379
- var LOCK_EXPIRY_MINUTES = 30, _db = null;
1466
+ var LOCK_EXPIRY_MINUTES = 30, _db = null, ALLOWED_TABLES;
1380
1467
  var init_database = __esm(() => {
1381
1468
  init_schema();
1382
1469
  init_machines();
1470
+ ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates"]);
1383
1471
  });
1384
1472
 
1385
1473
  // src/types/index.ts
@@ -1612,231 +1700,572 @@ function getDueDispatches(db) {
1612
1700
  ORDER BY created_at ASC`).all(now());
1613
1701
  return rows.map(rowToDispatch);
1614
1702
  }
1615
- var init_dispatches = __esm(() => {
1616
- init_database();
1617
- init_types();
1618
- });
1703
+ var init_dispatches = __esm(() => {
1704
+ init_database();
1705
+ init_types();
1706
+ });
1707
+
1708
+ // src/sdk/types.ts
1709
+ class TodosAPIError extends Error {
1710
+ status;
1711
+ statusText;
1712
+ body;
1713
+ constructor(message, status, statusText, body) {
1714
+ super(message);
1715
+ this.status = status;
1716
+ this.statusText = statusText;
1717
+ this.body = body;
1718
+ this.name = "TodosAPIError";
1719
+ }
1720
+ }
1721
+
1722
+ class TodosNotFoundError extends TodosAPIError {
1723
+ constructor(message, body) {
1724
+ super(message, 404, "Not Found", body);
1725
+ this.name = "TodosNotFoundError";
1726
+ }
1727
+ }
1728
+
1729
+ class TodosConflictError extends TodosAPIError {
1730
+ constructor(message, body) {
1731
+ super(message, 409, "Conflict", body);
1732
+ this.name = "TodosConflictError";
1733
+ }
1734
+ }
1735
+
1736
+ class TodosUnauthorizedError extends TodosAPIError {
1737
+ constructor(message, body) {
1738
+ super(message, 401, "Unauthorized", body);
1739
+ this.name = "TodosUnauthorizedError";
1740
+ }
1741
+ }
1742
+
1743
+ class TodosRateLimitError extends TodosAPIError {
1744
+ retryAfter;
1745
+ constructor(message, retryAfter, body) {
1746
+ super(message, 429, "Too Many Requests", body);
1747
+ this.retryAfter = retryAfter;
1748
+ this.name = "TodosRateLimitError";
1749
+ }
1750
+ }
1751
+
1752
+ class TodosTimeoutError extends Error {
1753
+ ms;
1754
+ constructor(ms) {
1755
+ super(`Request timed out after ${ms}ms`);
1756
+ this.ms = ms;
1757
+ this.name = "TodosTimeoutError";
1758
+ }
1759
+ }
1760
+
1761
+ // src/sdk/client.ts
1762
+ function buildQuery(params) {
1763
+ const search = new URLSearchParams;
1764
+ for (const [k, v] of Object.entries(params)) {
1765
+ if (v !== undefined)
1766
+ search.set(k, String(v));
1767
+ }
1768
+ const s = search.toString();
1769
+ return s ? `?${s}` : "";
1770
+ }
1771
+
1772
+ class TasksResource {
1773
+ client;
1774
+ constructor(client) {
1775
+ this.client = client;
1776
+ }
1777
+ async list(options) {
1778
+ return this.client._get("/api/tasks", buildQuery(options || {}));
1779
+ }
1780
+ async get(id) {
1781
+ return this.client._get(`/api/tasks/${id}`);
1782
+ }
1783
+ async getWithRelations(id) {
1784
+ return this.client._get(`/api/tasks/${id}`);
1785
+ }
1786
+ async create(data) {
1787
+ return this.client._post("/api/tasks", data);
1788
+ }
1789
+ async update(id, data) {
1790
+ return this.client._patch(`/api/tasks/${id}`, data);
1791
+ }
1792
+ async delete(id) {
1793
+ return this.client._delete(`/api/tasks/${id}`);
1794
+ }
1795
+ async start(id, agentId) {
1796
+ return this.client._post(`/api/tasks/${id}/start`, { agent_id: agentId });
1797
+ }
1798
+ async complete(id, agentId) {
1799
+ return this.client._post(`/api/tasks/${id}/complete`, { agent_id: agentId });
1800
+ }
1801
+ async fail(id, options) {
1802
+ return this.client._post(`/api/tasks/${id}/fail`, options || {});
1803
+ }
1804
+ async logProgress(taskId, message, pctComplete, agentId) {
1805
+ return this.client._post(`/api/tasks/${taskId}/progress`, {
1806
+ message,
1807
+ pct_complete: pctComplete,
1808
+ agent_id: agentId
1809
+ });
1810
+ }
1811
+ async getProgress(id) {
1812
+ return this.client._get(`/api/tasks/${id}/progress`);
1813
+ }
1814
+ async getHistory(id) {
1815
+ return this.client._get(`/api/tasks/${id}/history`);
1816
+ }
1817
+ async getAttachments(id) {
1818
+ return this.client._get(`/api/tasks/${id}/attachments`);
1819
+ }
1820
+ async status(options) {
1821
+ return this.client._get("/api/tasks/status", buildQuery(options || {}));
1822
+ }
1823
+ async next(options) {
1824
+ return this.client._get("/api/tasks/next", buildQuery(options || {}));
1825
+ }
1826
+ async active(projectId) {
1827
+ return this.client._get("/api/tasks/active", projectId ? `?project_id=${projectId}` : "");
1828
+ }
1829
+ async stale(options) {
1830
+ return this.client._get("/api/tasks/stale", buildQuery(options || {}));
1831
+ }
1832
+ async changedSince(since, projectId) {
1833
+ return this.client._get("/api/tasks/changed", buildQuery({ since, project_id: projectId }));
1834
+ }
1835
+ async context(options) {
1836
+ const q = buildQuery({
1837
+ agent_id: options?.agentId,
1838
+ project_id: options?.projectId,
1839
+ format: options?.format
1840
+ });
1841
+ const url = `${this.client.baseUrl}/api/tasks/context${q}`;
1842
+ const res = await this.client._fetchRaw(url);
1843
+ if (!res.ok)
1844
+ return options?.format === "json" ? {} : "";
1845
+ if (options?.format === "json")
1846
+ return res.json();
1847
+ return res.text();
1848
+ }
1849
+ async export(options) {
1850
+ const q = buildQuery(options || {});
1851
+ const url = `${this.client.baseUrl}/api/tasks/export${q}`;
1852
+ const res = await this.client._fetchRaw(url);
1853
+ if (!res.ok)
1854
+ throw new TodosAPIError("Export failed", res.status, res.statusText, null);
1855
+ if (options?.format === "csv")
1856
+ return res.text();
1857
+ return res.json();
1858
+ }
1859
+ async bulk(ids, action) {
1860
+ return this.client._post("/api/tasks/bulk", { ids, action });
1861
+ }
1862
+ async claim(agentId, projectId) {
1863
+ return this.client._post("/api/tasks/claim", { agent_id: agentId, project_id: projectId });
1864
+ }
1865
+ async* subscribe(options = {}) {
1866
+ const q = buildQuery({
1867
+ agent_id: options.agentId,
1868
+ project_id: options.projectId,
1869
+ events: options.events?.join(",")
1870
+ });
1871
+ const url = `${this.client.baseUrl}/api/tasks/stream${q}`;
1872
+ const resp = await fetch(url);
1873
+ if (!resp.ok || !resp.body)
1874
+ throw new Error(`SSE connection failed: ${resp.status}`);
1875
+ const reader = resp.body.getReader();
1876
+ const decoder = new TextDecoder;
1877
+ let buffer = "";
1878
+ try {
1879
+ while (true) {
1880
+ const { done, value } = await reader.read();
1881
+ if (done)
1882
+ break;
1883
+ buffer += decoder.decode(value, { stream: true });
1884
+ const lines = buffer.split(`
1885
+ `);
1886
+ buffer = lines.pop() || "";
1887
+ for (const line of lines) {
1888
+ if (line.startsWith("data: ")) {
1889
+ try {
1890
+ const data = JSON.parse(line.slice(6));
1891
+ if (data.type !== "connected")
1892
+ yield data;
1893
+ } catch {}
1894
+ }
1895
+ }
1896
+ }
1897
+ } finally {
1898
+ reader.releaseLock();
1899
+ }
1900
+ }
1901
+ }
1902
+
1903
+ class AgentsResource {
1904
+ client;
1905
+ constructor(client) {
1906
+ this.client = client;
1907
+ }
1908
+ async list(options) {
1909
+ return this.client._get("/api/agents", buildQuery(options || {}));
1910
+ }
1911
+ async register(data) {
1912
+ return this.client._post("/api/agents", data);
1913
+ }
1914
+ async fullRegister(data) {
1915
+ return this.client._post("/api/agents", data);
1916
+ }
1917
+ async update(id, data) {
1918
+ return this.client._patch(`/api/agents/${id}`, data);
1919
+ }
1920
+ async delete(id) {
1921
+ return this.client._delete(`/api/agents/${id}`);
1922
+ }
1923
+ async bulkDelete(ids) {
1924
+ return this.client._post("/api/agents/bulk", { ids, action: "delete" });
1925
+ }
1926
+ async me(name) {
1927
+ return this.client._get("/api/agents/me", `?name=${encodeURIComponent(name)}`);
1928
+ }
1929
+ async queue(agentId) {
1930
+ return this.client._get(`/api/agents/${encodeURIComponent(agentId)}/queue`);
1931
+ }
1932
+ async team(agentId) {
1933
+ return this.client._get(`/api/agents/${encodeURIComponent(agentId)}/team`);
1934
+ }
1935
+ async orgChart() {
1936
+ return this.client._get("/api/org");
1937
+ }
1938
+ }
1939
+
1940
+ class ProjectsResource {
1941
+ client;
1942
+ constructor(client) {
1943
+ this.client = client;
1944
+ }
1945
+ async list(options) {
1946
+ return this.client._get("/api/projects", buildQuery(options || {}));
1947
+ }
1948
+ async create(data) {
1949
+ return this.client._post("/api/projects", data);
1950
+ }
1951
+ async delete(id) {
1952
+ return this.client._delete(`/api/projects/${id}`);
1953
+ }
1954
+ async bulkDelete(ids) {
1955
+ return this.client._post("/api/projects/bulk", { ids, action: "delete" });
1956
+ }
1957
+ }
1958
+
1959
+ class PlansResource {
1960
+ client;
1961
+ constructor(client) {
1962
+ this.client = client;
1963
+ }
1964
+ async list(projectId) {
1965
+ return this.client._get("/api/plans", projectId ? `?project_id=${projectId}` : "");
1966
+ }
1967
+ async create(data) {
1968
+ return this.client._post("/api/plans", data);
1969
+ }
1970
+ async get(id) {
1971
+ return this.client._get(`/api/plans/${id}`);
1972
+ }
1973
+ async update(id, data) {
1974
+ return this.client._patch(`/api/plans/${id}`, data);
1975
+ }
1976
+ async delete(id) {
1977
+ return this.client._delete(`/api/plans/${id}`);
1978
+ }
1979
+ async bulkDelete(ids) {
1980
+ return this.client._post("/api/plans/bulk", { ids, action: "delete" });
1981
+ }
1982
+ }
1983
+
1984
+ class OrgsResource {
1985
+ client;
1986
+ constructor(client) {
1987
+ this.client = client;
1988
+ }
1989
+ async list() {
1990
+ return this.client._get("/api/orgs");
1991
+ }
1992
+ async create(data) {
1993
+ return this.client._post("/api/orgs", data);
1994
+ }
1995
+ async update(id, data) {
1996
+ return this.client._patch(`/api/orgs/${id}`, data);
1997
+ }
1998
+ async delete(id) {
1999
+ return this.client._delete(`/api/orgs/${id}`);
2000
+ }
2001
+ }
2002
+
2003
+ class WebhooksResource {
2004
+ client;
2005
+ constructor(client) {
2006
+ this.client = client;
2007
+ }
2008
+ async list() {
2009
+ return this.client._get("/api/webhooks");
2010
+ }
2011
+ async create(data) {
2012
+ return this.client._post("/api/webhooks", data);
2013
+ }
2014
+ async delete(id) {
2015
+ return this.client._delete(`/api/webhooks/${id}`);
2016
+ }
2017
+ }
2018
+
2019
+ class TemplatesResource {
2020
+ client;
2021
+ constructor(client) {
2022
+ this.client = client;
2023
+ }
2024
+ async list() {
2025
+ return this.client._get("/api/templates");
2026
+ }
2027
+ async create(data) {
2028
+ return this.client._post("/api/templates", data);
2029
+ }
2030
+ async delete(id) {
2031
+ return this.client._delete(`/api/templates/${id}`);
2032
+ }
2033
+ }
1619
2034
 
1620
- // src/sdk.ts
1621
2035
  class TodosClient {
1622
2036
  baseUrl;
1623
2037
  timeout;
2038
+ apiKey;
2039
+ maxRetries;
2040
+ retryDelay;
2041
+ tasks;
2042
+ agents;
2043
+ projects;
2044
+ plans;
2045
+ orgs;
2046
+ webhooks;
2047
+ templates;
1624
2048
  constructor(options = {}) {
1625
2049
  this.baseUrl = options.baseUrl || process.env["TODOS_URL"] || "http://localhost:19427";
1626
- this.timeout = options.timeout || 1e4;
2050
+ this.baseUrl = this.baseUrl.replace(/\/+$/, "");
2051
+ this.timeout = options.timeout ?? 1e4;
2052
+ this.apiKey = options.apiKey || process.env["TODOS_API_KEY"] || null;
2053
+ this.maxRetries = options.maxRetries ?? 0;
2054
+ this.retryDelay = options.retryDelay ?? 1000;
2055
+ this.tasks = new TasksResource(this);
2056
+ this.agents = new AgentsResource(this);
2057
+ this.projects = new ProjectsResource(this);
2058
+ this.plans = new PlansResource(this);
2059
+ this.orgs = new OrgsResource(this);
2060
+ this.webhooks = new WebhooksResource(this);
2061
+ this.templates = new TemplatesResource(this);
2062
+ }
2063
+ static fromEnv(apiKey) {
2064
+ return new TodosClient({ apiKey });
2065
+ }
2066
+ async _fetchRaw(url, init) {
2067
+ const controller = new AbortController;
2068
+ const timer = setTimeout(() => controller.abort(), this.timeout);
2069
+ try {
2070
+ const headers = this._buildHeaders(init?.headers);
2071
+ return await fetch(url, { ...init, headers, signal: controller.signal });
2072
+ } finally {
2073
+ clearTimeout(timer);
2074
+ }
1627
2075
  }
1628
- static fromEnv() {
1629
- return new TodosClient({ baseUrl: process.env["TODOS_URL"] });
2076
+ _buildHeaders(existing) {
2077
+ const headers = { "Content-Type": "application/json" };
2078
+ if (this.apiKey)
2079
+ headers["x-api-key"] = this.apiKey;
2080
+ if (existing) {
2081
+ if (existing instanceof Headers) {
2082
+ existing.forEach((v, k) => {
2083
+ headers[k] = v;
2084
+ });
2085
+ } else if (Array.isArray(existing)) {
2086
+ for (const [k, v] of existing)
2087
+ headers[k] = v;
2088
+ } else {
2089
+ Object.assign(headers, existing);
2090
+ }
2091
+ }
2092
+ return headers;
2093
+ }
2094
+ async _fetchWithRetry(path, init) {
2095
+ let lastError = null;
2096
+ const maxAttempts = this.maxRetries + 1;
2097
+ for (let attempt = 0;attempt < maxAttempts; attempt++) {
2098
+ try {
2099
+ return await this._fetch(path, init);
2100
+ } catch (e) {
2101
+ lastError = e;
2102
+ if (e instanceof TodosAPIError && e.status < 500 && e.status !== 429)
2103
+ throw e;
2104
+ if (e instanceof TodosUnauthorizedError || e instanceof TodosNotFoundError || e instanceof TodosConflictError)
2105
+ throw e;
2106
+ if (attempt < maxAttempts - 1) {
2107
+ const delay = this.retryDelay * Math.pow(2, attempt);
2108
+ if (e instanceof TodosRateLimitError) {
2109
+ await this._sleep(e.retryAfter * 1000);
2110
+ } else {
2111
+ await this._sleep(delay);
2112
+ }
2113
+ }
2114
+ }
2115
+ }
2116
+ throw lastError || new Error("Request failed after retries");
1630
2117
  }
1631
- async fetch(path, init) {
2118
+ async _fetch(path, init) {
1632
2119
  const controller = new AbortController;
1633
2120
  const timer = setTimeout(() => controller.abort(), this.timeout);
1634
2121
  try {
1635
- const res = await fetch(`${this.baseUrl}${path}`, { ...init, signal: controller.signal });
2122
+ const url = `${this.baseUrl}${path}`;
2123
+ const headers = this._buildHeaders(init?.headers);
2124
+ const res = await fetch(url, { ...init, headers, signal: controller.signal });
1636
2125
  if (!res.ok) {
1637
- const err = await res.json().catch(() => ({ error: res.statusText }));
1638
- throw new Error(err.error || `HTTP ${res.status}`);
2126
+ const body = await res.json().catch(() => ({ error: res.statusText }));
2127
+ const message = body.error || `HTTP ${res.status}: ${res.statusText}`;
2128
+ if (res.status === 401)
2129
+ throw new TodosUnauthorizedError(message, body);
2130
+ if (res.status === 404)
2131
+ throw new TodosNotFoundError(message, body);
2132
+ if (res.status === 409)
2133
+ throw new TodosConflictError(message, body);
2134
+ if (res.status === 429) {
2135
+ const retryAfter = parseInt(res.headers.get("retry-after") || "60", 10);
2136
+ throw new TodosRateLimitError(message, retryAfter, body);
2137
+ }
2138
+ throw new TodosAPIError(message, res.status, res.statusText, body);
1639
2139
  }
2140
+ const contentLength = res.headers.get("content-length");
2141
+ if (contentLength === "0" || contentLength === "4")
2142
+ return null;
1640
2143
  return res.json();
2144
+ } catch (e) {
2145
+ if (e instanceof DOMException && e.name === "AbortError") {
2146
+ throw new TodosTimeoutError(this.timeout);
2147
+ }
2148
+ throw e;
1641
2149
  } finally {
1642
2150
  clearTimeout(timer);
1643
2151
  }
1644
2152
  }
2153
+ async _get(path, query = "") {
2154
+ return this._fetchWithRetry(`${path}${query}`);
2155
+ }
2156
+ async _post(path, body) {
2157
+ return this._fetchWithRetry(path, {
2158
+ method: "POST",
2159
+ body: body ? JSON.stringify(body) : undefined
2160
+ });
2161
+ }
2162
+ async _patch(path, body) {
2163
+ return this._fetchWithRetry(path, {
2164
+ method: "PATCH",
2165
+ body: JSON.stringify(body)
2166
+ });
2167
+ }
2168
+ async _delete(path) {
2169
+ return this._fetchWithRetry(path, { method: "DELETE" });
2170
+ }
2171
+ _sleep(ms) {
2172
+ return new Promise((resolve) => setTimeout(resolve, ms));
2173
+ }
1645
2174
  async getHealth() {
1646
- return this.fetch("/api/health");
2175
+ return this._get("/api/health");
1647
2176
  }
1648
2177
  async isAlive() {
1649
2178
  try {
1650
- await this.fetch("/api/stats");
2179
+ await this._get("/api/stats");
1651
2180
  return true;
1652
2181
  } catch {
1653
2182
  return false;
1654
2183
  }
1655
2184
  }
1656
- async getStatus(projectId, agentId) {
1657
- const params = new URLSearchParams;
1658
- if (projectId)
1659
- params.set("project_id", projectId);
1660
- if (agentId)
1661
- params.set("agent_id", agentId);
1662
- return this.fetch(`/api/tasks/status?${params}`);
2185
+ async getStats() {
2186
+ return this._get("/api/stats");
2187
+ }
2188
+ async getReport(options) {
2189
+ return this._get("/api/report", buildQuery({ days: options?.days, project_id: options?.projectId }));
2190
+ }
2191
+ async doctor() {
2192
+ return this._get("/api/doctor");
2193
+ }
2194
+ async activity(limit) {
2195
+ return this._get("/api/activity", limit ? `?limit=${limit}` : "");
1663
2196
  }
1664
2197
  async listTasks(filter = {}) {
1665
- const params = new URLSearchParams;
1666
- for (const [k, v] of Object.entries(filter)) {
1667
- if (v !== undefined)
1668
- params.set(k, String(v));
1669
- }
1670
- return this.fetch(`/api/tasks?${params}`);
2198
+ return this.tasks.list(filter);
1671
2199
  }
1672
2200
  async getTask(id) {
1673
- return this.fetch(`/api/tasks/${id}`);
2201
+ return this.tasks.get(id);
1674
2202
  }
1675
- async getTaskAttachments(id) {
1676
- return this.fetch(`/api/tasks/${id}/attachments`);
2203
+ async createTask(data) {
2204
+ return this.tasks.create(data);
1677
2205
  }
1678
- async getTaskHistory(id) {
1679
- return this.fetch(`/api/tasks/${id}/history`);
2206
+ async updateTask(id, data) {
2207
+ return this.tasks.update(id, data);
1680
2208
  }
1681
- async getTaskProgress(id) {
1682
- return this.fetch(`/api/tasks/${id}/progress`);
2209
+ async deleteTask(id) {
2210
+ await this.tasks.delete(id);
1683
2211
  }
1684
- async claimNextTask(agentId, projectId) {
1685
- return this.fetch("/api/tasks/claim", {
1686
- method: "POST",
1687
- headers: { "Content-Type": "application/json" },
1688
- body: JSON.stringify({ agent_id: agentId, project_id: projectId })
1689
- });
2212
+ async startTask(id, agentId) {
2213
+ return this.tasks.start(id, agentId);
1690
2214
  }
1691
2215
  async completeTask(id, agentId) {
1692
- return this.fetch(`/api/tasks/${id}/complete`, {
1693
- method: "POST",
1694
- headers: { "Content-Type": "application/json" },
1695
- body: JSON.stringify({ agent_id: agentId })
1696
- });
1697
- }
1698
- async createTask(data) {
1699
- return this.fetch("/api/tasks", {
1700
- method: "POST",
1701
- headers: { "Content-Type": "application/json" },
1702
- body: JSON.stringify(data)
1703
- });
2216
+ return this.tasks.complete(id, agentId);
1704
2217
  }
1705
- async updateTask(id, data) {
1706
- return this.fetch(`/api/tasks/${id}`, {
1707
- method: "PATCH",
1708
- headers: { "Content-Type": "application/json" },
1709
- body: JSON.stringify(data)
1710
- });
2218
+ async failTask(id, options = {}) {
2219
+ return this.tasks.fail(id, options);
1711
2220
  }
1712
- async deleteTask(id) {
1713
- await this.fetch(`/api/tasks/${id}`, { method: "DELETE" });
2221
+ async logProgress(taskId, message, pctComplete, agentId) {
2222
+ return this.tasks.logProgress(taskId, message, pctComplete, agentId);
1714
2223
  }
1715
- async getStats() {
1716
- return this.fetch("/api/stats");
2224
+ async getStatus(projectId, agentId) {
2225
+ return this.tasks.status({ project_id: projectId, agent_id: agentId });
1717
2226
  }
1718
2227
  async getActiveWork(projectId) {
1719
- const params = new URLSearchParams;
1720
- if (projectId)
1721
- params.set("project_id", projectId);
1722
- return this.fetch(`/api/tasks/active?${params}`);
2228
+ const res = await this.tasks.active(projectId);
2229
+ return res.active;
1723
2230
  }
1724
2231
  async getTasksChangedSince(since, projectId) {
1725
- const params = new URLSearchParams({ since });
1726
- if (projectId)
1727
- params.set("project_id", projectId);
1728
- return this.fetch(`/api/tasks/changed?${params}`);
1729
- }
1730
- async startTask(id, agentId) {
1731
- return this.fetch(`/api/tasks/${id}/start`, {
1732
- method: "POST",
1733
- headers: { "Content-Type": "application/json" },
1734
- body: JSON.stringify({ agent_id: agentId })
1735
- });
2232
+ return this.tasks.changedSince(since, projectId);
1736
2233
  }
1737
- async failTask(id, options = {}) {
1738
- return this.fetch(`/api/tasks/${id}/fail`, {
1739
- method: "POST",
1740
- headers: { "Content-Type": "application/json" },
1741
- body: JSON.stringify(options)
1742
- });
2234
+ async getStaleTasks(minutes, projectId) {
2235
+ return this.tasks.stale({ minutes, project_id: projectId });
1743
2236
  }
1744
- async logProgress(taskId, message, pctComplete, agentId) {
1745
- return this.fetch(`/api/tasks/${taskId}/progress`, {
1746
- method: "POST",
1747
- headers: { "Content-Type": "application/json" },
1748
- body: JSON.stringify({ message, pct_complete: pctComplete, agent_id: agentId })
1749
- });
2237
+ async getContext(options = {}) {
2238
+ return this.tasks.context(options);
1750
2239
  }
1751
2240
  async exportTasks(filter = {}) {
1752
- const params = new URLSearchParams;
1753
- if (filter.status)
1754
- params.set("status", filter.status);
1755
- if (filter.project_id)
1756
- params.set("project_id", filter.project_id);
1757
- const fmt = filter.format || "json";
1758
- if (fmt === "csv")
1759
- params.set("format", "csv");
1760
- return this.fetch(`/api/tasks/export?${params}`);
2241
+ return this.tasks.export(filter);
1761
2242
  }
1762
- async getProjects() {
1763
- return this.fetch("/api/projects");
2243
+ async claimNextTask(agentId, projectId) {
2244
+ return this.tasks.claim(agentId, projectId);
1764
2245
  }
1765
- async getContext(options = {}) {
1766
- const params = new URLSearchParams;
1767
- if (options.agentId)
1768
- params.set("agent_id", options.agentId);
1769
- if (options.projectId)
1770
- params.set("project_id", options.projectId);
1771
- if (options.format)
1772
- params.set("format", options.format);
1773
- const url = `${this.baseUrl}/api/tasks/context?${params}`;
1774
- const controller = new AbortController;
1775
- const timer = setTimeout(() => controller.abort(), this.timeout);
1776
- try {
1777
- const res = await fetch(url, { signal: controller.signal });
1778
- if (!res.ok)
1779
- return options.format === "json" ? {} : "";
1780
- if (options.format === "json")
1781
- return res.json();
1782
- return res.text();
1783
- } catch {
1784
- return options.format === "json" ? {} : "";
1785
- } finally {
1786
- clearTimeout(timer);
1787
- }
2246
+ async getTaskHistory(id) {
2247
+ return this.tasks.getHistory(id);
2248
+ }
2249
+ async getTaskAttachments(id) {
2250
+ return this.tasks.getAttachments(id);
1788
2251
  }
1789
- async getReport(options = {}) {
1790
- const params = new URLSearchParams;
1791
- if (options.days)
1792
- params.set("days", String(options.days));
1793
- if (options.projectId)
1794
- params.set("project_id", options.projectId);
1795
- return this.fetch(`/api/report?${params}`);
2252
+ async getTaskProgress(id) {
2253
+ return this.tasks.getProgress(id);
1796
2254
  }
1797
2255
  async* subscribeToStream(options = {}) {
1798
- const params = new URLSearchParams;
1799
- if (options.agentId)
1800
- params.set("agent_id", options.agentId);
1801
- if (options.projectId)
1802
- params.set("project_id", options.projectId);
1803
- if (options.events)
1804
- params.set("events", options.events.join(","));
1805
- const url = `${this.baseUrl}/api/tasks/stream?${params}`;
1806
- const resp = await fetch(url);
1807
- if (!resp.ok || !resp.body)
1808
- throw new Error(`SSE connection failed: ${resp.status}`);
1809
- const reader = resp.body.getReader();
1810
- const decoder = new TextDecoder;
1811
- let buffer = "";
1812
- while (true) {
1813
- const { done, value } = await reader.read();
1814
- if (done)
1815
- break;
1816
- buffer += decoder.decode(value, { stream: true });
1817
- const lines = buffer.split(`
1818
- `);
1819
- buffer = lines.pop() || "";
1820
- for (const line of lines) {
1821
- if (line.startsWith("data: ")) {
1822
- try {
1823
- const data = JSON.parse(line.slice(6));
1824
- if (data.type !== "connected")
1825
- yield data;
1826
- } catch {}
1827
- }
1828
- }
1829
- }
2256
+ yield* this.tasks.subscribe(options);
2257
+ }
2258
+ async getProjects() {
2259
+ return this.projects.list();
1830
2260
  }
1831
2261
  }
1832
2262
  function createClient(options) {
1833
2263
  return new TodosClient(options);
1834
2264
  }
1835
-
1836
2265
  // src/index.ts
1837
2266
  init_database();
1838
2267
 
1839
- // src/db/tasks.ts
2268
+ // src/db/task-crud.ts
1840
2269
  init_types();
1841
2270
  init_database();
1842
2271
 
@@ -2207,184 +2636,111 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
2207
2636
  const remaining = Math.ceil(config.min_work_seconds - elapsedSeconds);
2208
2637
  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);
2209
2638
  }
2210
- }
2211
- if (agent && config.max_completions_per_window && config.window_minutes) {
2212
- const windowStart = new Date(Date.now() - config.window_minutes * 60 * 1000).toISOString();
2213
- const result = db.query(`SELECT COUNT(*) as count FROM tasks
2214
- WHERE completed_at > ? AND (assigned_to = ? OR agent_id = ?)`).get(windowStart, agent, agent);
2215
- if (result.count >= config.max_completions_per_window) {
2216
- throw new CompletionGuardError(`Rate limit: ${result.count} tasks completed in the last ${config.window_minutes} minutes (max ${config.max_completions_per_window}). Slow down.`);
2217
- }
2218
- }
2219
- if (agent && config.cooldown_seconds) {
2220
- const result = db.query(`SELECT MAX(completed_at) as last_completed FROM tasks
2221
- WHERE completed_at IS NOT NULL AND (assigned_to = ? OR agent_id = ?) AND id != ?`).get(agent, agent, task.id);
2222
- if (result.last_completed) {
2223
- const elapsedSeconds = (Date.now() - new Date(result.last_completed).getTime()) / 1000;
2224
- if (elapsedSeconds < config.cooldown_seconds) {
2225
- const remaining = Math.ceil(config.cooldown_seconds - elapsedSeconds);
2226
- throw new CompletionGuardError(`Cooldown: last completion was ${Math.floor(elapsedSeconds)}s ago. Wait ${remaining}s between completions.`, remaining);
2227
- }
2228
- }
2229
- }
2230
- }
2231
-
2232
- // src/db/audit.ts
2233
- init_database();
2234
- function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
2235
- const d = db || getDatabase();
2236
- const id = uuid();
2237
- const timestamp = now();
2238
- d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
2239
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
2240
- return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
2241
- }
2242
- function getTaskHistory(taskId, db) {
2243
- const d = db || getDatabase();
2244
- return d.query("SELECT * FROM task_history WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
2245
- }
2246
- function getRecentActivity(limit = 50, db) {
2247
- const d = db || getDatabase();
2248
- return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
2249
- }
2250
- function getRecap(hours = 8, projectId, db) {
2251
- const d = db || getDatabase();
2252
- const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
2253
- const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
2254
- const pf = projectId ? " AND project_id = ?" : "";
2255
- const tpf = projectId ? " AND t.project_id = ?" : "";
2256
- 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);
2257
- 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);
2258
- 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();
2259
- 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();
2260
- 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);
2261
- 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);
2262
- return {
2263
- hours,
2264
- since,
2265
- completed: completed.map((r) => ({
2266
- ...r,
2267
- duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
2268
- })),
2269
- created,
2270
- in_progress,
2271
- blocked,
2272
- stale,
2273
- agents
2274
- };
2275
- }
2276
-
2277
- // src/lib/recurrence.ts
2278
- var DAY_NAMES = {
2279
- sunday: 0,
2280
- sun: 0,
2281
- monday: 1,
2282
- mon: 1,
2283
- tuesday: 2,
2284
- tue: 2,
2285
- wednesday: 3,
2286
- wed: 3,
2287
- thursday: 4,
2288
- thu: 4,
2289
- friday: 5,
2290
- fri: 5,
2291
- saturday: 6,
2292
- sat: 6
2293
- };
2294
- function parseRecurrenceRule(rule) {
2295
- const normalized = rule.trim().toLowerCase();
2296
- if (normalized === "every weekday" || normalized === "every weekdays") {
2297
- return { type: "specific_days", days: [1, 2, 3, 4, 5] };
2298
- }
2299
- if (normalized === "every day" || normalized === "daily") {
2300
- return { type: "interval", interval: 1, unit: "day" };
2301
- }
2302
- if (normalized === "every week" || normalized === "weekly") {
2303
- return { type: "interval", interval: 1, unit: "week" };
2304
- }
2305
- if (normalized === "every month" || normalized === "monthly") {
2306
- return { type: "interval", interval: 1, unit: "month" };
2307
- }
2308
- const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
2309
- if (intervalMatch) {
2310
- return {
2311
- type: "interval",
2312
- interval: parseInt(intervalMatch[1], 10),
2313
- unit: intervalMatch[2]
2314
- };
2315
- }
2316
- const daysMatch = normalized.match(/^every\s+(.+)$/);
2317
- if (daysMatch) {
2318
- const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
2319
- const days = [];
2320
- for (const part of dayParts) {
2321
- const dayNum = DAY_NAMES[part];
2322
- if (dayNum !== undefined) {
2323
- days.push(dayNum);
2324
- }
2325
- }
2326
- if (days.length > 0) {
2327
- return { type: "specific_days", days: days.sort((a, b) => a - b) };
2328
- }
2329
- }
2330
- 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"`);
2331
- }
2332
- function isValidRecurrenceRule(rule) {
2333
- try {
2334
- parseRecurrenceRule(rule);
2335
- return true;
2336
- } catch {
2337
- return false;
2338
- }
2339
- }
2340
- function nextOccurrence(rule, from) {
2341
- const parsed = parseRecurrenceRule(rule);
2342
- const base = from || new Date;
2343
- if (parsed.type === "interval") {
2344
- const next = new Date(base);
2345
- if (parsed.unit === "day") {
2346
- next.setDate(next.getDate() + parsed.interval);
2347
- } else if (parsed.unit === "week") {
2348
- next.setDate(next.getDate() + parsed.interval * 7);
2349
- } else if (parsed.unit === "month") {
2350
- next.setMonth(next.getMonth() + parsed.interval);
2639
+ }
2640
+ if (agent && config.max_completions_per_window && config.window_minutes) {
2641
+ const windowStart = new Date(Date.now() - config.window_minutes * 60 * 1000).toISOString();
2642
+ const result = db.query(`SELECT COUNT(*) as count FROM tasks
2643
+ WHERE completed_at > ? AND (assigned_to = ? OR agent_id = ?)`).get(windowStart, agent, agent);
2644
+ if (result.count >= config.max_completions_per_window) {
2645
+ throw new CompletionGuardError(`Rate limit: ${result.count} tasks completed in the last ${config.window_minutes} minutes (max ${config.max_completions_per_window}). Slow down.`);
2351
2646
  }
2352
- return next.toISOString();
2353
2647
  }
2354
- if (parsed.type === "specific_days") {
2355
- const currentDay = base.getDay();
2356
- const days = parsed.days;
2357
- let daysToAdd = Infinity;
2358
- for (const day of days) {
2359
- let diff = day - currentDay;
2360
- if (diff <= 0)
2361
- diff += 7;
2362
- if (diff < daysToAdd)
2363
- daysToAdd = diff;
2648
+ if (agent && config.cooldown_seconds) {
2649
+ const result = db.query(`SELECT MAX(completed_at) as last_completed FROM tasks
2650
+ WHERE completed_at IS NOT NULL AND (assigned_to = ? OR agent_id = ?) AND id != ?`).get(agent, agent, task.id);
2651
+ if (result.last_completed) {
2652
+ const elapsedSeconds = (Date.now() - new Date(result.last_completed).getTime()) / 1000;
2653
+ if (elapsedSeconds < config.cooldown_seconds) {
2654
+ const remaining = Math.ceil(config.cooldown_seconds - elapsedSeconds);
2655
+ throw new CompletionGuardError(`Cooldown: last completion was ${Math.floor(elapsedSeconds)}s ago. Wait ${remaining}s between completions.`, remaining);
2656
+ }
2364
2657
  }
2365
- const next = new Date(base);
2366
- next.setDate(next.getDate() + daysToAdd);
2367
- return next.toISOString();
2368
2658
  }
2369
- throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
2659
+ }
2660
+
2661
+ // src/db/audit.ts
2662
+ init_database();
2663
+ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
2664
+ const d = db || getDatabase();
2665
+ const id = uuid();
2666
+ const timestamp = now();
2667
+ d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
2668
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
2669
+ return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
2670
+ }
2671
+ function getTaskHistory(taskId, db) {
2672
+ const d = db || getDatabase();
2673
+ return d.query("SELECT * FROM task_history WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
2674
+ }
2675
+ function getRecentActivity(limit = 50, db) {
2676
+ const d = db || getDatabase();
2677
+ return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
2678
+ }
2679
+ function getRecap(hours = 8, projectId, db) {
2680
+ const d = db || getDatabase();
2681
+ const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
2682
+ const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
2683
+ const pf = projectId ? " AND project_id = ?" : "";
2684
+ const tpf = projectId ? " AND t.project_id = ?" : "";
2685
+ 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);
2686
+ 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);
2687
+ 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();
2688
+ 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();
2689
+ 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);
2690
+ 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);
2691
+ return {
2692
+ hours,
2693
+ since,
2694
+ completed: completed.map((r) => ({
2695
+ ...r,
2696
+ duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
2697
+ })),
2698
+ created,
2699
+ in_progress,
2700
+ blocked,
2701
+ stale,
2702
+ agents
2703
+ };
2370
2704
  }
2371
2705
 
2372
2706
  // src/db/webhooks.ts
2373
2707
  init_database();
2374
2708
  var MAX_RETRY_ATTEMPTS = 3;
2375
2709
  var RETRY_BASE_DELAY_MS = 1000;
2710
+ function isPrivateOrInternal(ip) {
2711
+ const parts = ip.split(".").map(Number);
2712
+ if (parts.length !== 4)
2713
+ return true;
2714
+ const a = parts[0];
2715
+ const b = parts[1];
2716
+ if (parts.some((p) => isNaN(p) || p < 0 || p > 255))
2717
+ return true;
2718
+ if (a === 10)
2719
+ return true;
2720
+ if (a === 127)
2721
+ return true;
2722
+ if (a === 169 && b === 254)
2723
+ return true;
2724
+ if (a === 172 && b >= 16 && b <= 31)
2725
+ return true;
2726
+ if (a === 192 && b === 168)
2727
+ return true;
2728
+ if (a === 0)
2729
+ return true;
2730
+ return false;
2731
+ }
2376
2732
  function validateWebhookUrl(urlString) {
2377
2733
  try {
2378
2734
  const url = new URL(urlString);
2379
2735
  if (url.protocol !== "https:") {
2380
- throw new Error("Webhook URLs must use HTTPS");
2736
+ return { valid: false, error: "Webhook URLs must use HTTPS" };
2381
2737
  }
2382
2738
  const hostname = url.hostname.toLowerCase();
2383
2739
  if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0") {
2384
- throw new Error("Webhook URLs cannot target localhost");
2740
+ return { valid: false, error: "Webhook URLs cannot target localhost" };
2385
2741
  }
2386
2742
  if (hostname === "169.254.169.254" || hostname.startsWith("169.254.")) {
2387
- throw new Error("Webhook URLs cannot target cloud metadata endpoints");
2743
+ return { valid: false, error: "Webhook URLs cannot target cloud metadata endpoints" };
2388
2744
  }
2389
2745
  const privateRanges = [
2390
2746
  /^10\./,
@@ -2397,1008 +2753,1308 @@ function validateWebhookUrl(urlString) {
2397
2753
  ];
2398
2754
  for (const range of privateRanges) {
2399
2755
  if (range.test(hostname)) {
2400
- throw new Error("Webhook URLs cannot target private IP ranges");
2756
+ return { valid: false, error: "Webhook URLs cannot target private IP ranges" };
2401
2757
  }
2402
2758
  }
2759
+ return { valid: true };
2403
2760
  } catch (e) {
2404
2761
  if (e instanceof Error && e.message.startsWith("Webhook URLs")) {
2405
- throw e;
2762
+ return { valid: false, error: e.message };
2763
+ }
2764
+ return { valid: false, error: `Invalid webhook URL: ${urlString}` };
2765
+ }
2766
+ }
2767
+ async function resolveAndCheckIp(hostname) {
2768
+ try {
2769
+ const resolved = await Bun.dns.lookup(hostname);
2770
+ if (!resolved)
2771
+ return { allowed: false, error: `Could not resolve hostname: ${hostname}` };
2772
+ const addresses = Array.isArray(resolved) ? resolved : [resolved];
2773
+ for (const addr of addresses) {
2774
+ const ip = typeof addr === "string" ? addr : addr.address;
2775
+ if (isPrivateOrInternal(ip)) {
2776
+ return { allowed: false, error: `Hostname ${hostname} resolves to blocked address ${ip}` };
2777
+ }
2778
+ }
2779
+ const first = addresses[0];
2780
+ return { allowed: true, ip: typeof first === "string" ? first : first?.address ?? "" };
2781
+ } catch {
2782
+ return { allowed: true, ip: "" };
2783
+ }
2784
+ }
2785
+ var activeDeliveries = 0;
2786
+ var MAX_CONCURRENT_DELIVERIES = 20;
2787
+ function rowToWebhook(row) {
2788
+ return {
2789
+ ...row,
2790
+ events: JSON.parse(row.events || "[]"),
2791
+ active: !!row.active,
2792
+ project_id: row.project_id || null,
2793
+ task_list_id: row.task_list_id || null,
2794
+ agent_id: row.agent_id || null,
2795
+ task_id: row.task_id || null
2796
+ };
2797
+ }
2798
+ function createWebhook(input, db) {
2799
+ const urlValidation = validateWebhookUrl(input.url);
2800
+ if (!urlValidation.valid) {
2801
+ throw new Error(`Invalid webhook URL: ${urlValidation.error}`);
2802
+ }
2803
+ const d = db || getDatabase();
2804
+ const id = uuid();
2805
+ d.run(`INSERT INTO webhooks (id, url, events, secret, project_id, task_list_id, agent_id, task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2806
+ id,
2807
+ input.url,
2808
+ JSON.stringify(input.events || []),
2809
+ input.secret || null,
2810
+ input.project_id || null,
2811
+ input.task_list_id || null,
2812
+ input.agent_id || null,
2813
+ input.task_id || null,
2814
+ now()
2815
+ ]);
2816
+ return getWebhook(id, d);
2817
+ }
2818
+ function getWebhook(id, db) {
2819
+ const d = db || getDatabase();
2820
+ const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
2821
+ return row ? rowToWebhook(row) : null;
2822
+ }
2823
+ function listWebhooks(db) {
2824
+ const d = db || getDatabase();
2825
+ return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
2826
+ }
2827
+ function deleteWebhook(id, db) {
2828
+ const d = db || getDatabase();
2829
+ return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
2830
+ }
2831
+ function listDeliveries(webhookId, limit = 50, db) {
2832
+ const d = db || getDatabase();
2833
+ if (webhookId) {
2834
+ return d.query("SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY created_at DESC LIMIT ?").all(webhookId, limit);
2835
+ }
2836
+ return d.query("SELECT * FROM webhook_deliveries ORDER BY created_at DESC LIMIT ?").all(limit);
2837
+ }
2838
+ function logDelivery(d, webhookId, event, payload, statusCode, response, attempt) {
2839
+ const id = uuid();
2840
+ 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()]);
2841
+ }
2842
+ function matchesScope(wh, payload) {
2843
+ if (wh.project_id && payload.project_id !== wh.project_id)
2844
+ return false;
2845
+ if (wh.task_list_id && payload.task_list_id !== wh.task_list_id)
2846
+ return false;
2847
+ if (wh.agent_id && payload.agent_id !== wh.agent_id && payload.assigned_to !== wh.agent_id)
2848
+ return false;
2849
+ if (wh.task_id && payload.id !== wh.task_id)
2850
+ return false;
2851
+ return true;
2852
+ }
2853
+ async function deliverWebhook(wh, event, body, attempt, db) {
2854
+ try {
2855
+ const url = new URL(wh.url);
2856
+ const hostname = url.hostname.toLowerCase();
2857
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
2858
+ logDelivery(db, wh.id, event, body, null, "Blocked: webhook URL points to localhost", attempt);
2859
+ return;
2860
+ }
2861
+ const ipCheck = await resolveAndCheckIp(hostname);
2862
+ if (!ipCheck.allowed) {
2863
+ logDelivery(db, wh.id, event, body, null, `Blocked: ${ipCheck.error}`, attempt);
2864
+ return;
2865
+ }
2866
+ } catch {
2867
+ logDelivery(db, wh.id, event, body, null, `Invalid URL at delivery time: ${wh.url}`, attempt);
2868
+ return;
2869
+ }
2870
+ if (activeDeliveries >= MAX_CONCURRENT_DELIVERIES) {
2871
+ logDelivery(db, wh.id, event, body, null, "Dropped: too many concurrent deliveries", attempt);
2872
+ return;
2873
+ }
2874
+ activeDeliveries++;
2875
+ try {
2876
+ const headers = { "Content-Type": "application/json" };
2877
+ if (wh.secret) {
2878
+ const encoder = new TextEncoder;
2879
+ const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
2880
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
2881
+ headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
2882
+ }
2883
+ const resp = await fetch(wh.url, { method: "POST", headers, body });
2884
+ const respText = await resp.text().catch(() => "");
2885
+ logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
2886
+ if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
2887
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
2888
+ setTimeout(() => {
2889
+ deliverWebhook(wh, event, body, attempt + 1, db).catch((retryErr) => {
2890
+ console.error(`[webhook] Retry failed for webhook ${wh.id}:`, retryErr);
2891
+ });
2892
+ }, delay);
2893
+ }
2894
+ } catch (err) {
2895
+ const errorMsg = err instanceof Error ? err.message : String(err);
2896
+ logDelivery(db, wh.id, event, body, null, errorMsg.slice(0, 1000), attempt);
2897
+ console.error(`[webhook] Delivery failed for webhook ${wh.id} (attempt ${attempt}):`, errorMsg);
2898
+ if (attempt < MAX_RETRY_ATTEMPTS) {
2899
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
2900
+ setTimeout(() => {
2901
+ deliverWebhook(wh, event, body, attempt + 1, db).catch((retryErr) => {
2902
+ console.error(`[webhook] Retry failed for webhook ${wh.id}:`, retryErr);
2903
+ });
2904
+ }, delay);
2406
2905
  }
2407
- throw new Error(`Invalid webhook URL: ${urlString}`);
2906
+ } finally {
2907
+ activeDeliveries--;
2908
+ }
2909
+ }
2910
+ async function dispatchWebhook(event, payload, db) {
2911
+ const d = db || getDatabase();
2912
+ const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
2913
+ const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
2914
+ for (const wh of webhooks) {
2915
+ if (!matchesScope(wh, payloadObj))
2916
+ continue;
2917
+ const body = JSON.stringify({ event, payload, timestamp: now() });
2918
+ deliverWebhook(wh, event, body, 1, d).catch((err) => {
2919
+ console.error(`[webhook] Dispatch failed for webhook ${wh.id}:`, err);
2920
+ });
2921
+ }
2922
+ }
2923
+
2924
+ // src/db/checklists.ts
2925
+ init_database();
2926
+ function rowToItem(row) {
2927
+ return { ...row, checked: !!row.checked };
2928
+ }
2929
+ function getChecklist(taskId, db) {
2930
+ const d = db || getDatabase();
2931
+ const rows = d.query("SELECT * FROM task_checklists WHERE task_id = ? ORDER BY position, created_at").all(taskId);
2932
+ return rows.map(rowToItem);
2933
+ }
2934
+ function addChecklistItem(input, db) {
2935
+ const d = db || getDatabase();
2936
+ const id = uuid();
2937
+ const timestamp = now();
2938
+ let position = input.position;
2939
+ if (position === undefined) {
2940
+ const maxRow = d.query("SELECT MAX(position) as max_pos FROM task_checklists WHERE task_id = ?").get(input.task_id);
2941
+ position = (maxRow?.max_pos ?? -1) + 1;
2408
2942
  }
2943
+ d.run("INSERT INTO task_checklists (id, task_id, position, text, checked, created_at, updated_at) VALUES (?, ?, ?, ?, 0, ?, ?)", [id, input.task_id, position, input.text, timestamp, timestamp]);
2944
+ return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
2945
+ }
2946
+ function checkChecklistItem(id, checked, db) {
2947
+ const d = db || getDatabase();
2948
+ const timestamp = now();
2949
+ const result = d.run("UPDATE task_checklists SET checked = ?, updated_at = ? WHERE id = ?", [checked ? 1 : 0, timestamp, id]);
2950
+ if (result.changes === 0)
2951
+ return null;
2952
+ return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
2953
+ }
2954
+ function updateChecklistItemText(id, text, db) {
2955
+ const d = db || getDatabase();
2956
+ const timestamp = now();
2957
+ const result = d.run("UPDATE task_checklists SET text = ?, updated_at = ? WHERE id = ?", [text, timestamp, id]);
2958
+ if (result.changes === 0)
2959
+ return null;
2960
+ return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
2961
+ }
2962
+ function removeChecklistItem(id, db) {
2963
+ const d = db || getDatabase();
2964
+ const result = d.run("DELETE FROM task_checklists WHERE id = ?", [id]);
2965
+ return result.changes > 0;
2966
+ }
2967
+ function clearChecklist(taskId, db) {
2968
+ const d = db || getDatabase();
2969
+ const result = d.run("DELETE FROM task_checklists WHERE task_id = ?", [taskId]);
2970
+ return result.changes;
2971
+ }
2972
+ function getChecklistStats(taskId, db) {
2973
+ const d = db || getDatabase();
2974
+ const row = d.query("SELECT COUNT(*) as total, SUM(checked) as checked FROM task_checklists WHERE task_id = ?").get(taskId);
2975
+ return { total: row?.total ?? 0, checked: row?.checked ?? 0 };
2409
2976
  }
2410
- function rowToWebhook(row) {
2977
+
2978
+ // src/db/task-crud.ts
2979
+ function rowToTask(row) {
2411
2980
  return {
2412
2981
  ...row,
2413
- events: JSON.parse(row.events || "[]"),
2414
- active: !!row.active,
2415
- project_id: row.project_id || null,
2416
- task_list_id: row.task_list_id || null,
2417
- agent_id: row.agent_id || null,
2418
- task_id: row.task_id || null
2982
+ tags: JSON.parse(row.tags || "[]"),
2983
+ metadata: JSON.parse(row.metadata || "{}"),
2984
+ status: row.status,
2985
+ priority: row.priority,
2986
+ requires_approval: !!row.requires_approval
2419
2987
  };
2420
2988
  }
2421
- function createWebhook(input, db) {
2422
- const d = db || getDatabase();
2423
- validateWebhookUrl(input.url);
2424
- const id = uuid();
2425
- d.run(`INSERT INTO webhooks (id, url, events, secret, project_id, task_list_id, agent_id, task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2426
- id,
2427
- input.url,
2428
- JSON.stringify(input.events || []),
2429
- input.secret || null,
2430
- input.project_id || null,
2431
- input.task_list_id || null,
2432
- input.agent_id || null,
2433
- input.task_id || null,
2434
- now()
2435
- ]);
2436
- return getWebhook(id, d);
2989
+ function insertTaskTags(taskId, tags, db) {
2990
+ if (tags.length === 0)
2991
+ return;
2992
+ const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
2993
+ for (const tag of tags) {
2994
+ if (tag)
2995
+ stmt.run(taskId, tag);
2996
+ }
2437
2997
  }
2438
- function getWebhook(id, db) {
2998
+ function replaceTaskTags(taskId, tags, db) {
2999
+ db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
3000
+ insertTaskTags(taskId, tags, db);
3001
+ }
3002
+ function createTask(input, db) {
2439
3003
  const d = db || getDatabase();
2440
- const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
2441
- return row ? rowToWebhook(row) : null;
3004
+ const timestamp = now();
3005
+ const tags = input.tags || [];
3006
+ const assignedBy = input.assigned_by || input.agent_id;
3007
+ const assignedFromProject = input.assigned_from_project || null;
3008
+ let id = uuid();
3009
+ for (let attempt = 0;attempt < 3; attempt++) {
3010
+ try {
3011
+ 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)
3012
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3013
+ id,
3014
+ null,
3015
+ input.project_id || null,
3016
+ input.parent_id || null,
3017
+ input.plan_id || null,
3018
+ input.task_list_id || null,
3019
+ input.title,
3020
+ input.description || null,
3021
+ input.status || "pending",
3022
+ input.priority || "medium",
3023
+ input.agent_id || null,
3024
+ input.assigned_to || null,
3025
+ input.session_id || null,
3026
+ input.working_dir || null,
3027
+ JSON.stringify(tags),
3028
+ JSON.stringify(input.metadata || {}),
3029
+ timestamp,
3030
+ timestamp,
3031
+ input.due_at || null,
3032
+ input.estimated_minutes || null,
3033
+ input.requires_approval ? 1 : 0,
3034
+ null,
3035
+ null,
3036
+ input.recurrence_rule || null,
3037
+ input.recurrence_parent_id || null,
3038
+ input.spawns_template_id || null,
3039
+ input.reason || null,
3040
+ input.spawned_from_session || null,
3041
+ assignedBy || null,
3042
+ assignedFromProject || null,
3043
+ input.task_type || null
3044
+ ]);
3045
+ break;
3046
+ } catch (e) {
3047
+ if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
3048
+ id = uuid();
3049
+ continue;
3050
+ }
3051
+ throw e;
3052
+ }
3053
+ }
3054
+ if (tags.length > 0) {
3055
+ insertTaskTags(id, tags, d);
3056
+ }
3057
+ const task = getTask(id, d);
3058
+ 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(() => {});
3059
+ return task;
2442
3060
  }
2443
- function listWebhooks(db) {
3061
+ function getTask(id, db) {
2444
3062
  const d = db || getDatabase();
2445
- return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
3063
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
3064
+ if (!row)
3065
+ return null;
3066
+ return rowToTask(row);
2446
3067
  }
2447
- function deleteWebhook(id, db) {
3068
+ function getTaskWithRelations(id, db) {
2448
3069
  const d = db || getDatabase();
2449
- return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
3070
+ const task = getTask(id, d);
3071
+ if (!task)
3072
+ return null;
3073
+ const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
3074
+ const subtasks = subtaskRows.map(rowToTask);
3075
+ const depRows = d.query(`SELECT t.* FROM tasks t
3076
+ JOIN task_dependencies td ON td.depends_on = t.id
3077
+ WHERE td.task_id = ?`).all(id);
3078
+ const dependencies = depRows.map(rowToTask);
3079
+ const blockedByRows = d.query(`SELECT t.* FROM tasks t
3080
+ JOIN task_dependencies td ON td.task_id = t.id
3081
+ WHERE td.depends_on = ?`).all(id);
3082
+ const blocked_by = blockedByRows.map(rowToTask);
3083
+ const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
3084
+ const parent = task.parent_id ? getTask(task.parent_id, d) : null;
3085
+ const checklist = getChecklist(id, d);
3086
+ return {
3087
+ ...task,
3088
+ subtasks,
3089
+ dependencies,
3090
+ blocked_by,
3091
+ comments,
3092
+ parent,
3093
+ checklist
3094
+ };
2450
3095
  }
2451
- function listDeliveries(webhookId, limit = 50, db) {
3096
+ function listTasks(filter = {}, db) {
2452
3097
  const d = db || getDatabase();
2453
- if (webhookId) {
2454
- return d.query("SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY created_at DESC LIMIT ?").all(webhookId, limit);
3098
+ const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
3099
+ clearExpiredLocks2(d);
3100
+ const conditions = [];
3101
+ const params = [];
3102
+ if (filter.project_id) {
3103
+ conditions.push("project_id = ?");
3104
+ params.push(filter.project_id);
2455
3105
  }
2456
- return d.query("SELECT * FROM webhook_deliveries ORDER BY created_at DESC LIMIT ?").all(limit);
2457
- }
2458
- function logDelivery(d, webhookId, event, payload, statusCode, response, attempt) {
2459
- const id = uuid();
2460
- 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()]);
2461
- }
2462
- function matchesScope(wh, payload) {
2463
- if (wh.project_id && payload.project_id !== wh.project_id)
2464
- return false;
2465
- if (wh.task_list_id && payload.task_list_id !== wh.task_list_id)
2466
- return false;
2467
- if (wh.agent_id && payload.agent_id !== wh.agent_id && payload.assigned_to !== wh.agent_id)
2468
- return false;
2469
- if (wh.task_id && payload.id !== wh.task_id)
2470
- return false;
2471
- return true;
2472
- }
2473
- async function deliverWebhook(wh, event, body, attempt, db) {
2474
- try {
2475
- const headers = { "Content-Type": "application/json" };
2476
- if (wh.secret) {
2477
- const encoder = new TextEncoder;
2478
- const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
2479
- const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
2480
- headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
3106
+ if (filter.ids && filter.ids.length > 0) {
3107
+ conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
3108
+ params.push(...filter.ids);
3109
+ }
3110
+ if (filter.parent_id !== undefined) {
3111
+ if (filter.parent_id === null) {
3112
+ conditions.push("parent_id IS NULL");
3113
+ } else {
3114
+ conditions.push("parent_id = ?");
3115
+ params.push(filter.parent_id);
2481
3116
  }
2482
- const resp = await fetch(wh.url, { method: "POST", headers, body });
2483
- const respText = await resp.text().catch(() => "");
2484
- logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
2485
- if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
2486
- const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
2487
- setTimeout(() => {
2488
- deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
2489
- }, delay);
3117
+ }
3118
+ if (filter.status) {
3119
+ if (Array.isArray(filter.status)) {
3120
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
3121
+ params.push(...filter.status);
3122
+ } else {
3123
+ conditions.push("status = ?");
3124
+ params.push(filter.status);
2490
3125
  }
2491
- } catch (err) {
2492
- const errorMsg = err instanceof Error ? err.message : String(err);
2493
- logDelivery(db, wh.id, event, body, null, errorMsg.slice(0, 1000), attempt);
2494
- if (attempt < MAX_RETRY_ATTEMPTS) {
2495
- const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
2496
- setTimeout(() => {
2497
- deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
2498
- }, delay);
3126
+ }
3127
+ if (filter.priority) {
3128
+ if (Array.isArray(filter.priority)) {
3129
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
3130
+ params.push(...filter.priority);
3131
+ } else {
3132
+ conditions.push("priority = ?");
3133
+ params.push(filter.priority);
3134
+ }
3135
+ }
3136
+ if (filter.assigned_to) {
3137
+ conditions.push("assigned_to = ?");
3138
+ params.push(filter.assigned_to);
3139
+ }
3140
+ if (filter.agent_id) {
3141
+ conditions.push("agent_id = ?");
3142
+ params.push(filter.agent_id);
3143
+ }
3144
+ if (filter.session_id) {
3145
+ conditions.push("session_id = ?");
3146
+ params.push(filter.session_id);
3147
+ }
3148
+ if (filter.tags && filter.tags.length > 0) {
3149
+ const placeholders = filter.tags.map(() => "?").join(",");
3150
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
3151
+ params.push(...filter.tags);
3152
+ }
3153
+ if (filter.plan_id) {
3154
+ conditions.push("plan_id = ?");
3155
+ params.push(filter.plan_id);
3156
+ }
3157
+ if (filter.task_list_id) {
3158
+ conditions.push("task_list_id = ?");
3159
+ params.push(filter.task_list_id);
3160
+ }
3161
+ if (filter.has_recurrence === true) {
3162
+ conditions.push("recurrence_rule IS NOT NULL");
3163
+ } else if (filter.has_recurrence === false) {
3164
+ conditions.push("recurrence_rule IS NULL");
3165
+ }
3166
+ if (filter.task_type) {
3167
+ if (Array.isArray(filter.task_type)) {
3168
+ conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
3169
+ params.push(...filter.task_type);
3170
+ } else {
3171
+ conditions.push("task_type = ?");
3172
+ params.push(filter.task_type);
3173
+ }
3174
+ }
3175
+ const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
3176
+ if (filter.cursor) {
3177
+ try {
3178
+ const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
3179
+ conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
3180
+ params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
3181
+ } catch {}
3182
+ }
3183
+ if (!filter.include_archived) {
3184
+ conditions.push("archived_at IS NULL");
3185
+ }
3186
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3187
+ let limitClause = "";
3188
+ if (filter.limit) {
3189
+ limitClause = " LIMIT ?";
3190
+ params.push(filter.limit);
3191
+ if (!filter.cursor && filter.offset) {
3192
+ limitClause += " OFFSET ?";
3193
+ params.push(filter.offset);
2499
3194
  }
2500
3195
  }
3196
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
3197
+ return rows.map(rowToTask);
2501
3198
  }
2502
- async function dispatchWebhook(event, payload, db) {
3199
+ function countTasks(filter = {}, db) {
2503
3200
  const d = db || getDatabase();
2504
- const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
2505
- const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
2506
- for (const wh of webhooks) {
2507
- if (!matchesScope(wh, payloadObj))
2508
- continue;
2509
- const body = JSON.stringify({ event, payload, timestamp: now() });
2510
- deliverWebhook(wh, event, body, 1, d).catch(() => {});
3201
+ const conditions = [];
3202
+ const params = [];
3203
+ if (filter.project_id) {
3204
+ conditions.push("project_id = ?");
3205
+ params.push(filter.project_id);
2511
3206
  }
2512
- }
2513
-
2514
- // src/db/templates.ts
2515
- init_database();
2516
- function rowToTemplate(row) {
2517
- return {
2518
- ...row,
2519
- tags: JSON.parse(row.tags || "[]"),
2520
- variables: JSON.parse(row.variables || "[]"),
2521
- metadata: JSON.parse(row.metadata || "{}"),
2522
- priority: row.priority || "medium",
2523
- version: row.version ?? 1
2524
- };
2525
- }
2526
- function rowToTemplateTask(row) {
2527
- return {
2528
- ...row,
2529
- tags: JSON.parse(row.tags || "[]"),
2530
- depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
2531
- metadata: JSON.parse(row.metadata || "{}"),
2532
- priority: row.priority || "medium",
2533
- condition: row.condition ?? null,
2534
- include_template_id: row.include_template_id ?? null
2535
- };
2536
- }
2537
- function resolveTemplateId(id, d) {
2538
- return resolvePartialId(d, "task_templates", id);
2539
- }
2540
- function createTemplate(input, db) {
2541
- const d = db || getDatabase();
2542
- const id = uuid();
2543
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
2544
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2545
- id,
2546
- input.name,
2547
- input.title_pattern,
2548
- input.description || null,
2549
- input.priority || "medium",
2550
- JSON.stringify(input.tags || []),
2551
- JSON.stringify(input.variables || []),
2552
- input.project_id || null,
2553
- input.plan_id || null,
2554
- JSON.stringify(input.metadata || {}),
2555
- now()
2556
- ]);
2557
- if (input.tasks && input.tasks.length > 0) {
2558
- addTemplateTasks(id, input.tasks, d);
3207
+ if (filter.ids && filter.ids.length > 0) {
3208
+ conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
3209
+ params.push(...filter.ids);
2559
3210
  }
2560
- return getTemplate(id, d);
2561
- }
2562
- function getTemplate(id, db) {
2563
- const d = db || getDatabase();
2564
- const resolved = resolveTemplateId(id, d);
2565
- if (!resolved)
2566
- return null;
2567
- const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
2568
- return row ? rowToTemplate(row) : null;
2569
- }
2570
- function listTemplates(db) {
2571
- const d = db || getDatabase();
2572
- return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
2573
- }
2574
- function deleteTemplate(id, db) {
2575
- const d = db || getDatabase();
2576
- const resolved = resolveTemplateId(id, d);
2577
- if (!resolved)
2578
- return false;
2579
- return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
3211
+ if (filter.parent_id !== undefined) {
3212
+ if (filter.parent_id === null) {
3213
+ conditions.push("parent_id IS NULL");
3214
+ } else {
3215
+ conditions.push("parent_id = ?");
3216
+ params.push(filter.parent_id);
3217
+ }
3218
+ }
3219
+ if (filter.status) {
3220
+ if (Array.isArray(filter.status)) {
3221
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
3222
+ params.push(...filter.status);
3223
+ } else {
3224
+ conditions.push("status = ?");
3225
+ params.push(filter.status);
3226
+ }
3227
+ }
3228
+ if (filter.priority) {
3229
+ if (Array.isArray(filter.priority)) {
3230
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
3231
+ params.push(...filter.priority);
3232
+ } else {
3233
+ conditions.push("priority = ?");
3234
+ params.push(filter.priority);
3235
+ }
3236
+ }
3237
+ if (filter.assigned_to) {
3238
+ conditions.push("assigned_to = ?");
3239
+ params.push(filter.assigned_to);
3240
+ }
3241
+ if (filter.agent_id) {
3242
+ conditions.push("agent_id = ?");
3243
+ params.push(filter.agent_id);
3244
+ }
3245
+ if (filter.session_id) {
3246
+ conditions.push("session_id = ?");
3247
+ params.push(filter.session_id);
3248
+ }
3249
+ if (filter.tags && filter.tags.length > 0) {
3250
+ const placeholders = filter.tags.map(() => "?").join(",");
3251
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
3252
+ params.push(...filter.tags);
3253
+ }
3254
+ if (filter.plan_id) {
3255
+ conditions.push("plan_id = ?");
3256
+ params.push(filter.plan_id);
3257
+ }
3258
+ if (filter.task_list_id) {
3259
+ conditions.push("task_list_id = ?");
3260
+ params.push(filter.task_list_id);
3261
+ }
3262
+ if (!filter.include_archived) {
3263
+ conditions.push("archived_at IS NULL");
3264
+ }
3265
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3266
+ const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
3267
+ return row.count;
2580
3268
  }
2581
- function updateTemplate(id, updates, db) {
3269
+ function updateTask(id, input, db) {
2582
3270
  const d = db || getDatabase();
2583
- const resolved = resolveTemplateId(id, d);
2584
- if (!resolved)
2585
- return null;
2586
- const current = getTemplateWithTasks(resolved, d);
2587
- if (current) {
2588
- const snapshot = JSON.stringify({
2589
- name: current.name,
2590
- title_pattern: current.title_pattern,
2591
- description: current.description,
2592
- priority: current.priority,
2593
- tags: current.tags,
2594
- variables: current.variables,
2595
- project_id: current.project_id,
2596
- plan_id: current.plan_id,
2597
- metadata: current.metadata,
2598
- tasks: current.tasks
2599
- });
2600
- d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
2601
- }
2602
- const sets = ["version = version + 1"];
2603
- const values = [];
2604
- if (updates.name !== undefined) {
2605
- sets.push("name = ?");
2606
- values.push(updates.name);
3271
+ const task = getTask(id, d);
3272
+ if (!task)
3273
+ throw new TaskNotFoundError(id);
3274
+ if (task.version !== input.version) {
3275
+ throw new VersionConflictError(id, input.version, task.version);
2607
3276
  }
2608
- if (updates.title_pattern !== undefined) {
2609
- sets.push("title_pattern = ?");
2610
- values.push(updates.title_pattern);
3277
+ const sets = ["version = version + 1", "updated_at = ?"];
3278
+ const params = [now()];
3279
+ if (input.title !== undefined) {
3280
+ sets.push("title = ?");
3281
+ params.push(input.title);
2611
3282
  }
2612
- if (updates.description !== undefined) {
3283
+ if (input.description !== undefined) {
2613
3284
  sets.push("description = ?");
2614
- values.push(updates.description);
3285
+ params.push(input.description);
2615
3286
  }
2616
- if (updates.priority !== undefined) {
3287
+ if (input.status !== undefined) {
3288
+ if (input.status === "completed") {
3289
+ checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
3290
+ }
3291
+ sets.push("status = ?");
3292
+ params.push(input.status);
3293
+ if (input.status === "completed") {
3294
+ sets.push("completed_at = ?");
3295
+ params.push(now());
3296
+ }
3297
+ }
3298
+ if (input.priority !== undefined) {
2617
3299
  sets.push("priority = ?");
2618
- values.push(updates.priority);
3300
+ params.push(input.priority);
2619
3301
  }
2620
- if (updates.tags !== undefined) {
3302
+ if (input.assigned_to !== undefined) {
3303
+ sets.push("assigned_to = ?");
3304
+ params.push(input.assigned_to);
3305
+ }
3306
+ if (input.tags !== undefined) {
2621
3307
  sets.push("tags = ?");
2622
- values.push(JSON.stringify(updates.tags));
3308
+ params.push(JSON.stringify(input.tags));
3309
+ }
3310
+ if (input.metadata !== undefined) {
3311
+ sets.push("metadata = ?");
3312
+ params.push(JSON.stringify(input.metadata));
3313
+ }
3314
+ if (input.plan_id !== undefined) {
3315
+ sets.push("plan_id = ?");
3316
+ params.push(input.plan_id);
3317
+ }
3318
+ if (input.task_list_id !== undefined) {
3319
+ sets.push("task_list_id = ?");
3320
+ params.push(input.task_list_id);
3321
+ }
3322
+ if (input.due_at !== undefined) {
3323
+ sets.push("due_at = ?");
3324
+ params.push(input.due_at);
3325
+ }
3326
+ if (input.estimated_minutes !== undefined) {
3327
+ sets.push("estimated_minutes = ?");
3328
+ params.push(input.estimated_minutes);
3329
+ }
3330
+ if (input.requires_approval !== undefined) {
3331
+ sets.push("requires_approval = ?");
3332
+ params.push(input.requires_approval ? 1 : 0);
3333
+ }
3334
+ if (input.approved_by !== undefined) {
3335
+ sets.push("approved_by = ?");
3336
+ params.push(input.approved_by);
3337
+ sets.push("approved_at = ?");
3338
+ params.push(now());
3339
+ }
3340
+ if (input.recurrence_rule !== undefined) {
3341
+ sets.push("recurrence_rule = ?");
3342
+ params.push(input.recurrence_rule);
3343
+ }
3344
+ if (input.task_type !== undefined) {
3345
+ sets.push("task_type = ?");
3346
+ params.push(input.task_type ?? null);
2623
3347
  }
2624
- if (updates.variables !== undefined) {
2625
- sets.push("variables = ?");
2626
- values.push(JSON.stringify(updates.variables));
3348
+ params.push(id, input.version);
3349
+ const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
3350
+ if (result.changes === 0) {
3351
+ const current = getTask(id, d);
3352
+ throw new VersionConflictError(id, input.version, current?.version ?? -1);
2627
3353
  }
2628
- if (updates.project_id !== undefined) {
2629
- sets.push("project_id = ?");
2630
- values.push(updates.project_id);
3354
+ if (input.tags !== undefined) {
3355
+ replaceTaskTags(id, input.tags, d);
2631
3356
  }
2632
- if (updates.plan_id !== undefined) {
2633
- sets.push("plan_id = ?");
2634
- values.push(updates.plan_id);
3357
+ const agentId = task.assigned_to || task.agent_id || null;
3358
+ if (input.status !== undefined && input.status !== task.status)
3359
+ logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
3360
+ if (input.priority !== undefined && input.priority !== task.priority)
3361
+ logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
3362
+ if (input.title !== undefined && input.title !== task.title)
3363
+ logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
3364
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
3365
+ logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
3366
+ if (input.approved_by !== undefined)
3367
+ logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
3368
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
3369
+ dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
2635
3370
  }
2636
- if (updates.metadata !== undefined) {
2637
- sets.push("metadata = ?");
2638
- values.push(JSON.stringify(updates.metadata));
3371
+ if (input.status !== undefined && input.status !== task.status) {
3372
+ dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
2639
3373
  }
2640
- values.push(resolved);
2641
- d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
2642
- return getTemplate(resolved, d);
2643
- }
2644
- function taskFromTemplate(templateId, overrides = {}, db) {
2645
- const t = getTemplate(templateId, db);
2646
- if (!t)
2647
- throw new Error(`Template not found: ${templateId}`);
2648
- const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
2649
3374
  return {
2650
- title: cleanOverrides.title || t.title_pattern,
2651
- description: cleanOverrides.description ?? t.description ?? undefined,
2652
- priority: cleanOverrides.priority ?? t.priority,
2653
- tags: cleanOverrides.tags ?? t.tags,
2654
- project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
2655
- plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
2656
- metadata: cleanOverrides.metadata ?? t.metadata,
2657
- ...cleanOverrides
3375
+ ...task,
3376
+ ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
3377
+ tags: input.tags ?? task.tags,
3378
+ metadata: input.metadata ?? task.metadata,
3379
+ version: task.version + 1,
3380
+ updated_at: now(),
3381
+ completed_at: input.status === "completed" ? now() : task.completed_at,
3382
+ requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
3383
+ approved_by: input.approved_by ?? task.approved_by,
3384
+ approved_at: input.approved_by ? now() : task.approved_at
2658
3385
  };
2659
3386
  }
2660
- function addTemplateTasks(templateId, tasks, db) {
2661
- const d = db || getDatabase();
2662
- const template = getTemplate(templateId, d);
2663
- if (!template)
2664
- throw new Error(`Template not found: ${templateId}`);
2665
- d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
2666
- const results = [];
2667
- for (let i = 0;i < tasks.length; i++) {
2668
- const task = tasks[i];
2669
- const id = uuid();
2670
- 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)
2671
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2672
- id,
2673
- templateId,
2674
- i,
2675
- task.title_pattern,
2676
- task.description || null,
2677
- task.priority || "medium",
2678
- JSON.stringify(task.tags || []),
2679
- task.task_type || null,
2680
- task.condition || null,
2681
- task.include_template_id || null,
2682
- JSON.stringify(task.depends_on || []),
2683
- JSON.stringify(task.metadata || {}),
2684
- now()
2685
- ]);
2686
- const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
2687
- if (row)
2688
- results.push(rowToTemplateTask(row));
2689
- }
2690
- return results;
2691
- }
2692
- function getTemplateWithTasks(id, db) {
3387
+ function deleteTask(id, db) {
2693
3388
  const d = db || getDatabase();
2694
- const template = getTemplate(id, d);
2695
- if (!template)
2696
- return null;
2697
- const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
2698
- const tasks = rows.map(rowToTemplateTask);
2699
- return { ...template, tasks };
3389
+ const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
3390
+ return result.changes > 0;
2700
3391
  }
2701
- function getTemplateTasks(templateId, db) {
2702
- const d = db || getDatabase();
2703
- const resolved = resolveTemplateId(templateId, d);
2704
- if (!resolved)
2705
- return [];
2706
- const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
2707
- return rows.map(rowToTemplateTask);
3392
+ // src/db/task-lifecycle.ts
3393
+ init_types();
3394
+ init_database();
3395
+
3396
+ // src/lib/recurrence.ts
3397
+ var DAY_NAMES = {
3398
+ sunday: 0,
3399
+ sun: 0,
3400
+ monday: 1,
3401
+ mon: 1,
3402
+ tuesday: 2,
3403
+ tue: 2,
3404
+ wednesday: 3,
3405
+ wed: 3,
3406
+ thursday: 4,
3407
+ thu: 4,
3408
+ friday: 5,
3409
+ fri: 5,
3410
+ saturday: 6,
3411
+ sat: 6
3412
+ };
3413
+ function parseRecurrenceRule(rule) {
3414
+ const normalized = rule.trim().toLowerCase();
3415
+ if (normalized === "every weekday" || normalized === "every weekdays") {
3416
+ return { type: "specific_days", days: [1, 2, 3, 4, 5] };
3417
+ }
3418
+ if (normalized === "every day" || normalized === "daily") {
3419
+ return { type: "interval", interval: 1, unit: "day" };
3420
+ }
3421
+ if (normalized === "every week" || normalized === "weekly") {
3422
+ return { type: "interval", interval: 1, unit: "week" };
3423
+ }
3424
+ if (normalized === "every month" || normalized === "monthly") {
3425
+ return { type: "interval", interval: 1, unit: "month" };
3426
+ }
3427
+ const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
3428
+ if (intervalMatch) {
3429
+ return {
3430
+ type: "interval",
3431
+ interval: parseInt(intervalMatch[1], 10),
3432
+ unit: intervalMatch[2]
3433
+ };
3434
+ }
3435
+ const daysMatch = normalized.match(/^every\s+(.+)$/);
3436
+ if (daysMatch) {
3437
+ const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
3438
+ const days = [];
3439
+ for (const part of dayParts) {
3440
+ const dayNum = DAY_NAMES[part];
3441
+ if (dayNum !== undefined) {
3442
+ days.push(dayNum);
3443
+ }
3444
+ }
3445
+ if (days.length > 0) {
3446
+ return { type: "specific_days", days: days.sort((a, b) => a - b) };
3447
+ }
3448
+ }
3449
+ 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"`);
2708
3450
  }
2709
- function evaluateCondition(condition, variables) {
2710
- if (!condition || condition.trim() === "")
3451
+ function isValidRecurrenceRule(rule) {
3452
+ try {
3453
+ parseRecurrenceRule(rule);
2711
3454
  return true;
2712
- const trimmed = condition.trim();
2713
- const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
2714
- if (eqMatch) {
2715
- const varName = eqMatch[1];
2716
- const expected = eqMatch[2].trim();
2717
- return (variables[varName] ?? "") === expected;
2718
- }
2719
- const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
2720
- if (neqMatch) {
2721
- const varName = neqMatch[1];
2722
- const expected = neqMatch[2].trim();
2723
- return (variables[varName] ?? "") !== expected;
3455
+ } catch {
3456
+ return false;
2724
3457
  }
2725
- const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
2726
- if (falsyMatch) {
2727
- const varName = falsyMatch[1];
2728
- const val = variables[varName];
2729
- return !val || val === "" || val === "false";
3458
+ }
3459
+ function nextOccurrence(rule, from) {
3460
+ const parsed = parseRecurrenceRule(rule);
3461
+ const base = from || new Date;
3462
+ if (parsed.type === "interval") {
3463
+ const next = new Date(base);
3464
+ if (parsed.unit === "day") {
3465
+ next.setDate(next.getDate() + parsed.interval);
3466
+ } else if (parsed.unit === "week") {
3467
+ next.setDate(next.getDate() + parsed.interval * 7);
3468
+ } else if (parsed.unit === "month") {
3469
+ next.setMonth(next.getMonth() + parsed.interval);
3470
+ }
3471
+ return next.toISOString();
2730
3472
  }
2731
- const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
2732
- if (truthyMatch) {
2733
- const varName = truthyMatch[1];
2734
- const val = variables[varName];
2735
- return !!val && val !== "" && val !== "false";
3473
+ if (parsed.type === "specific_days") {
3474
+ const currentDay = base.getDay();
3475
+ const days = parsed.days;
3476
+ let daysToAdd = Infinity;
3477
+ for (const day of days) {
3478
+ let diff = day - currentDay;
3479
+ if (diff <= 0)
3480
+ diff += 7;
3481
+ if (diff < daysToAdd)
3482
+ daysToAdd = diff;
3483
+ }
3484
+ const next = new Date(base);
3485
+ next.setDate(next.getDate() + daysToAdd);
3486
+ return next.toISOString();
2736
3487
  }
2737
- return true;
3488
+ throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
2738
3489
  }
2739
- function exportTemplate(id, db) {
2740
- const d = db || getDatabase();
2741
- const template = getTemplateWithTasks(id, d);
2742
- if (!template)
2743
- throw new Error(`Template not found: ${id}`);
2744
- return {
2745
- name: template.name,
2746
- title_pattern: template.title_pattern,
2747
- description: template.description,
2748
- priority: template.priority,
2749
- tags: template.tags,
2750
- variables: template.variables,
2751
- project_id: template.project_id,
2752
- plan_id: template.plan_id,
2753
- metadata: template.metadata,
2754
- tasks: template.tasks.map((t) => ({
2755
- position: t.position,
2756
- title_pattern: t.title_pattern,
2757
- description: t.description,
2758
- priority: t.priority,
2759
- tags: t.tags,
2760
- task_type: t.task_type,
2761
- condition: t.condition,
2762
- include_template_id: t.include_template_id,
2763
- depends_on_positions: t.depends_on_positions,
2764
- metadata: t.metadata
2765
- }))
3490
+
3491
+ // src/db/templates.ts
3492
+ init_database();
3493
+ function rowToTemplate(row) {
3494
+ return {
3495
+ ...row,
3496
+ tags: JSON.parse(row.tags || "[]"),
3497
+ variables: JSON.parse(row.variables || "[]"),
3498
+ metadata: JSON.parse(row.metadata || "{}"),
3499
+ priority: row.priority || "medium",
3500
+ version: row.version ?? 1
2766
3501
  };
2767
3502
  }
2768
- function importTemplate(json, db) {
3503
+ function rowToTemplateTask(row) {
3504
+ return {
3505
+ ...row,
3506
+ tags: JSON.parse(row.tags || "[]"),
3507
+ depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
3508
+ metadata: JSON.parse(row.metadata || "{}"),
3509
+ priority: row.priority || "medium",
3510
+ condition: row.condition ?? null,
3511
+ include_template_id: row.include_template_id ?? null
3512
+ };
3513
+ }
3514
+ function resolveTemplateId(id, d) {
3515
+ return resolvePartialId(d, "task_templates", id);
3516
+ }
3517
+ function createTemplate(input, db) {
2769
3518
  const d = db || getDatabase();
2770
- const taskInputs = (json.tasks || []).map((t) => ({
2771
- title_pattern: t.title_pattern,
2772
- description: t.description ?? undefined,
2773
- priority: t.priority,
2774
- tags: t.tags,
2775
- task_type: t.task_type ?? undefined,
2776
- condition: t.condition ?? undefined,
2777
- include_template_id: t.include_template_id ?? undefined,
2778
- depends_on: t.depends_on_positions,
2779
- metadata: t.metadata
2780
- }));
2781
- return createTemplate({
2782
- name: json.name,
2783
- title_pattern: json.title_pattern,
2784
- description: json.description ?? undefined,
2785
- priority: json.priority,
2786
- tags: json.tags,
2787
- variables: json.variables,
2788
- project_id: json.project_id ?? undefined,
2789
- plan_id: json.plan_id ?? undefined,
2790
- metadata: json.metadata,
2791
- tasks: taskInputs
2792
- }, d);
3519
+ const id = uuid();
3520
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
3521
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3522
+ id,
3523
+ input.name,
3524
+ input.title_pattern,
3525
+ input.description || null,
3526
+ input.priority || "medium",
3527
+ JSON.stringify(input.tags || []),
3528
+ JSON.stringify(input.variables || []),
3529
+ input.project_id || null,
3530
+ input.plan_id || null,
3531
+ JSON.stringify(input.metadata || {}),
3532
+ now()
3533
+ ]);
3534
+ if (input.tasks && input.tasks.length > 0) {
3535
+ addTemplateTasks(id, input.tasks, d);
3536
+ }
3537
+ return getTemplate(id, d);
2793
3538
  }
2794
- function getTemplateVersion(id, version, db) {
3539
+ function getTemplate(id, db) {
2795
3540
  const d = db || getDatabase();
2796
3541
  const resolved = resolveTemplateId(id, d);
2797
3542
  if (!resolved)
2798
3543
  return null;
2799
- const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
2800
- return row || null;
3544
+ const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
3545
+ return row ? rowToTemplate(row) : null;
2801
3546
  }
2802
- function listTemplateVersions(id, db) {
3547
+ function listTemplates(db) {
3548
+ const d = db || getDatabase();
3549
+ return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
3550
+ }
3551
+ function deleteTemplate(id, db) {
2803
3552
  const d = db || getDatabase();
2804
3553
  const resolved = resolveTemplateId(id, d);
2805
3554
  if (!resolved)
2806
- return [];
2807
- return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
3555
+ return false;
3556
+ return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
2808
3557
  }
2809
- function resolveVariables(templateVars, provided) {
2810
- const merged = { ...provided };
2811
- for (const v of templateVars) {
2812
- if (merged[v.name] === undefined && v.default !== undefined) {
2813
- merged[v.name] = v.default;
2814
- }
3558
+ function updateTemplate(id, updates, db) {
3559
+ const d = db || getDatabase();
3560
+ const resolved = resolveTemplateId(id, d);
3561
+ if (!resolved)
3562
+ return null;
3563
+ const current = getTemplateWithTasks(resolved, d);
3564
+ if (current) {
3565
+ const snapshot = JSON.stringify({
3566
+ name: current.name,
3567
+ title_pattern: current.title_pattern,
3568
+ description: current.description,
3569
+ priority: current.priority,
3570
+ tags: current.tags,
3571
+ variables: current.variables,
3572
+ project_id: current.project_id,
3573
+ plan_id: current.plan_id,
3574
+ metadata: current.metadata,
3575
+ tasks: current.tasks
3576
+ });
3577
+ d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
2815
3578
  }
2816
- const missing = [];
2817
- for (const v of templateVars) {
2818
- if (v.required && merged[v.name] === undefined) {
2819
- missing.push(v.name);
2820
- }
3579
+ const sets = ["version = version + 1"];
3580
+ const values = [];
3581
+ if (updates.name !== undefined) {
3582
+ sets.push("name = ?");
3583
+ values.push(updates.name);
2821
3584
  }
2822
- if (missing.length > 0) {
2823
- throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
3585
+ if (updates.title_pattern !== undefined) {
3586
+ sets.push("title_pattern = ?");
3587
+ values.push(updates.title_pattern);
2824
3588
  }
2825
- return merged;
2826
- }
2827
- function substituteVars(text, variables) {
2828
- let result = text;
2829
- for (const [key, val] of Object.entries(variables)) {
2830
- result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
3589
+ if (updates.description !== undefined) {
3590
+ sets.push("description = ?");
3591
+ values.push(updates.description);
2831
3592
  }
2832
- return result;
2833
- }
2834
- function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
2835
- const d = db || getDatabase();
2836
- const template = getTemplateWithTasks(templateId, d);
2837
- if (!template)
2838
- throw new Error(`Template not found: ${templateId}`);
2839
- const visited = _visitedTemplateIds || new Set;
2840
- if (visited.has(template.id)) {
2841
- throw new Error(`Circular template reference detected: ${template.id}`);
3593
+ if (updates.priority !== undefined) {
3594
+ sets.push("priority = ?");
3595
+ values.push(updates.priority);
2842
3596
  }
2843
- visited.add(template.id);
2844
- const resolved = resolveVariables(template.variables, variables);
2845
- if (template.tasks.length === 0) {
2846
- const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
2847
- const task = createTask(input, d);
2848
- return [task];
3597
+ if (updates.tags !== undefined) {
3598
+ sets.push("tags = ?");
3599
+ values.push(JSON.stringify(updates.tags));
2849
3600
  }
2850
- const createdTasks = [];
2851
- const positionToId = new Map;
2852
- const skippedPositions = new Set;
2853
- for (const tt of template.tasks) {
2854
- if (tt.include_template_id) {
2855
- const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
2856
- createdTasks.push(...includedTasks);
2857
- if (includedTasks.length > 0) {
2858
- positionToId.set(tt.position, includedTasks[0].id);
2859
- } else {
2860
- skippedPositions.add(tt.position);
2861
- }
2862
- continue;
2863
- }
2864
- if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
2865
- skippedPositions.add(tt.position);
2866
- continue;
2867
- }
2868
- let title = tt.title_pattern;
2869
- let desc = tt.description;
2870
- title = substituteVars(title, resolved);
2871
- if (desc)
2872
- desc = substituteVars(desc, resolved);
2873
- const task = createTask({
2874
- title,
2875
- description: desc ?? undefined,
2876
- priority: tt.priority,
2877
- tags: tt.tags,
2878
- task_type: tt.task_type ?? undefined,
2879
- project_id: projectId,
2880
- task_list_id: taskListId,
2881
- metadata: tt.metadata
2882
- }, d);
2883
- createdTasks.push(task);
2884
- positionToId.set(tt.position, task.id);
3601
+ if (updates.variables !== undefined) {
3602
+ sets.push("variables = ?");
3603
+ values.push(JSON.stringify(updates.variables));
2885
3604
  }
2886
- for (const tt of template.tasks) {
2887
- if (skippedPositions.has(tt.position))
2888
- continue;
2889
- if (tt.include_template_id)
2890
- continue;
2891
- const deps = tt.depends_on_positions;
2892
- for (const depPos of deps) {
2893
- if (skippedPositions.has(depPos))
2894
- continue;
2895
- const taskId = positionToId.get(tt.position);
2896
- const depId = positionToId.get(depPos);
2897
- if (taskId && depId) {
2898
- addDependency(taskId, depId, d);
2899
- }
2900
- }
3605
+ if (updates.project_id !== undefined) {
3606
+ sets.push("project_id = ?");
3607
+ values.push(updates.project_id);
2901
3608
  }
2902
- return createdTasks;
2903
- }
2904
- function previewTemplate(templateId, variables, db) {
2905
- const d = db || getDatabase();
2906
- const template = getTemplateWithTasks(templateId, d);
2907
- if (!template)
2908
- throw new Error(`Template not found: ${templateId}`);
2909
- const resolved = resolveVariables(template.variables, variables);
2910
- const tasks = [];
2911
- if (template.tasks.length === 0) {
2912
- tasks.push({
2913
- position: 0,
2914
- title: substituteVars(template.title_pattern, resolved),
2915
- description: template.description ? substituteVars(template.description, resolved) : null,
2916
- priority: template.priority,
2917
- tags: template.tags,
2918
- task_type: null,
2919
- depends_on_positions: []
2920
- });
2921
- } else {
2922
- for (const tt of template.tasks) {
2923
- if (tt.condition && !evaluateCondition(tt.condition, resolved))
2924
- continue;
2925
- tasks.push({
2926
- position: tt.position,
2927
- title: substituteVars(tt.title_pattern, resolved),
2928
- description: tt.description ? substituteVars(tt.description, resolved) : null,
2929
- priority: tt.priority,
2930
- tags: tt.tags,
2931
- task_type: tt.task_type,
2932
- depends_on_positions: tt.depends_on_positions
2933
- });
2934
- }
3609
+ if (updates.plan_id !== undefined) {
3610
+ sets.push("plan_id = ?");
3611
+ values.push(updates.plan_id);
2935
3612
  }
3613
+ if (updates.metadata !== undefined) {
3614
+ sets.push("metadata = ?");
3615
+ values.push(JSON.stringify(updates.metadata));
3616
+ }
3617
+ values.push(resolved);
3618
+ d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
3619
+ return getTemplate(resolved, d);
3620
+ }
3621
+ function taskFromTemplate(templateId, overrides = {}, db) {
3622
+ const t = getTemplate(templateId, db);
3623
+ if (!t)
3624
+ throw new Error(`Template not found: ${templateId}`);
3625
+ const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
2936
3626
  return {
2937
- template_id: template.id,
2938
- template_name: template.name,
2939
- description: template.description,
2940
- variables: template.variables,
2941
- resolved_variables: resolved,
2942
- tasks
3627
+ title: cleanOverrides.title || t.title_pattern,
3628
+ description: cleanOverrides.description ?? t.description ?? undefined,
3629
+ priority: cleanOverrides.priority ?? t.priority,
3630
+ tags: cleanOverrides.tags ?? t.tags,
3631
+ project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
3632
+ plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
3633
+ metadata: cleanOverrides.metadata ?? t.metadata,
3634
+ ...cleanOverrides
2943
3635
  };
2944
3636
  }
2945
-
2946
- // src/db/checklists.ts
2947
- init_database();
2948
- function rowToItem(row) {
2949
- return { ...row, checked: !!row.checked };
2950
- }
2951
- function getChecklist(taskId, db) {
2952
- const d = db || getDatabase();
2953
- const rows = d.query("SELECT * FROM task_checklists WHERE task_id = ? ORDER BY position, created_at").all(taskId);
2954
- return rows.map(rowToItem);
2955
- }
2956
- function addChecklistItem(input, db) {
3637
+ function addTemplateTasks(templateId, tasks, db) {
2957
3638
  const d = db || getDatabase();
2958
- const id = uuid();
2959
- const timestamp = now();
2960
- let position = input.position;
2961
- if (position === undefined) {
2962
- const maxRow = d.query("SELECT MAX(position) as max_pos FROM task_checklists WHERE task_id = ?").get(input.task_id);
2963
- position = (maxRow?.max_pos ?? -1) + 1;
3639
+ const template = getTemplate(templateId, d);
3640
+ if (!template)
3641
+ throw new Error(`Template not found: ${templateId}`);
3642
+ d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
3643
+ const results = [];
3644
+ for (let i = 0;i < tasks.length; i++) {
3645
+ const task = tasks[i];
3646
+ const id = uuid();
3647
+ 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)
3648
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3649
+ id,
3650
+ templateId,
3651
+ i,
3652
+ task.title_pattern,
3653
+ task.description || null,
3654
+ task.priority || "medium",
3655
+ JSON.stringify(task.tags || []),
3656
+ task.task_type || null,
3657
+ task.condition || null,
3658
+ task.include_template_id || null,
3659
+ JSON.stringify(task.depends_on || []),
3660
+ JSON.stringify(task.metadata || {}),
3661
+ now()
3662
+ ]);
3663
+ const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
3664
+ if (row)
3665
+ results.push(rowToTemplateTask(row));
2964
3666
  }
2965
- d.run("INSERT INTO task_checklists (id, task_id, position, text, checked, created_at, updated_at) VALUES (?, ?, ?, ?, 0, ?, ?)", [id, input.task_id, position, input.text, timestamp, timestamp]);
2966
- return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
2967
- }
2968
- function checkChecklistItem(id, checked, db) {
2969
- const d = db || getDatabase();
2970
- const timestamp = now();
2971
- const result = d.run("UPDATE task_checklists SET checked = ?, updated_at = ? WHERE id = ?", [checked ? 1 : 0, timestamp, id]);
2972
- if (result.changes === 0)
2973
- return null;
2974
- return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
3667
+ return results;
2975
3668
  }
2976
- function updateChecklistItemText(id, text, db) {
3669
+ function getTemplateWithTasks(id, db) {
2977
3670
  const d = db || getDatabase();
2978
- const timestamp = now();
2979
- const result = d.run("UPDATE task_checklists SET text = ?, updated_at = ? WHERE id = ?", [text, timestamp, id]);
2980
- if (result.changes === 0)
3671
+ const template = getTemplate(id, d);
3672
+ if (!template)
2981
3673
  return null;
2982
- return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
3674
+ const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
3675
+ const tasks = rows.map(rowToTemplateTask);
3676
+ return { ...template, tasks };
2983
3677
  }
2984
- function removeChecklistItem(id, db) {
3678
+ function getTemplateTasks(templateId, db) {
2985
3679
  const d = db || getDatabase();
2986
- const result = d.run("DELETE FROM task_checklists WHERE id = ?", [id]);
2987
- return result.changes > 0;
3680
+ const resolved = resolveTemplateId(templateId, d);
3681
+ if (!resolved)
3682
+ return [];
3683
+ const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
3684
+ return rows.map(rowToTemplateTask);
2988
3685
  }
2989
- function clearChecklist(taskId, db) {
2990
- const d = db || getDatabase();
2991
- const result = d.run("DELETE FROM task_checklists WHERE task_id = ?", [taskId]);
2992
- return result.changes;
3686
+ function evaluateCondition(condition, variables) {
3687
+ if (!condition || condition.trim() === "")
3688
+ return true;
3689
+ const trimmed = condition.trim();
3690
+ const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
3691
+ if (eqMatch) {
3692
+ const varName = eqMatch[1];
3693
+ const expected = eqMatch[2].trim();
3694
+ return (variables[varName] ?? "") === expected;
3695
+ }
3696
+ const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
3697
+ if (neqMatch) {
3698
+ const varName = neqMatch[1];
3699
+ const expected = neqMatch[2].trim();
3700
+ return (variables[varName] ?? "") !== expected;
3701
+ }
3702
+ const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
3703
+ if (falsyMatch) {
3704
+ const varName = falsyMatch[1];
3705
+ const val = variables[varName];
3706
+ return !val || val === "" || val === "false";
3707
+ }
3708
+ const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
3709
+ if (truthyMatch) {
3710
+ const varName = truthyMatch[1];
3711
+ const val = variables[varName];
3712
+ return !!val && val !== "" && val !== "false";
3713
+ }
3714
+ return true;
2993
3715
  }
2994
- function getChecklistStats(taskId, db) {
3716
+ function exportTemplate(id, db) {
2995
3717
  const d = db || getDatabase();
2996
- const row = d.query("SELECT COUNT(*) as total, SUM(checked) as checked FROM task_checklists WHERE task_id = ?").get(taskId);
2997
- return { total: row?.total ?? 0, checked: row?.checked ?? 0 };
2998
- }
2999
-
3000
- // src/db/tasks.ts
3001
- function rowToTask(row) {
3718
+ const template = getTemplateWithTasks(id, d);
3719
+ if (!template)
3720
+ throw new Error(`Template not found: ${id}`);
3002
3721
  return {
3003
- ...row,
3004
- tags: JSON.parse(row.tags || "[]"),
3005
- metadata: JSON.parse(row.metadata || "{}"),
3006
- status: row.status,
3007
- priority: row.priority,
3008
- requires_approval: !!row.requires_approval
3722
+ name: template.name,
3723
+ title_pattern: template.title_pattern,
3724
+ description: template.description,
3725
+ priority: template.priority,
3726
+ tags: template.tags,
3727
+ variables: template.variables,
3728
+ project_id: template.project_id,
3729
+ plan_id: template.plan_id,
3730
+ metadata: template.metadata,
3731
+ tasks: template.tasks.map((t) => ({
3732
+ position: t.position,
3733
+ title_pattern: t.title_pattern,
3734
+ description: t.description,
3735
+ priority: t.priority,
3736
+ tags: t.tags,
3737
+ task_type: t.task_type,
3738
+ condition: t.condition,
3739
+ include_template_id: t.include_template_id,
3740
+ depends_on_positions: t.depends_on_positions,
3741
+ metadata: t.metadata
3742
+ }))
3009
3743
  };
3010
3744
  }
3011
- function insertTaskTags(taskId, tags, db) {
3012
- if (tags.length === 0)
3013
- return;
3014
- const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
3015
- for (const tag of tags) {
3016
- if (tag)
3017
- stmt.run(taskId, tag);
3018
- }
3019
- }
3020
- function replaceTaskTags(taskId, tags, db) {
3021
- db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
3022
- insertTaskTags(taskId, tags, db);
3023
- }
3024
- function createTask(input, db) {
3745
+ function importTemplate(json, db) {
3025
3746
  const d = db || getDatabase();
3026
- const timestamp = now();
3027
- const tags = input.tags || [];
3028
- const assignedBy = input.assigned_by || input.agent_id;
3029
- const assignedFromProject = input.assigned_from_project || null;
3030
- let id = uuid();
3031
- for (let attempt = 0;attempt < 3; attempt++) {
3032
- try {
3033
- 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)
3034
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3035
- id,
3036
- null,
3037
- input.project_id || null,
3038
- input.parent_id || null,
3039
- input.plan_id || null,
3040
- input.task_list_id || null,
3041
- input.title,
3042
- input.description || null,
3043
- input.status || "pending",
3044
- input.priority || "medium",
3045
- input.agent_id || null,
3046
- input.assigned_to || null,
3047
- input.session_id || null,
3048
- input.working_dir || null,
3049
- JSON.stringify(tags),
3050
- JSON.stringify(input.metadata || {}),
3051
- timestamp,
3052
- timestamp,
3053
- input.due_at || null,
3054
- input.estimated_minutes || null,
3055
- input.requires_approval ? 1 : 0,
3056
- null,
3057
- null,
3058
- input.recurrence_rule || null,
3059
- input.recurrence_parent_id || null,
3060
- input.spawns_template_id || null,
3061
- input.reason || null,
3062
- input.spawned_from_session || null,
3063
- assignedBy || null,
3064
- assignedFromProject || null,
3065
- input.task_type || null
3066
- ]);
3067
- break;
3068
- } catch (e) {
3069
- if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
3070
- id = uuid();
3071
- continue;
3072
- }
3073
- throw e;
3074
- }
3075
- }
3076
- if (tags.length > 0) {
3077
- insertTaskTags(id, tags, d);
3078
- }
3079
- const task = getTask(id, d);
3080
- 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(() => {});
3081
- return task;
3747
+ const taskInputs = (json.tasks || []).map((t) => ({
3748
+ title_pattern: t.title_pattern,
3749
+ description: t.description ?? undefined,
3750
+ priority: t.priority,
3751
+ tags: t.tags,
3752
+ task_type: t.task_type ?? undefined,
3753
+ condition: t.condition ?? undefined,
3754
+ include_template_id: t.include_template_id ?? undefined,
3755
+ depends_on: t.depends_on_positions,
3756
+ metadata: t.metadata
3757
+ }));
3758
+ return createTemplate({
3759
+ name: json.name,
3760
+ title_pattern: json.title_pattern,
3761
+ description: json.description ?? undefined,
3762
+ priority: json.priority,
3763
+ tags: json.tags,
3764
+ variables: json.variables,
3765
+ project_id: json.project_id ?? undefined,
3766
+ plan_id: json.plan_id ?? undefined,
3767
+ metadata: json.metadata,
3768
+ tasks: taskInputs
3769
+ }, d);
3082
3770
  }
3083
- function getTask(id, db) {
3771
+ function getTemplateVersion(id, version, db) {
3084
3772
  const d = db || getDatabase();
3085
- const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
3086
- if (!row)
3773
+ const resolved = resolveTemplateId(id, d);
3774
+ if (!resolved)
3087
3775
  return null;
3088
- return rowToTask(row);
3776
+ const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
3777
+ return row || null;
3089
3778
  }
3090
- function getTaskWithRelations(id, db) {
3779
+ function listTemplateVersions(id, db) {
3091
3780
  const d = db || getDatabase();
3092
- const task = getTask(id, d);
3093
- if (!task)
3094
- return null;
3095
- const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
3096
- const subtasks = subtaskRows.map(rowToTask);
3097
- const depRows = d.query(`SELECT t.* FROM tasks t
3098
- JOIN task_dependencies td ON td.depends_on = t.id
3099
- WHERE td.task_id = ?`).all(id);
3100
- const dependencies = depRows.map(rowToTask);
3101
- const blockedByRows = d.query(`SELECT t.* FROM tasks t
3102
- JOIN task_dependencies td ON td.task_id = t.id
3103
- WHERE td.depends_on = ?`).all(id);
3104
- const blocked_by = blockedByRows.map(rowToTask);
3105
- const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
3106
- const parent = task.parent_id ? getTask(task.parent_id, d) : null;
3107
- const checklist = getChecklist(id, d);
3108
- return {
3109
- ...task,
3110
- subtasks,
3111
- dependencies,
3112
- blocked_by,
3113
- comments,
3114
- parent,
3115
- checklist
3116
- };
3781
+ const resolved = resolveTemplateId(id, d);
3782
+ if (!resolved)
3783
+ return [];
3784
+ return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
3117
3785
  }
3118
- function listTasks(filter = {}, db) {
3119
- const d = db || getDatabase();
3120
- clearExpiredLocks(d);
3121
- const conditions = [];
3122
- const params = [];
3123
- if (filter.project_id) {
3124
- conditions.push("project_id = ?");
3125
- params.push(filter.project_id);
3126
- }
3127
- if (filter.parent_id !== undefined) {
3128
- if (filter.parent_id === null) {
3129
- conditions.push("parent_id IS NULL");
3130
- } else {
3131
- conditions.push("parent_id = ?");
3132
- params.push(filter.parent_id);
3133
- }
3134
- }
3135
- if (filter.status) {
3136
- if (Array.isArray(filter.status)) {
3137
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
3138
- params.push(...filter.status);
3139
- } else {
3140
- conditions.push("status = ?");
3141
- params.push(filter.status);
3786
+ function resolveVariables(templateVars, provided) {
3787
+ const merged = { ...provided };
3788
+ for (const v of templateVars) {
3789
+ if (merged[v.name] === undefined && v.default !== undefined) {
3790
+ merged[v.name] = v.default;
3142
3791
  }
3143
3792
  }
3144
- if (filter.priority) {
3145
- if (Array.isArray(filter.priority)) {
3146
- conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
3147
- params.push(...filter.priority);
3148
- } else {
3149
- conditions.push("priority = ?");
3150
- params.push(filter.priority);
3793
+ const missing = [];
3794
+ for (const v of templateVars) {
3795
+ if (v.required && merged[v.name] === undefined) {
3796
+ missing.push(v.name);
3151
3797
  }
3152
3798
  }
3153
- if (filter.assigned_to) {
3154
- conditions.push("assigned_to = ?");
3155
- params.push(filter.assigned_to);
3156
- }
3157
- if (filter.agent_id) {
3158
- conditions.push("agent_id = ?");
3159
- params.push(filter.agent_id);
3160
- }
3161
- if (filter.session_id) {
3162
- conditions.push("session_id = ?");
3163
- params.push(filter.session_id);
3164
- }
3165
- if (filter.tags && filter.tags.length > 0) {
3166
- const placeholders = filter.tags.map(() => "?").join(",");
3167
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
3168
- params.push(...filter.tags);
3799
+ if (missing.length > 0) {
3800
+ throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
3169
3801
  }
3170
- if (filter.plan_id) {
3171
- conditions.push("plan_id = ?");
3172
- params.push(filter.plan_id);
3802
+ return merged;
3803
+ }
3804
+ function substituteVars(text, variables) {
3805
+ let result = text;
3806
+ for (const [key, val] of Object.entries(variables)) {
3807
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
3173
3808
  }
3174
- if (filter.task_list_id) {
3175
- conditions.push("task_list_id = ?");
3176
- params.push(filter.task_list_id);
3809
+ return result;
3810
+ }
3811
+ function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
3812
+ const d = db || getDatabase();
3813
+ const template = getTemplateWithTasks(templateId, d);
3814
+ if (!template)
3815
+ throw new Error(`Template not found: ${templateId}`);
3816
+ const visited = _visitedTemplateIds || new Set;
3817
+ if (visited.has(template.id)) {
3818
+ throw new Error(`Circular template reference detected: ${template.id}`);
3177
3819
  }
3178
- if (filter.has_recurrence === true) {
3179
- conditions.push("recurrence_rule IS NOT NULL");
3180
- } else if (filter.has_recurrence === false) {
3181
- conditions.push("recurrence_rule IS NULL");
3820
+ visited.add(template.id);
3821
+ const resolved = resolveVariables(template.variables, variables);
3822
+ if (template.tasks.length === 0) {
3823
+ const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
3824
+ const task = createTask(input, d);
3825
+ return [task];
3182
3826
  }
3183
- if (filter.task_type) {
3184
- if (Array.isArray(filter.task_type)) {
3185
- conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
3186
- params.push(...filter.task_type);
3187
- } else {
3188
- conditions.push("task_type = ?");
3189
- params.push(filter.task_type);
3827
+ const createdTasks = [];
3828
+ const positionToId = new Map;
3829
+ const skippedPositions = new Set;
3830
+ for (const tt of template.tasks) {
3831
+ if (tt.include_template_id) {
3832
+ const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
3833
+ createdTasks.push(...includedTasks);
3834
+ if (includedTasks.length > 0) {
3835
+ positionToId.set(tt.position, includedTasks[0].id);
3836
+ } else {
3837
+ skippedPositions.add(tt.position);
3838
+ }
3839
+ continue;
3190
3840
  }
3841
+ if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
3842
+ skippedPositions.add(tt.position);
3843
+ continue;
3844
+ }
3845
+ let title = tt.title_pattern;
3846
+ let desc = tt.description;
3847
+ title = substituteVars(title, resolved);
3848
+ if (desc)
3849
+ desc = substituteVars(desc, resolved);
3850
+ const task = createTask({
3851
+ title,
3852
+ description: desc ?? undefined,
3853
+ priority: tt.priority,
3854
+ tags: tt.tags,
3855
+ task_type: tt.task_type ?? undefined,
3856
+ project_id: projectId,
3857
+ task_list_id: taskListId,
3858
+ metadata: tt.metadata
3859
+ }, d);
3860
+ createdTasks.push(task);
3861
+ positionToId.set(tt.position, task.id);
3191
3862
  }
3192
- const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
3193
- if (filter.cursor) {
3194
- try {
3195
- const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
3196
- conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
3197
- params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
3198
- } catch {}
3199
- }
3200
- if (!filter.include_archived) {
3201
- conditions.push("archived_at IS NULL");
3202
- }
3203
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3204
- let limitClause = "";
3205
- if (filter.limit) {
3206
- limitClause = " LIMIT ?";
3207
- params.push(filter.limit);
3208
- if (!filter.cursor && filter.offset) {
3209
- limitClause += " OFFSET ?";
3210
- params.push(filter.offset);
3863
+ for (const tt of template.tasks) {
3864
+ if (skippedPositions.has(tt.position))
3865
+ continue;
3866
+ if (tt.include_template_id)
3867
+ continue;
3868
+ const deps = tt.depends_on_positions;
3869
+ for (const depPos of deps) {
3870
+ if (skippedPositions.has(depPos))
3871
+ continue;
3872
+ const taskId = positionToId.get(tt.position);
3873
+ const depId = positionToId.get(depPos);
3874
+ if (taskId && depId) {
3875
+ addDependency(taskId, depId, d);
3876
+ }
3211
3877
  }
3212
3878
  }
3213
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
3214
- return rows.map(rowToTask);
3879
+ return createdTasks;
3215
3880
  }
3216
- function countTasks(filter = {}, db) {
3881
+ function previewTemplate(templateId, variables, db) {
3217
3882
  const d = db || getDatabase();
3218
- const conditions = [];
3219
- const params = [];
3220
- if (filter.project_id) {
3221
- conditions.push("project_id = ?");
3222
- params.push(filter.project_id);
3223
- }
3224
- if (filter.parent_id !== undefined) {
3225
- if (filter.parent_id === null) {
3226
- conditions.push("parent_id IS NULL");
3227
- } else {
3228
- conditions.push("parent_id = ?");
3229
- params.push(filter.parent_id);
3230
- }
3231
- }
3232
- if (filter.status) {
3233
- if (Array.isArray(filter.status)) {
3234
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
3235
- params.push(...filter.status);
3236
- } else {
3237
- conditions.push("status = ?");
3238
- params.push(filter.status);
3239
- }
3240
- }
3241
- if (filter.priority) {
3242
- if (Array.isArray(filter.priority)) {
3243
- conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
3244
- params.push(...filter.priority);
3245
- } else {
3246
- conditions.push("priority = ?");
3247
- params.push(filter.priority);
3883
+ const template = getTemplateWithTasks(templateId, d);
3884
+ if (!template)
3885
+ throw new Error(`Template not found: ${templateId}`);
3886
+ const resolved = resolveVariables(template.variables, variables);
3887
+ const tasks = [];
3888
+ if (template.tasks.length === 0) {
3889
+ tasks.push({
3890
+ position: 0,
3891
+ title: substituteVars(template.title_pattern, resolved),
3892
+ description: template.description ? substituteVars(template.description, resolved) : null,
3893
+ priority: template.priority,
3894
+ tags: template.tags,
3895
+ task_type: null,
3896
+ depends_on_positions: []
3897
+ });
3898
+ } else {
3899
+ for (const tt of template.tasks) {
3900
+ if (tt.condition && !evaluateCondition(tt.condition, resolved))
3901
+ continue;
3902
+ tasks.push({
3903
+ position: tt.position,
3904
+ title: substituteVars(tt.title_pattern, resolved),
3905
+ description: tt.description ? substituteVars(tt.description, resolved) : null,
3906
+ priority: tt.priority,
3907
+ tags: tt.tags,
3908
+ task_type: tt.task_type,
3909
+ depends_on_positions: tt.depends_on_positions
3910
+ });
3248
3911
  }
3249
3912
  }
3250
- if (filter.assigned_to) {
3251
- conditions.push("assigned_to = ?");
3252
- params.push(filter.assigned_to);
3253
- }
3254
- if (filter.agent_id) {
3255
- conditions.push("agent_id = ?");
3256
- params.push(filter.agent_id);
3257
- }
3258
- if (filter.session_id) {
3259
- conditions.push("session_id = ?");
3260
- params.push(filter.session_id);
3913
+ return {
3914
+ template_id: template.id,
3915
+ template_name: template.name,
3916
+ description: template.description,
3917
+ variables: template.variables,
3918
+ resolved_variables: resolved,
3919
+ tasks
3920
+ };
3921
+ }
3922
+
3923
+ // src/db/task-graph.ts
3924
+ init_types();
3925
+ init_database();
3926
+ function addDependency(taskId, dependsOn, db) {
3927
+ const d = db || getDatabase();
3928
+ if (!getTask(taskId, d))
3929
+ throw new TaskNotFoundError(taskId);
3930
+ if (!getTask(dependsOn, d))
3931
+ throw new TaskNotFoundError(dependsOn);
3932
+ if (wouldCreateCycle(taskId, dependsOn, d)) {
3933
+ throw new DependencyCycleError(taskId, dependsOn);
3261
3934
  }
3262
- if (filter.tags && filter.tags.length > 0) {
3263
- const placeholders = filter.tags.map(() => "?").join(",");
3264
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
3265
- params.push(...filter.tags);
3935
+ d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
3936
+ }
3937
+ function removeDependency(taskId, dependsOn, db) {
3938
+ const d = db || getDatabase();
3939
+ const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
3940
+ return result.changes > 0;
3941
+ }
3942
+ function getTaskDependencies(taskId, db) {
3943
+ const d = db || getDatabase();
3944
+ return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
3945
+ }
3946
+ function getTaskDependents(taskId, db) {
3947
+ const d = db || getDatabase();
3948
+ return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
3949
+ }
3950
+ function cloneTask(taskId, overrides, db) {
3951
+ const d = db || getDatabase();
3952
+ const source = getTask(taskId, d);
3953
+ if (!source)
3954
+ throw new TaskNotFoundError(taskId);
3955
+ const input = {
3956
+ title: overrides?.title ?? source.title,
3957
+ description: overrides?.description ?? source.description ?? undefined,
3958
+ priority: overrides?.priority ?? source.priority,
3959
+ project_id: overrides?.project_id ?? source.project_id ?? undefined,
3960
+ parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
3961
+ plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
3962
+ task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
3963
+ status: overrides?.status ?? "pending",
3964
+ agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
3965
+ assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
3966
+ tags: overrides?.tags ?? source.tags,
3967
+ metadata: overrides?.metadata ?? source.metadata,
3968
+ estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
3969
+ recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
3970
+ };
3971
+ return createTask(input, d);
3972
+ }
3973
+ function getTaskGraph(taskId, direction = "both", db) {
3974
+ const d = db || getDatabase();
3975
+ const task = getTask(taskId, d);
3976
+ if (!task)
3977
+ throw new TaskNotFoundError(taskId);
3978
+ function toNode(t) {
3979
+ const deps = getTaskDependencies(t.id, d);
3980
+ const hasUnfinishedDeps = deps.some((dep) => {
3981
+ const depTask = getTask(dep.depends_on, d);
3982
+ return depTask && depTask.status !== "completed";
3983
+ });
3984
+ return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
3266
3985
  }
3267
- if (filter.plan_id) {
3268
- conditions.push("plan_id = ?");
3269
- params.push(filter.plan_id);
3986
+ function buildUp(id, visited) {
3987
+ if (visited.has(id))
3988
+ return [];
3989
+ visited.add(id);
3990
+ const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
3991
+ return deps.map((dep) => {
3992
+ const depTask = getTask(dep.depends_on, d);
3993
+ if (!depTask)
3994
+ return null;
3995
+ return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
3996
+ }).filter(Boolean);
3270
3997
  }
3271
- if (filter.task_list_id) {
3272
- conditions.push("task_list_id = ?");
3273
- params.push(filter.task_list_id);
3998
+ function buildDown(id, visited) {
3999
+ if (visited.has(id))
4000
+ return [];
4001
+ visited.add(id);
4002
+ const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
4003
+ return dependents.map((dep) => {
4004
+ const depTask = getTask(dep.task_id, d);
4005
+ if (!depTask)
4006
+ return null;
4007
+ return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
4008
+ }).filter(Boolean);
3274
4009
  }
3275
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3276
- const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
3277
- return row.count;
4010
+ const rootNode = toNode(task);
4011
+ const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
4012
+ const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
4013
+ return { task: rootNode, depends_on, blocks };
3278
4014
  }
3279
- function updateTask(id, input, db) {
4015
+ function moveTask(taskId, target, db) {
3280
4016
  const d = db || getDatabase();
3281
- const task = getTask(id, d);
4017
+ const task = getTask(taskId, d);
3282
4018
  if (!task)
3283
- throw new TaskNotFoundError(id);
3284
- if (task.version !== input.version) {
3285
- throw new VersionConflictError(id, input.version, task.version);
3286
- }
3287
- const sets = ["version = version + 1", "updated_at = ?"];
4019
+ throw new TaskNotFoundError(taskId);
4020
+ const sets = ["updated_at = ?", "version = version + 1"];
3288
4021
  const params = [now()];
3289
- if (input.title !== undefined) {
3290
- sets.push("title = ?");
3291
- params.push(input.title);
3292
- }
3293
- if (input.description !== undefined) {
3294
- sets.push("description = ?");
3295
- params.push(input.description);
3296
- }
3297
- if (input.status !== undefined) {
3298
- if (input.status === "completed") {
3299
- checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
3300
- }
3301
- sets.push("status = ?");
3302
- params.push(input.status);
3303
- if (input.status === "completed") {
3304
- sets.push("completed_at = ?");
3305
- params.push(now());
3306
- }
3307
- }
3308
- if (input.priority !== undefined) {
3309
- sets.push("priority = ?");
3310
- params.push(input.priority);
3311
- }
3312
- if (input.assigned_to !== undefined) {
3313
- sets.push("assigned_to = ?");
3314
- params.push(input.assigned_to);
3315
- }
3316
- if (input.tags !== undefined) {
3317
- sets.push("tags = ?");
3318
- params.push(JSON.stringify(input.tags));
3319
- }
3320
- if (input.metadata !== undefined) {
3321
- sets.push("metadata = ?");
3322
- params.push(JSON.stringify(input.metadata));
3323
- }
3324
- if (input.plan_id !== undefined) {
3325
- sets.push("plan_id = ?");
3326
- params.push(input.plan_id);
3327
- }
3328
- if (input.task_list_id !== undefined) {
4022
+ if (target.task_list_id !== undefined) {
3329
4023
  sets.push("task_list_id = ?");
3330
- params.push(input.task_list_id);
3331
- }
3332
- if (input.due_at !== undefined) {
3333
- sets.push("due_at = ?");
3334
- params.push(input.due_at);
3335
- }
3336
- if (input.estimated_minutes !== undefined) {
3337
- sets.push("estimated_minutes = ?");
3338
- params.push(input.estimated_minutes);
3339
- }
3340
- if (input.requires_approval !== undefined) {
3341
- sets.push("requires_approval = ?");
3342
- params.push(input.requires_approval ? 1 : 0);
3343
- }
3344
- if (input.approved_by !== undefined) {
3345
- sets.push("approved_by = ?");
3346
- params.push(input.approved_by);
3347
- sets.push("approved_at = ?");
3348
- params.push(now());
3349
- }
3350
- if (input.recurrence_rule !== undefined) {
3351
- sets.push("recurrence_rule = ?");
3352
- params.push(input.recurrence_rule);
3353
- }
3354
- if (input.task_type !== undefined) {
3355
- sets.push("task_type = ?");
3356
- params.push(input.task_type ?? null);
3357
- }
3358
- params.push(id, input.version);
3359
- const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
3360
- if (result.changes === 0) {
3361
- const current = getTask(id, d);
3362
- throw new VersionConflictError(id, input.version, current?.version ?? -1);
3363
- }
3364
- if (input.tags !== undefined) {
3365
- replaceTaskTags(id, input.tags, d);
3366
- }
3367
- const agentId = task.assigned_to || task.agent_id || null;
3368
- if (input.status !== undefined && input.status !== task.status)
3369
- logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
3370
- if (input.priority !== undefined && input.priority !== task.priority)
3371
- logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
3372
- if (input.title !== undefined && input.title !== task.title)
3373
- logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
3374
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
3375
- logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
3376
- if (input.approved_by !== undefined)
3377
- logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
3378
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
3379
- dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
4024
+ params.push(target.task_list_id);
3380
4025
  }
3381
- if (input.status !== undefined && input.status !== task.status) {
3382
- dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
4026
+ if (target.project_id !== undefined) {
4027
+ sets.push("project_id = ?");
4028
+ params.push(target.project_id);
3383
4029
  }
3384
- return {
3385
- ...task,
3386
- ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
3387
- tags: input.tags ?? task.tags,
3388
- metadata: input.metadata ?? task.metadata,
3389
- version: task.version + 1,
3390
- updated_at: now(),
3391
- completed_at: input.status === "completed" ? now() : task.completed_at,
3392
- requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
3393
- approved_by: input.approved_by ?? task.approved_by,
3394
- approved_at: input.approved_by ? now() : task.approved_at
3395
- };
4030
+ if (target.plan_id !== undefined) {
4031
+ sets.push("plan_id = ?");
4032
+ params.push(target.plan_id);
4033
+ }
4034
+ params.push(taskId);
4035
+ d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
4036
+ return getTask(taskId, d);
3396
4037
  }
3397
- function deleteTask(id, db) {
3398
- const d = db || getDatabase();
3399
- const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
3400
- return result.changes > 0;
4038
+ function wouldCreateCycle(taskId, dependsOn, db) {
4039
+ const visited = new Set;
4040
+ const queue = [dependsOn];
4041
+ while (queue.length > 0) {
4042
+ const current = queue.shift();
4043
+ if (current === taskId)
4044
+ return true;
4045
+ if (visited.has(current))
4046
+ continue;
4047
+ visited.add(current);
4048
+ const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
4049
+ for (const dep of deps) {
4050
+ queue.push(dep.depends_on);
4051
+ }
4052
+ }
4053
+ return false;
3401
4054
  }
4055
+
4056
+ // src/db/task-lifecycle.ts
4057
+ var MAX_SPAWN_DEPTH = 10;
3402
4058
  function getBlockingDeps(id, db) {
3403
4059
  const d = db || getDatabase();
3404
4060
  const deps = getTaskDependencies(id, d);
@@ -3453,14 +4109,21 @@ function completeTask(id, agentId, db, options) {
3453
4109
  completionMeta._completion = { confidence: options.confidence };
3454
4110
  }
3455
4111
  const hasMeta = Object.keys(completionMeta).length > 0;
3456
- if (hasMeta) {
3457
- const meta2 = { ...task.metadata, ...completionMeta };
3458
- d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
3459
- }
3460
4112
  const timestamp = now();
3461
4113
  const confidence = options?.confidence !== undefined ? options.confidence : null;
3462
- d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
3463
- WHERE id = ?`, [timestamp, confidence, timestamp, id]);
4114
+ const tx = d.transaction(() => {
4115
+ if (hasMeta) {
4116
+ const meta2 = { ...task.metadata, ...completionMeta };
4117
+ const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
4118
+ if (metaResult.changes === 0) {
4119
+ const current = getTask(id, d);
4120
+ throw new VersionConflictError(id, task.version, current?.version ?? -1);
4121
+ }
4122
+ }
4123
+ d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
4124
+ WHERE id = ?`, [timestamp, confidence, timestamp, id]);
4125
+ });
4126
+ tx();
3464
4127
  logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
3465
4128
  dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
3466
4129
  let spawnedTask = null;
@@ -3469,15 +4132,21 @@ function completeTask(id, agentId, db, options) {
3469
4132
  }
3470
4133
  let spawnedFromTemplate = null;
3471
4134
  if (task.spawns_template_id) {
3472
- try {
3473
- const input = taskFromTemplate(task.spawns_template_id, {
3474
- project_id: task.project_id ?? undefined,
3475
- plan_id: task.plan_id ?? undefined,
3476
- task_list_id: task.task_list_id ?? undefined,
3477
- assigned_to: task.assigned_to ?? undefined
3478
- }, d);
3479
- spawnedFromTemplate = createTask(input, d);
3480
- } catch {}
4135
+ const spawnDepth = task.metadata?._spawn_depth || 0;
4136
+ if (spawnDepth >= MAX_SPAWN_DEPTH) {
4137
+ console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
4138
+ } else {
4139
+ try {
4140
+ const input = taskFromTemplate(task.spawns_template_id, {
4141
+ project_id: task.project_id ?? undefined,
4142
+ plan_id: task.plan_id ?? undefined,
4143
+ task_list_id: task.task_list_id ?? undefined,
4144
+ assigned_to: task.assigned_to ?? undefined
4145
+ }, d);
4146
+ input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
4147
+ spawnedFromTemplate = createTask(input, d);
4148
+ } catch {}
4149
+ }
3481
4150
  }
3482
4151
  const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
3483
4152
  if (spawnedTask) {
@@ -3542,141 +4211,6 @@ function unlockTask(id, agentId, db) {
3542
4211
  WHERE id = ?`, [timestamp, id]);
3543
4212
  return true;
3544
4213
  }
3545
- function addDependency(taskId, dependsOn, db) {
3546
- const d = db || getDatabase();
3547
- if (!getTask(taskId, d))
3548
- throw new TaskNotFoundError(taskId);
3549
- if (!getTask(dependsOn, d))
3550
- throw new TaskNotFoundError(dependsOn);
3551
- if (wouldCreateCycle(taskId, dependsOn, d)) {
3552
- throw new DependencyCycleError(taskId, dependsOn);
3553
- }
3554
- d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
3555
- }
3556
- function removeDependency(taskId, dependsOn, db) {
3557
- const d = db || getDatabase();
3558
- const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
3559
- return result.changes > 0;
3560
- }
3561
- function getTaskDependencies(taskId, db) {
3562
- const d = db || getDatabase();
3563
- return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
3564
- }
3565
- function getTaskDependents(taskId, db) {
3566
- const d = db || getDatabase();
3567
- return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
3568
- }
3569
- function cloneTask(taskId, overrides, db) {
3570
- const d = db || getDatabase();
3571
- const source = getTask(taskId, d);
3572
- if (!source)
3573
- throw new TaskNotFoundError(taskId);
3574
- const input = {
3575
- title: overrides?.title ?? source.title,
3576
- description: overrides?.description ?? source.description ?? undefined,
3577
- priority: overrides?.priority ?? source.priority,
3578
- project_id: overrides?.project_id ?? source.project_id ?? undefined,
3579
- parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
3580
- plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
3581
- task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
3582
- status: overrides?.status ?? "pending",
3583
- agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
3584
- assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
3585
- tags: overrides?.tags ?? source.tags,
3586
- metadata: overrides?.metadata ?? source.metadata,
3587
- estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
3588
- recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
3589
- };
3590
- return createTask(input, d);
3591
- }
3592
- function getTaskGraph(taskId, direction = "both", db) {
3593
- const d = db || getDatabase();
3594
- const task = getTask(taskId, d);
3595
- if (!task)
3596
- throw new TaskNotFoundError(taskId);
3597
- function toNode(t) {
3598
- const deps = getTaskDependencies(t.id, d);
3599
- const hasUnfinishedDeps = deps.some((dep) => {
3600
- const depTask = getTask(dep.depends_on, d);
3601
- return depTask && depTask.status !== "completed";
3602
- });
3603
- return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
3604
- }
3605
- function buildUp(id, visited) {
3606
- if (visited.has(id))
3607
- return [];
3608
- visited.add(id);
3609
- const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
3610
- return deps.map((dep) => {
3611
- const depTask = getTask(dep.depends_on, d);
3612
- if (!depTask)
3613
- return null;
3614
- return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
3615
- }).filter(Boolean);
3616
- }
3617
- function buildDown(id, visited) {
3618
- if (visited.has(id))
3619
- return [];
3620
- visited.add(id);
3621
- const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
3622
- return dependents.map((dep) => {
3623
- const depTask = getTask(dep.task_id, d);
3624
- if (!depTask)
3625
- return null;
3626
- return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
3627
- }).filter(Boolean);
3628
- }
3629
- const rootNode = toNode(task);
3630
- const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
3631
- const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
3632
- return { task: rootNode, depends_on, blocks };
3633
- }
3634
- function moveTask(taskId, target, db) {
3635
- const d = db || getDatabase();
3636
- const task = getTask(taskId, d);
3637
- if (!task)
3638
- throw new TaskNotFoundError(taskId);
3639
- const sets = ["updated_at = ?", "version = version + 1"];
3640
- const params = [now()];
3641
- if (target.task_list_id !== undefined) {
3642
- sets.push("task_list_id = ?");
3643
- params.push(target.task_list_id);
3644
- }
3645
- if (target.project_id !== undefined) {
3646
- sets.push("project_id = ?");
3647
- params.push(target.project_id);
3648
- }
3649
- if (target.plan_id !== undefined) {
3650
- sets.push("plan_id = ?");
3651
- params.push(target.plan_id);
3652
- }
3653
- params.push(taskId);
3654
- d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
3655
- return getTask(taskId, d);
3656
- }
3657
- function spawnNextRecurrence(completedTask, db) {
3658
- const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
3659
- let title = completedTask.title;
3660
- if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
3661
- title = title.slice(completedTask.short_id.length + 2);
3662
- }
3663
- const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
3664
- return createTask({
3665
- title,
3666
- description: completedTask.description ?? undefined,
3667
- priority: completedTask.priority,
3668
- project_id: completedTask.project_id ?? undefined,
3669
- task_list_id: completedTask.task_list_id ?? undefined,
3670
- plan_id: completedTask.plan_id ?? undefined,
3671
- assigned_to: completedTask.assigned_to ?? undefined,
3672
- tags: completedTask.tags,
3673
- metadata: completedTask.metadata,
3674
- estimated_minutes: completedTask.estimated_minutes ?? undefined,
3675
- recurrence_rule: completedTask.recurrence_rule,
3676
- recurrence_parent_id: recurrenceParentId,
3677
- due_at: dueAt
3678
- }, db);
3679
- }
3680
4214
  function claimNextTask(agentId, filters, db) {
3681
4215
  const d = db || getDatabase();
3682
4216
  const tx = d.transaction(() => {
@@ -3849,10 +4383,6 @@ function getStaleTasks(staleMinutes = 30, filters, db) {
3849
4383
  const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
3850
4384
  return rows.map(rowToTask);
3851
4385
  }
3852
- function logCost(taskId, tokens, usd, db) {
3853
- const d = db || getDatabase();
3854
- d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
3855
- }
3856
4386
  function stealTask(agentId, opts, db) {
3857
4387
  const d = db || getDatabase();
3858
4388
  const staleMinutes = opts?.stale_minutes ?? 30;
@@ -3883,6 +4413,32 @@ function claimOrSteal(agentId, filters, db) {
3883
4413
  });
3884
4414
  return tx();
3885
4415
  }
4416
+ function spawnNextRecurrence(completedTask, db) {
4417
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
4418
+ let title = completedTask.title;
4419
+ if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
4420
+ title = title.slice(completedTask.short_id.length + 2);
4421
+ }
4422
+ const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
4423
+ return createTask({
4424
+ title,
4425
+ description: completedTask.description ?? undefined,
4426
+ priority: completedTask.priority,
4427
+ project_id: completedTask.project_id ?? undefined,
4428
+ task_list_id: completedTask.task_list_id ?? undefined,
4429
+ plan_id: completedTask.plan_id ?? undefined,
4430
+ assigned_to: completedTask.assigned_to ?? undefined,
4431
+ tags: completedTask.tags,
4432
+ metadata: completedTask.metadata,
4433
+ estimated_minutes: completedTask.estimated_minutes ?? undefined,
4434
+ recurrence_rule: completedTask.recurrence_rule,
4435
+ recurrence_parent_id: recurrenceParentId,
4436
+ due_at: dueAt
4437
+ }, db);
4438
+ }
4439
+ // src/db/task-status.ts
4440
+ init_types();
4441
+ init_database();
3886
4442
  function getStatus(filters, agentId, options, db) {
3887
4443
  const d = db || getDatabase();
3888
4444
  const pending = countTasks({ ...filters, status: "pending" }, d);
@@ -4011,23 +4567,6 @@ function redistributeStaleTasks(agentId, options, db) {
4011
4567
  const claimed = released.length > 0 ? claimNextTask(agentId, options?.project_id ? { project_id: options.project_id } : undefined, d) : null;
4012
4568
  return { released, claimed };
4013
4569
  }
4014
- function wouldCreateCycle(taskId, dependsOn, db) {
4015
- const visited = new Set;
4016
- const queue = [dependsOn];
4017
- while (queue.length > 0) {
4018
- const current = queue.shift();
4019
- if (current === taskId)
4020
- return true;
4021
- if (visited.has(current))
4022
- continue;
4023
- visited.add(current);
4024
- const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
4025
- for (const dep of deps) {
4026
- queue.push(dep.depends_on);
4027
- }
4028
- }
4029
- return false;
4030
- }
4031
4570
  function getTaskStats(filters, db) {
4032
4571
  const d = db || getDatabase();
4033
4572
  const conditions = [];
@@ -4062,6 +4601,8 @@ function getTaskStats(filters, db) {
4062
4601
  const completion_rate = totalRow.count > 0 ? Math.round(completed / totalRow.count * 100) : 0;
4063
4602
  return { total: totalRow.count, by_status, by_priority, completion_rate, by_agent };
4064
4603
  }
4604
+ // src/db/task-relations.ts
4605
+ init_database();
4065
4606
  function bulkCreateTasks(inputs, db) {
4066
4607
  const d = db || getDatabase();
4067
4608
  const tempIdToRealId = new Map;
@@ -4155,6 +4696,10 @@ function getOverdueTasks(projectId, db) {
4155
4696
  const rows = d.query(query).all(...params);
4156
4697
  return rows.map(rowToTask);
4157
4698
  }
4699
+ function logCost(taskId, tokens, usd, db) {
4700
+ const d = db || getDatabase();
4701
+ d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
4702
+ }
4158
4703
  // src/db/plans.ts
4159
4704
  init_types();
4160
4705
  init_database();
@@ -6266,7 +6811,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
6266
6811
  }
6267
6812
  return { pushed, pulled, errors };
6268
6813
  }
6269
- // ../../../../node_modules/@hasna/cloud/dist/index.js
6814
+ // node_modules/@hasna/cloud/dist/index.js
6270
6815
  import { createRequire } from "module";
6271
6816
  import { Database as Database2 } from "bun:sqlite";
6272
6817
  import {
@@ -6290,9 +6835,9 @@ import { homedir as homedir5, platform } from "os";
6290
6835
  var __create = Object.create;
6291
6836
  var __getProtoOf = Object.getPrototypeOf;
6292
6837
  var __defProp2 = Object.defineProperty;
6293
- var __getOwnPropNames = Object.getOwnPropertyNames;
6294
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6295
- function __accessProp(key) {
6838
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
6839
+ var __hasOwnProp2 = Object.prototype.hasOwnProperty;
6840
+ function __accessProp2(key) {
6296
6841
  return this[key];
6297
6842
  }
6298
6843
  var __toESMCache_node;
@@ -6307,10 +6852,10 @@ var __toESM = (mod, isNodeMode, target) => {
6307
6852
  }
6308
6853
  target = mod != null ? __create(__getProtoOf(mod)) : {};
6309
6854
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target;
6310
- for (let key of __getOwnPropNames(mod))
6311
- if (!__hasOwnProp.call(to, key))
6855
+ for (let key of __getOwnPropNames2(mod))
6856
+ if (!__hasOwnProp2.call(to, key))
6312
6857
  __defProp2(to, key, {
6313
- get: __accessProp.bind(mod, key),
6858
+ get: __accessProp2.bind(mod, key),
6314
6859
  enumerable: true
6315
6860
  });
6316
6861
  if (canCache)
@@ -6318,9 +6863,9 @@ var __toESM = (mod, isNodeMode, target) => {
6318
6863
  return to;
6319
6864
  };
6320
6865
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
6321
- var __returnValue = (v) => v;
6322
- function __exportSetter(name, newValue) {
6323
- this[name] = __returnValue.bind(null, newValue);
6866
+ var __returnValue2 = (v) => v;
6867
+ function __exportSetter2(name, newValue) {
6868
+ this[name] = __returnValue2.bind(null, newValue);
6324
6869
  }
6325
6870
  var __export2 = (target, all) => {
6326
6871
  for (var name in all)
@@ -6328,7 +6873,7 @@ var __export2 = (target, all) => {
6328
6873
  get: all[name],
6329
6874
  enumerable: true,
6330
6875
  configurable: true,
6331
- set: __exportSetter.bind(all, name)
6876
+ set: __exportSetter2.bind(all, name)
6332
6877
  });
6333
6878
  };
6334
6879
  var __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -16459,6 +17004,45 @@ var PG_MIGRATIONS = [
16459
17004
  ALTER TABLE task_dependencies ADD COLUMN external_project_id TEXT;
16460
17005
  ALTER TABLE task_dependencies ADD COLUMN external_task_id TEXT;
16461
17006
  INSERT INTO _migrations (id) VALUES (43) ON CONFLICT DO NOTHING;
17007
+ `,
17008
+ `
17009
+ CREATE TABLE IF NOT EXISTS task_checkpoints (
17010
+ id TEXT PRIMARY KEY,
17011
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
17012
+ agent_id TEXT,
17013
+ step TEXT NOT NULL,
17014
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed', 'skipped')),
17015
+ data TEXT DEFAULT '{}',
17016
+ error TEXT,
17017
+ attempt INTEGER NOT NULL DEFAULT 1,
17018
+ max_attempts INTEGER NOT NULL DEFAULT 1,
17019
+ started_at TIMESTAMPTZ,
17020
+ completed_at TIMESTAMPTZ,
17021
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
17022
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
17023
+ );
17024
+ CREATE INDEX IF NOT EXISTS idx_task_checkpoints_task ON task_checkpoints(task_id);
17025
+ CREATE INDEX IF NOT EXISTS idx_task_checkpoints_status ON task_checkpoints(status);
17026
+
17027
+ CREATE TABLE IF NOT EXISTS task_heartbeats (
17028
+ id TEXT PRIMARY KEY,
17029
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
17030
+ agent_id TEXT,
17031
+ step TEXT,
17032
+ message TEXT,
17033
+ progress REAL CHECK(progress >= 0 AND progress <= 1),
17034
+ meta TEXT DEFAULT '{}',
17035
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
17036
+ );
17037
+ CREATE INDEX IF NOT EXISTS idx_task_heartbeats_task ON task_heartbeats(task_id);
17038
+ CREATE INDEX IF NOT EXISTS idx_task_heartbeats_agent ON task_heartbeats(agent_id);
17039
+
17040
+ ALTER TABLE tasks ADD COLUMN runner_id TEXT;
17041
+ ALTER TABLE tasks ADD COLUMN runner_started_at TIMESTAMPTZ;
17042
+ ALTER TABLE tasks ADD COLUMN runner_completed_at TIMESTAMPTZ;
17043
+ ALTER TABLE tasks ADD COLUMN current_step TEXT;
17044
+ ALTER TABLE tasks ADD COLUMN total_steps INTEGER;
17045
+ INSERT INTO _migrations (id) VALUES (44) ON CONFLICT DO NOTHING;
16462
17046
  `
16463
17047
  ];
16464
17048
 
@@ -16772,15 +17356,24 @@ function renderBurndownChart(total, days) {
16772
17356
  `);
16773
17357
  }
16774
17358
  // src/lib/github.ts
16775
- import { execSync } from "child_process";
17359
+ import { execFileSync } from "child_process";
16776
17360
  function parseGitHubUrl(url) {
16777
- const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
17361
+ const match = url.match(/github\.com\/([a-zA-Z0-9](?:[a-zA-Z0-9._-]{0,38}[a-zA-Z0-9])?)\/([a-zA-Z0-9._-]+)\/issues\/(\d+)/);
16778
17362
  if (!match)
16779
17363
  return null;
16780
17364
  return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
16781
17365
  }
17366
+ function isSafeOwnerRepo(value) {
17367
+ return /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value) && value.length <= 39;
17368
+ }
16782
17369
  function fetchGitHubIssue(owner, repo, number) {
16783
- const json = execSync(`gh api repos/${owner}/${repo}/issues/${number}`, { encoding: "utf-8", timeout: 15000 });
17370
+ if (!isSafeOwnerRepo(owner))
17371
+ throw new Error(`Invalid GitHub owner: ${owner}`);
17372
+ if (!isSafeOwnerRepo(repo))
17373
+ throw new Error(`Invalid GitHub repo: ${repo}`);
17374
+ if (!Number.isInteger(number) || number < 1)
17375
+ throw new Error(`Invalid issue number: ${number}`);
17376
+ const json = execFileSync("gh", ["api", `repos/${owner}/${repo}/issues/${number}`], { encoding: "utf-8", timeout: 15000 });
16784
17377
  const data = JSON.parse(json);
16785
17378
  return {
16786
17379
  number: data.number,