@hasna/todos 0.11.29 → 0.11.31

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 (75) hide show
  1. package/dist/cli/commands/machines.d.ts +3 -0
  2. package/dist/cli/commands/machines.d.ts.map +1 -0
  3. package/dist/cli/index.js +26812 -28466
  4. package/dist/db/checkpoints.d.ts +55 -0
  5. package/dist/db/checkpoints.d.ts.map +1 -0
  6. package/dist/db/database.d.ts.map +1 -1
  7. package/dist/db/machines.d.ts +30 -1
  8. package/dist/db/machines.d.ts.map +1 -1
  9. package/dist/db/migrations.d.ts +2 -0
  10. package/dist/db/migrations.d.ts.map +1 -0
  11. package/dist/db/pg-migrations.d.ts.map +1 -1
  12. package/dist/db/schema.d.ts +0 -1
  13. package/dist/db/schema.d.ts.map +1 -1
  14. package/dist/db/task-crud.d.ts +13 -0
  15. package/dist/db/task-crud.d.ts.map +1 -0
  16. package/dist/db/task-graph.d.ts +27 -0
  17. package/dist/db/task-graph.d.ts.map +1 -0
  18. package/dist/db/task-lifecycle.d.ts +80 -0
  19. package/dist/db/task-lifecycle.d.ts.map +1 -0
  20. package/dist/db/task-relations.d.ts +86 -0
  21. package/dist/db/task-relations.d.ts.map +1 -0
  22. package/dist/db/task-status.d.ts +65 -0
  23. package/dist/db/task-status.d.ts.map +1 -0
  24. package/dist/db/tasks.d.ts +10 -255
  25. package/dist/db/tasks.d.ts.map +1 -1
  26. package/dist/db/webhooks.d.ts +6 -1
  27. package/dist/db/webhooks.d.ts.map +1 -1
  28. package/dist/index.js +2400 -1791
  29. package/dist/lib/github.d.ts +1 -1
  30. package/dist/lib/github.d.ts.map +1 -1
  31. package/dist/lib/logger.d.ts +17 -0
  32. package/dist/lib/logger.d.ts.map +1 -0
  33. package/dist/lib/north-star.d.ts +33 -0
  34. package/dist/lib/north-star.d.ts.map +1 -0
  35. package/dist/lib/task-runner.d.ts +101 -0
  36. package/dist/lib/task-runner.d.ts.map +1 -0
  37. package/dist/mcp/index.d.ts.map +1 -1
  38. package/dist/mcp/index.js +5423 -9138
  39. package/dist/mcp/tools/cloud.d.ts.map +1 -1
  40. package/dist/mcp/tools/code-tools.d.ts +15 -0
  41. package/dist/mcp/tools/code-tools.d.ts.map +1 -0
  42. package/dist/mcp/tools/machines.d.ts +8 -0
  43. package/dist/mcp/tools/machines.d.ts.map +1 -0
  44. package/dist/mcp/tools/task-adv-tools.d.ts +20 -0
  45. package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -0
  46. package/dist/mcp/tools/task-auto-tools.d.ts +20 -0
  47. package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -0
  48. package/dist/mcp/tools/task-crud.d.ts +20 -0
  49. package/dist/mcp/tools/task-crud.d.ts.map +1 -0
  50. package/dist/mcp/tools/task-meta-tools.d.ts +12 -0
  51. package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -0
  52. package/dist/mcp/tools/task-project-tools.d.ts +20 -0
  53. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -0
  54. package/dist/mcp/tools/task-rel-tools.d.ts +19 -0
  55. package/dist/mcp/tools/task-rel-tools.d.ts.map +1 -0
  56. package/dist/mcp/tools/task-resources.d.ts +13 -0
  57. package/dist/mcp/tools/task-resources.d.ts.map +1 -0
  58. package/dist/mcp/tools/task-workflow-tools.d.ts +20 -0
  59. package/dist/mcp/tools/task-workflow-tools.d.ts.map +1 -0
  60. package/dist/sdk/client.d.ts +361 -0
  61. package/dist/sdk/client.d.ts.map +1 -0
  62. package/dist/sdk/index.d.ts +14 -0
  63. package/dist/sdk/index.d.ts.map +1 -0
  64. package/dist/sdk/types.d.ts +167 -0
  65. package/dist/sdk/types.d.ts.map +1 -0
  66. package/dist/sdk.d.ts +15 -182
  67. package/dist/sdk.d.ts.map +1 -1
  68. package/dist/server/index.js +1968 -2804
  69. package/dist/server/routes.d.ts +85 -0
  70. package/dist/server/routes.d.ts.map +1 -0
  71. package/dist/server/serve.d.ts +29 -0
  72. package/dist/server/serve.d.ts.map +1 -1
  73. package/dist/types/index.d.ts +83 -0
  74. package/dist/types/index.d.ts.map +1 -1
  75. package/package.json +3 -2
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,15 +769,465 @@ 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
+ ssh_address TEXT, is_primary INTEGER NOT NULL DEFAULT 0,
992
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
993
+ archived_at TEXT,
994
+ metadata TEXT DEFAULT '{}',
995
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
996
+ )`);
997
+ ensureColumn("machines", "ssh_address", "TEXT");
998
+ ensureColumn("machines", "is_primary", "INTEGER NOT NULL DEFAULT 0");
999
+ ensureColumn("machines", "archived_at", "TEXT");
1000
+ ensureColumn("projects", "task_list_id", "TEXT");
1001
+ ensureColumn("projects", "task_prefix", "TEXT");
1002
+ ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
1003
+ ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
1004
+ ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
1005
+ ensureColumn("tasks", "short_id", "TEXT");
1006
+ ensureColumn("tasks", "due_at", "TEXT");
1007
+ ensureColumn("tasks", "estimated_minutes", "INTEGER");
1008
+ ensureColumn("tasks", "requires_approval", "INTEGER NOT NULL DEFAULT 0");
1009
+ ensureColumn("tasks", "approved_by", "TEXT");
1010
+ ensureColumn("tasks", "approved_at", "TEXT");
1011
+ ensureColumn("tasks", "recurrence_rule", "TEXT");
1012
+ ensureColumn("tasks", "recurrence_parent_id", "TEXT REFERENCES tasks(id) ON DELETE SET NULL");
1013
+ ensureColumn("tasks", "confidence", "REAL");
1014
+ ensureColumn("tasks", "reason", "TEXT");
1015
+ ensureColumn("tasks", "spawned_from_session", "TEXT");
1016
+ ensureColumn("tasks", "assigned_by", "TEXT");
1017
+ ensureColumn("tasks", "assigned_from_project", "TEXT");
1018
+ ensureColumn("tasks", "started_at", "TEXT");
1019
+ ensureColumn("tasks", "task_type", "TEXT");
1020
+ ensureColumn("tasks", "cost_tokens", "INTEGER DEFAULT 0");
1021
+ ensureColumn("tasks", "cost_usd", "REAL DEFAULT 0");
1022
+ ensureColumn("tasks", "delegated_from", "TEXT");
1023
+ ensureColumn("tasks", "delegation_depth", "INTEGER DEFAULT 0");
1024
+ ensureColumn("tasks", "retry_count", "INTEGER DEFAULT 0");
1025
+ ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
1026
+ ensureColumn("tasks", "retry_after", "TEXT");
1027
+ ensureColumn("tasks", "sla_minutes", "INTEGER");
1028
+ ensureColumn("tasks", "archived_at", "TEXT");
1029
+ ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
1030
+ ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
1031
+ ensureColumn("agents", "reports_to", "TEXT");
1032
+ ensureColumn("agents", "title", "TEXT");
1033
+ ensureColumn("agents", "level", "TEXT");
1034
+ ensureColumn("agents", "org_id", "TEXT");
1035
+ ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
1036
+ ensureColumn("projects", "org_id", "TEXT");
1037
+ ensureColumn("plans", "task_list_id", "TEXT");
1038
+ ensureColumn("plans", "agent_id", "TEXT");
1039
+ ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
1040
+ ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
1041
+ ensureColumn("template_tasks", "condition", "TEXT");
1042
+ ensureColumn("template_tasks", "include_template_id", "TEXT");
1043
+ ensureTable("template_versions", `
1044
+ CREATE TABLE template_versions (
1045
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
1046
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
1047
+ version INTEGER NOT NULL,
1048
+ snapshot TEXT NOT NULL,
1049
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1050
+ )`);
1051
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id)");
1052
+ ensureTable("dispatches", `
1053
+ CREATE TABLE dispatches (
1054
+ id TEXT PRIMARY KEY,
1055
+ title TEXT,
1056
+ target_window TEXT NOT NULL,
1057
+ task_ids TEXT NOT NULL DEFAULT '[]',
1058
+ task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL,
1059
+ message TEXT,
1060
+ delay_ms INTEGER,
1061
+ scheduled_at TEXT,
1062
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'sent', 'failed', 'cancelled')),
1063
+ error TEXT,
1064
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1065
+ sent_at TEXT
1066
+ )`);
1067
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_status ON dispatches(status)");
1068
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_scheduled ON dispatches(scheduled_at)");
1069
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_task_list ON dispatches(task_list_id)");
1070
+ ensureTable("dispatch_logs", `
1071
+ CREATE TABLE dispatch_logs (
1072
+ id TEXT PRIMARY KEY,
1073
+ dispatch_id TEXT NOT NULL REFERENCES dispatches(id) ON DELETE CASCADE,
1074
+ target_window TEXT NOT NULL,
1075
+ message TEXT NOT NULL,
1076
+ delay_ms INTEGER NOT NULL,
1077
+ status TEXT NOT NULL CHECK(status IN ('sent', 'failed')),
1078
+ error TEXT,
1079
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1080
+ )`);
1081
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatch_logs_dispatch ON dispatch_logs(dispatch_id)");
1082
+ ensureColumn("webhooks", "project_id", "TEXT");
1083
+ ensureColumn("webhooks", "task_list_id", "TEXT");
1084
+ ensureColumn("webhooks", "agent_id", "TEXT");
1085
+ ensureColumn("webhooks", "task_id", "TEXT");
1086
+ ensureTable("webhook_deliveries", `
1087
+ CREATE TABLE webhook_deliveries (
1088
+ id TEXT PRIMARY KEY,
1089
+ webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
1090
+ event TEXT NOT NULL,
1091
+ payload TEXT NOT NULL,
1092
+ status_code INTEGER,
1093
+ response TEXT,
1094
+ attempt INTEGER NOT NULL DEFAULT 1,
1095
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1096
+ )`);
1097
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id)");
1098
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
1099
+ ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
1100
+ ensureColumn("task_comments", "progress_pct", "INTEGER");
1101
+ ensureColumn("projects", "machine_id", "TEXT");
1102
+ ensureColumn("projects", "synced_at", "TEXT");
1103
+ ensureColumn("tasks", "machine_id", "TEXT");
1104
+ ensureColumn("tasks", "synced_at", "TEXT");
1105
+ ensureColumn("agents", "machine_id", "TEXT");
1106
+ ensureColumn("agents", "synced_at", "TEXT");
1107
+ ensureColumn("task_lists", "machine_id", "TEXT");
1108
+ ensureColumn("task_lists", "synced_at", "TEXT");
1109
+ ensureColumn("plans", "machine_id", "TEXT");
1110
+ ensureColumn("plans", "synced_at", "TEXT");
1111
+ ensureColumn("task_comments", "machine_id", "TEXT");
1112
+ ensureColumn("task_comments", "synced_at", "TEXT");
1113
+ ensureColumn("sessions", "machine_id", "TEXT");
1114
+ ensureColumn("sessions", "synced_at", "TEXT");
1115
+ ensureColumn("task_history", "machine_id", "TEXT");
1116
+ ensureColumn("webhooks", "machine_id", "TEXT");
1117
+ ensureColumn("webhooks", "synced_at", "TEXT");
1118
+ ensureColumn("task_templates", "machine_id", "TEXT");
1119
+ ensureColumn("task_templates", "synced_at", "TEXT");
1120
+ ensureColumn("orgs", "machine_id", "TEXT");
1121
+ ensureColumn("orgs", "synced_at", "TEXT");
1122
+ ensureColumn("handoffs", "machine_id", "TEXT");
1123
+ ensureColumn("handoffs", "synced_at", "TEXT");
1124
+ ensureColumn("task_checklists", "machine_id", "TEXT");
1125
+ ensureColumn("project_sources", "machine_id", "TEXT");
1126
+ ensureColumn("project_sources", "synced_at", "TEXT");
1127
+ ensureColumn("task_files", "machine_id", "TEXT");
1128
+ ensureColumn("task_relationships", "machine_id", "TEXT");
1129
+ ensureColumn("kg_edges", "machine_id", "TEXT");
1130
+ ensureColumn("project_agent_roles", "machine_id", "TEXT");
1131
+ ensureColumn("dispatches", "machine_id", "TEXT");
1132
+ ensureColumn("dispatches", "synced_at", "TEXT");
1133
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
1134
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id)");
1135
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at)");
1136
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL");
1137
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name)");
1138
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_project ON task_lists(project_id)");
1139
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug)");
1140
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag)");
1141
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id)");
1142
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
1143
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
1144
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
1145
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
1146
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
1147
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(task_id)");
1148
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id)");
1149
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL");
1150
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_checklists_task ON task_checklists(task_id)");
1151
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id)");
1152
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type)");
1153
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by)");
1154
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id)");
1155
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id)");
1156
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type)");
1157
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id)");
1158
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type)");
1159
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type)");
1160
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type)");
1161
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_machine ON tasks(machine_id)");
1162
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_synced ON tasks(synced_at)");
1163
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_machine ON projects(machine_id)");
1164
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_machine ON agents(machine_id)");
1165
+ ensureTable("task_time_logs", `
1166
+ CREATE TABLE task_time_logs (
1167
+ id TEXT PRIMARY KEY,
1168
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1169
+ agent_id TEXT,
1170
+ started_at TEXT,
1171
+ ended_at TEXT,
1172
+ minutes INTEGER NOT NULL,
1173
+ notes TEXT,
1174
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1175
+ )`);
1176
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_task ON task_time_logs(task_id)");
1177
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_agent ON task_time_logs(agent_id)");
1178
+ ensureColumn("tasks", "actual_minutes", "INTEGER");
1179
+ ensureTable("task_watchers", `
1180
+ CREATE TABLE task_watchers (
1181
+ id TEXT PRIMARY KEY,
1182
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1183
+ agent_id TEXT NOT NULL,
1184
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1185
+ UNIQUE(task_id, agent_id)
1186
+ )`);
1187
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_task ON task_watchers(task_id)");
1188
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_agent ON task_watchers(agent_id)");
1189
+ ensureColumn("task_dependencies", "external_project_id", "TEXT");
1190
+ ensureColumn("task_dependencies", "external_task_id", "TEXT");
1191
+ }
1192
+ function backfillTaskTags(db) {
1193
+ try {
1194
+ const count = db.query("SELECT COUNT(*) as count FROM task_tags").get();
1195
+ if (count && count.count > 0)
1196
+ return;
1197
+ } catch {
1198
+ return;
1199
+ }
1200
+ try {
1201
+ const rows = db.query("SELECT id, tags FROM tasks WHERE tags IS NOT NULL AND tags != '[]'").all();
1202
+ if (rows.length === 0)
1203
+ return;
1204
+ const insert = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
1205
+ for (const row of rows) {
1206
+ if (!row.tags)
1207
+ continue;
1208
+ let tags = [];
1209
+ try {
1210
+ tags = JSON.parse(row.tags);
1211
+ } catch {
1212
+ continue;
1213
+ }
1214
+ for (const tag of tags) {
1215
+ if (tag)
1216
+ insert.run(row.id, tag);
1217
+ }
1218
+ }
1219
+ } catch {}
1220
+ }
1221
+ var init_schema = __esm(() => {
1222
+ init_migrations();
1223
+ });
1224
+
1149
1225
  // src/db/machines.ts
1150
1226
  import { hostname as osHostname, platform as osPlatform } from "os";
1151
1227
  function rowToMachine(row) {
1152
1228
  return {
1153
1229
  ...row,
1230
+ is_primary: !!row.is_primary,
1154
1231
  metadata: row.metadata ? JSON.parse(row.metadata) : {}
1155
1232
  };
1156
1233
  }
@@ -1167,7 +1244,7 @@ function getOrCreateLocalMachine(db) {
1167
1244
  const id = uuid();
1168
1245
  const ts = now();
1169
1246
  d.run("INSERT INTO machines (id, name, hostname, platform, last_seen_at, metadata, created_at) VALUES (?, ?, ?, ?, ?, '{}', ?)", [id, name, host, plat, ts, ts]);
1170
- return { id, name, hostname: host, platform: plat, last_seen_at: ts, metadata: {}, created_at: ts };
1247
+ return { id, name, hostname: host, platform: plat, ssh_address: null, is_primary: false, last_seen_at: ts, archived_at: null, metadata: {}, created_at: ts };
1171
1248
  }
1172
1249
  function getMachineId(db) {
1173
1250
  if (_machineId)
@@ -1189,13 +1266,23 @@ function getMachineByName(name, db) {
1189
1266
  const row = d.query("SELECT * FROM machines WHERE name = ?").get(name);
1190
1267
  return row ? rowToMachine(row) : null;
1191
1268
  }
1192
- function listMachines(db) {
1269
+ function listMachines(db, includeArchived = false) {
1193
1270
  const d = db || getDatabase();
1194
- const rows = d.query("SELECT * FROM machines ORDER BY last_seen_at DESC").all();
1271
+ const query = includeArchived ? "SELECT * FROM machines ORDER BY last_seen_at DESC" : "SELECT * FROM machines WHERE archived_at IS NULL ORDER BY last_seen_at DESC";
1272
+ const rows = d.query(query).all();
1195
1273
  return rows.map(rowToMachine);
1196
1274
  }
1197
1275
  function deleteMachine(id, db) {
1198
1276
  const d = db || getDatabase();
1277
+ const row = d.query("SELECT * FROM machines WHERE id = ?").get(id);
1278
+ if (!row)
1279
+ return false;
1280
+ if (row.is_primary)
1281
+ throw new Error("Cannot delete the primary machine");
1282
+ const activeCount = d.query("SELECT COUNT(*) as cnt FROM tasks WHERE machine_id = ? AND status IN ('pending', 'in_progress')").get(id);
1283
+ if (activeCount.cnt > 0) {
1284
+ throw new Error(`Cannot delete machine with ${activeCount.cnt} active/pending tasks`);
1285
+ }
1199
1286
  const result = d.run("DELETE FROM machines WHERE id = ?", [id]);
1200
1287
  return result.changes > 0;
1201
1288
  }
@@ -1238,6 +1325,19 @@ var init_machines = __esm(() => {
1238
1325
  });
1239
1326
 
1240
1327
  // src/db/database.ts
1328
+ var exports_database = {};
1329
+ __export(exports_database, {
1330
+ uuid: () => uuid,
1331
+ resolvePartialId: () => resolvePartialId,
1332
+ resetDatabase: () => resetDatabase,
1333
+ now: () => now,
1334
+ lockExpiryCutoff: () => lockExpiryCutoff,
1335
+ isLockExpired: () => isLockExpired,
1336
+ getDatabase: () => getDatabase,
1337
+ closeDatabase: () => closeDatabase,
1338
+ clearExpiredLocks: () => clearExpiredLocks,
1339
+ LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
1340
+ });
1241
1341
  import { Database } from "bun:sqlite";
1242
1342
  import { existsSync, mkdirSync } from "fs";
1243
1343
  import { dirname, join, resolve } from "path";
@@ -1347,6 +1447,9 @@ function clearExpiredLocks(db) {
1347
1447
  db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
1348
1448
  }
1349
1449
  function resolvePartialId(db, table, partialId) {
1450
+ if (!ALLOWED_TABLES.has(table)) {
1451
+ throw new Error(`Invalid table name: ${table}`);
1452
+ }
1350
1453
  if (partialId.length >= 36) {
1351
1454
  const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
1352
1455
  return row?.id ?? null;
@@ -1376,10 +1479,11 @@ function resolvePartialId(db, table, partialId) {
1376
1479
  }
1377
1480
  return null;
1378
1481
  }
1379
- var LOCK_EXPIRY_MINUTES = 30, _db = null;
1482
+ var LOCK_EXPIRY_MINUTES = 30, _db = null, ALLOWED_TABLES;
1380
1483
  var init_database = __esm(() => {
1381
1484
  init_schema();
1382
1485
  init_machines();
1486
+ ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates"]);
1383
1487
  });
1384
1488
 
1385
1489
  // src/types/index.ts
@@ -1612,231 +1716,572 @@ function getDueDispatches(db) {
1612
1716
  ORDER BY created_at ASC`).all(now());
1613
1717
  return rows.map(rowToDispatch);
1614
1718
  }
1615
- var init_dispatches = __esm(() => {
1616
- init_database();
1617
- init_types();
1618
- });
1719
+ var init_dispatches = __esm(() => {
1720
+ init_database();
1721
+ init_types();
1722
+ });
1723
+
1724
+ // src/sdk/types.ts
1725
+ class TodosAPIError extends Error {
1726
+ status;
1727
+ statusText;
1728
+ body;
1729
+ constructor(message, status, statusText, body) {
1730
+ super(message);
1731
+ this.status = status;
1732
+ this.statusText = statusText;
1733
+ this.body = body;
1734
+ this.name = "TodosAPIError";
1735
+ }
1736
+ }
1737
+
1738
+ class TodosNotFoundError extends TodosAPIError {
1739
+ constructor(message, body) {
1740
+ super(message, 404, "Not Found", body);
1741
+ this.name = "TodosNotFoundError";
1742
+ }
1743
+ }
1744
+
1745
+ class TodosConflictError extends TodosAPIError {
1746
+ constructor(message, body) {
1747
+ super(message, 409, "Conflict", body);
1748
+ this.name = "TodosConflictError";
1749
+ }
1750
+ }
1751
+
1752
+ class TodosUnauthorizedError extends TodosAPIError {
1753
+ constructor(message, body) {
1754
+ super(message, 401, "Unauthorized", body);
1755
+ this.name = "TodosUnauthorizedError";
1756
+ }
1757
+ }
1758
+
1759
+ class TodosRateLimitError extends TodosAPIError {
1760
+ retryAfter;
1761
+ constructor(message, retryAfter, body) {
1762
+ super(message, 429, "Too Many Requests", body);
1763
+ this.retryAfter = retryAfter;
1764
+ this.name = "TodosRateLimitError";
1765
+ }
1766
+ }
1767
+
1768
+ class TodosTimeoutError extends Error {
1769
+ ms;
1770
+ constructor(ms) {
1771
+ super(`Request timed out after ${ms}ms`);
1772
+ this.ms = ms;
1773
+ this.name = "TodosTimeoutError";
1774
+ }
1775
+ }
1776
+
1777
+ // src/sdk/client.ts
1778
+ function buildQuery(params) {
1779
+ const search = new URLSearchParams;
1780
+ for (const [k, v] of Object.entries(params)) {
1781
+ if (v !== undefined)
1782
+ search.set(k, String(v));
1783
+ }
1784
+ const s = search.toString();
1785
+ return s ? `?${s}` : "";
1786
+ }
1787
+
1788
+ class TasksResource {
1789
+ client;
1790
+ constructor(client) {
1791
+ this.client = client;
1792
+ }
1793
+ async list(options) {
1794
+ return this.client._get("/api/tasks", buildQuery(options || {}));
1795
+ }
1796
+ async get(id) {
1797
+ return this.client._get(`/api/tasks/${id}`);
1798
+ }
1799
+ async getWithRelations(id) {
1800
+ return this.client._get(`/api/tasks/${id}`);
1801
+ }
1802
+ async create(data) {
1803
+ return this.client._post("/api/tasks", data);
1804
+ }
1805
+ async update(id, data) {
1806
+ return this.client._patch(`/api/tasks/${id}`, data);
1807
+ }
1808
+ async delete(id) {
1809
+ return this.client._delete(`/api/tasks/${id}`);
1810
+ }
1811
+ async start(id, agentId) {
1812
+ return this.client._post(`/api/tasks/${id}/start`, { agent_id: agentId });
1813
+ }
1814
+ async complete(id, agentId) {
1815
+ return this.client._post(`/api/tasks/${id}/complete`, { agent_id: agentId });
1816
+ }
1817
+ async fail(id, options) {
1818
+ return this.client._post(`/api/tasks/${id}/fail`, options || {});
1819
+ }
1820
+ async logProgress(taskId, message, pctComplete, agentId) {
1821
+ return this.client._post(`/api/tasks/${taskId}/progress`, {
1822
+ message,
1823
+ pct_complete: pctComplete,
1824
+ agent_id: agentId
1825
+ });
1826
+ }
1827
+ async getProgress(id) {
1828
+ return this.client._get(`/api/tasks/${id}/progress`);
1829
+ }
1830
+ async getHistory(id) {
1831
+ return this.client._get(`/api/tasks/${id}/history`);
1832
+ }
1833
+ async getAttachments(id) {
1834
+ return this.client._get(`/api/tasks/${id}/attachments`);
1835
+ }
1836
+ async status(options) {
1837
+ return this.client._get("/api/tasks/status", buildQuery(options || {}));
1838
+ }
1839
+ async next(options) {
1840
+ return this.client._get("/api/tasks/next", buildQuery(options || {}));
1841
+ }
1842
+ async active(projectId) {
1843
+ return this.client._get("/api/tasks/active", projectId ? `?project_id=${projectId}` : "");
1844
+ }
1845
+ async stale(options) {
1846
+ return this.client._get("/api/tasks/stale", buildQuery(options || {}));
1847
+ }
1848
+ async changedSince(since, projectId) {
1849
+ return this.client._get("/api/tasks/changed", buildQuery({ since, project_id: projectId }));
1850
+ }
1851
+ async context(options) {
1852
+ const q = buildQuery({
1853
+ agent_id: options?.agentId,
1854
+ project_id: options?.projectId,
1855
+ format: options?.format
1856
+ });
1857
+ const url = `${this.client.baseUrl}/api/tasks/context${q}`;
1858
+ const res = await this.client._fetchRaw(url);
1859
+ if (!res.ok)
1860
+ return options?.format === "json" ? {} : "";
1861
+ if (options?.format === "json")
1862
+ return res.json();
1863
+ return res.text();
1864
+ }
1865
+ async export(options) {
1866
+ const q = buildQuery(options || {});
1867
+ const url = `${this.client.baseUrl}/api/tasks/export${q}`;
1868
+ const res = await this.client._fetchRaw(url);
1869
+ if (!res.ok)
1870
+ throw new TodosAPIError("Export failed", res.status, res.statusText, null);
1871
+ if (options?.format === "csv")
1872
+ return res.text();
1873
+ return res.json();
1874
+ }
1875
+ async bulk(ids, action) {
1876
+ return this.client._post("/api/tasks/bulk", { ids, action });
1877
+ }
1878
+ async claim(agentId, projectId) {
1879
+ return this.client._post("/api/tasks/claim", { agent_id: agentId, project_id: projectId });
1880
+ }
1881
+ async* subscribe(options = {}) {
1882
+ const q = buildQuery({
1883
+ agent_id: options.agentId,
1884
+ project_id: options.projectId,
1885
+ events: options.events?.join(",")
1886
+ });
1887
+ const url = `${this.client.baseUrl}/api/tasks/stream${q}`;
1888
+ const resp = await fetch(url);
1889
+ if (!resp.ok || !resp.body)
1890
+ throw new Error(`SSE connection failed: ${resp.status}`);
1891
+ const reader = resp.body.getReader();
1892
+ const decoder = new TextDecoder;
1893
+ let buffer = "";
1894
+ try {
1895
+ while (true) {
1896
+ const { done, value } = await reader.read();
1897
+ if (done)
1898
+ break;
1899
+ buffer += decoder.decode(value, { stream: true });
1900
+ const lines = buffer.split(`
1901
+ `);
1902
+ buffer = lines.pop() || "";
1903
+ for (const line of lines) {
1904
+ if (line.startsWith("data: ")) {
1905
+ try {
1906
+ const data = JSON.parse(line.slice(6));
1907
+ if (data.type !== "connected")
1908
+ yield data;
1909
+ } catch {}
1910
+ }
1911
+ }
1912
+ }
1913
+ } finally {
1914
+ reader.releaseLock();
1915
+ }
1916
+ }
1917
+ }
1918
+
1919
+ class AgentsResource {
1920
+ client;
1921
+ constructor(client) {
1922
+ this.client = client;
1923
+ }
1924
+ async list(options) {
1925
+ return this.client._get("/api/agents", buildQuery(options || {}));
1926
+ }
1927
+ async register(data) {
1928
+ return this.client._post("/api/agents", data);
1929
+ }
1930
+ async fullRegister(data) {
1931
+ return this.client._post("/api/agents", data);
1932
+ }
1933
+ async update(id, data) {
1934
+ return this.client._patch(`/api/agents/${id}`, data);
1935
+ }
1936
+ async delete(id) {
1937
+ return this.client._delete(`/api/agents/${id}`);
1938
+ }
1939
+ async bulkDelete(ids) {
1940
+ return this.client._post("/api/agents/bulk", { ids, action: "delete" });
1941
+ }
1942
+ async me(name) {
1943
+ return this.client._get("/api/agents/me", `?name=${encodeURIComponent(name)}`);
1944
+ }
1945
+ async queue(agentId) {
1946
+ return this.client._get(`/api/agents/${encodeURIComponent(agentId)}/queue`);
1947
+ }
1948
+ async team(agentId) {
1949
+ return this.client._get(`/api/agents/${encodeURIComponent(agentId)}/team`);
1950
+ }
1951
+ async orgChart() {
1952
+ return this.client._get("/api/org");
1953
+ }
1954
+ }
1955
+
1956
+ class ProjectsResource {
1957
+ client;
1958
+ constructor(client) {
1959
+ this.client = client;
1960
+ }
1961
+ async list(options) {
1962
+ return this.client._get("/api/projects", buildQuery(options || {}));
1963
+ }
1964
+ async create(data) {
1965
+ return this.client._post("/api/projects", data);
1966
+ }
1967
+ async delete(id) {
1968
+ return this.client._delete(`/api/projects/${id}`);
1969
+ }
1970
+ async bulkDelete(ids) {
1971
+ return this.client._post("/api/projects/bulk", { ids, action: "delete" });
1972
+ }
1973
+ }
1974
+
1975
+ class PlansResource {
1976
+ client;
1977
+ constructor(client) {
1978
+ this.client = client;
1979
+ }
1980
+ async list(projectId) {
1981
+ return this.client._get("/api/plans", projectId ? `?project_id=${projectId}` : "");
1982
+ }
1983
+ async create(data) {
1984
+ return this.client._post("/api/plans", data);
1985
+ }
1986
+ async get(id) {
1987
+ return this.client._get(`/api/plans/${id}`);
1988
+ }
1989
+ async update(id, data) {
1990
+ return this.client._patch(`/api/plans/${id}`, data);
1991
+ }
1992
+ async delete(id) {
1993
+ return this.client._delete(`/api/plans/${id}`);
1994
+ }
1995
+ async bulkDelete(ids) {
1996
+ return this.client._post("/api/plans/bulk", { ids, action: "delete" });
1997
+ }
1998
+ }
1999
+
2000
+ class OrgsResource {
2001
+ client;
2002
+ constructor(client) {
2003
+ this.client = client;
2004
+ }
2005
+ async list() {
2006
+ return this.client._get("/api/orgs");
2007
+ }
2008
+ async create(data) {
2009
+ return this.client._post("/api/orgs", data);
2010
+ }
2011
+ async update(id, data) {
2012
+ return this.client._patch(`/api/orgs/${id}`, data);
2013
+ }
2014
+ async delete(id) {
2015
+ return this.client._delete(`/api/orgs/${id}`);
2016
+ }
2017
+ }
2018
+
2019
+ class WebhooksResource {
2020
+ client;
2021
+ constructor(client) {
2022
+ this.client = client;
2023
+ }
2024
+ async list() {
2025
+ return this.client._get("/api/webhooks");
2026
+ }
2027
+ async create(data) {
2028
+ return this.client._post("/api/webhooks", data);
2029
+ }
2030
+ async delete(id) {
2031
+ return this.client._delete(`/api/webhooks/${id}`);
2032
+ }
2033
+ }
2034
+
2035
+ class TemplatesResource {
2036
+ client;
2037
+ constructor(client) {
2038
+ this.client = client;
2039
+ }
2040
+ async list() {
2041
+ return this.client._get("/api/templates");
2042
+ }
2043
+ async create(data) {
2044
+ return this.client._post("/api/templates", data);
2045
+ }
2046
+ async delete(id) {
2047
+ return this.client._delete(`/api/templates/${id}`);
2048
+ }
2049
+ }
1619
2050
 
1620
- // src/sdk.ts
1621
2051
  class TodosClient {
1622
2052
  baseUrl;
1623
2053
  timeout;
2054
+ apiKey;
2055
+ maxRetries;
2056
+ retryDelay;
2057
+ tasks;
2058
+ agents;
2059
+ projects;
2060
+ plans;
2061
+ orgs;
2062
+ webhooks;
2063
+ templates;
1624
2064
  constructor(options = {}) {
1625
2065
  this.baseUrl = options.baseUrl || process.env["TODOS_URL"] || "http://localhost:19427";
1626
- this.timeout = options.timeout || 1e4;
2066
+ this.baseUrl = this.baseUrl.replace(/\/+$/, "");
2067
+ this.timeout = options.timeout ?? 1e4;
2068
+ this.apiKey = options.apiKey || process.env["TODOS_API_KEY"] || null;
2069
+ this.maxRetries = options.maxRetries ?? 0;
2070
+ this.retryDelay = options.retryDelay ?? 1000;
2071
+ this.tasks = new TasksResource(this);
2072
+ this.agents = new AgentsResource(this);
2073
+ this.projects = new ProjectsResource(this);
2074
+ this.plans = new PlansResource(this);
2075
+ this.orgs = new OrgsResource(this);
2076
+ this.webhooks = new WebhooksResource(this);
2077
+ this.templates = new TemplatesResource(this);
2078
+ }
2079
+ static fromEnv(apiKey) {
2080
+ return new TodosClient({ apiKey });
2081
+ }
2082
+ async _fetchRaw(url, init) {
2083
+ const controller = new AbortController;
2084
+ const timer = setTimeout(() => controller.abort(), this.timeout);
2085
+ try {
2086
+ const headers = this._buildHeaders(init?.headers);
2087
+ return await fetch(url, { ...init, headers, signal: controller.signal });
2088
+ } finally {
2089
+ clearTimeout(timer);
2090
+ }
1627
2091
  }
1628
- static fromEnv() {
1629
- return new TodosClient({ baseUrl: process.env["TODOS_URL"] });
2092
+ _buildHeaders(existing) {
2093
+ const headers = { "Content-Type": "application/json" };
2094
+ if (this.apiKey)
2095
+ headers["x-api-key"] = this.apiKey;
2096
+ if (existing) {
2097
+ if (existing instanceof Headers) {
2098
+ existing.forEach((v, k) => {
2099
+ headers[k] = v;
2100
+ });
2101
+ } else if (Array.isArray(existing)) {
2102
+ for (const [k, v] of existing)
2103
+ headers[k] = v;
2104
+ } else {
2105
+ Object.assign(headers, existing);
2106
+ }
2107
+ }
2108
+ return headers;
2109
+ }
2110
+ async _fetchWithRetry(path, init) {
2111
+ let lastError = null;
2112
+ const maxAttempts = this.maxRetries + 1;
2113
+ for (let attempt = 0;attempt < maxAttempts; attempt++) {
2114
+ try {
2115
+ return await this._fetch(path, init);
2116
+ } catch (e) {
2117
+ lastError = e;
2118
+ if (e instanceof TodosAPIError && e.status < 500 && e.status !== 429)
2119
+ throw e;
2120
+ if (e instanceof TodosUnauthorizedError || e instanceof TodosNotFoundError || e instanceof TodosConflictError)
2121
+ throw e;
2122
+ if (attempt < maxAttempts - 1) {
2123
+ const delay = this.retryDelay * Math.pow(2, attempt);
2124
+ if (e instanceof TodosRateLimitError) {
2125
+ await this._sleep(e.retryAfter * 1000);
2126
+ } else {
2127
+ await this._sleep(delay);
2128
+ }
2129
+ }
2130
+ }
2131
+ }
2132
+ throw lastError || new Error("Request failed after retries");
1630
2133
  }
1631
- async fetch(path, init) {
2134
+ async _fetch(path, init) {
1632
2135
  const controller = new AbortController;
1633
2136
  const timer = setTimeout(() => controller.abort(), this.timeout);
1634
2137
  try {
1635
- const res = await fetch(`${this.baseUrl}${path}`, { ...init, signal: controller.signal });
2138
+ const url = `${this.baseUrl}${path}`;
2139
+ const headers = this._buildHeaders(init?.headers);
2140
+ const res = await fetch(url, { ...init, headers, signal: controller.signal });
1636
2141
  if (!res.ok) {
1637
- const err = await res.json().catch(() => ({ error: res.statusText }));
1638
- throw new Error(err.error || `HTTP ${res.status}`);
2142
+ const body = await res.json().catch(() => ({ error: res.statusText }));
2143
+ const message = body.error || `HTTP ${res.status}: ${res.statusText}`;
2144
+ if (res.status === 401)
2145
+ throw new TodosUnauthorizedError(message, body);
2146
+ if (res.status === 404)
2147
+ throw new TodosNotFoundError(message, body);
2148
+ if (res.status === 409)
2149
+ throw new TodosConflictError(message, body);
2150
+ if (res.status === 429) {
2151
+ const retryAfter = parseInt(res.headers.get("retry-after") || "60", 10);
2152
+ throw new TodosRateLimitError(message, retryAfter, body);
2153
+ }
2154
+ throw new TodosAPIError(message, res.status, res.statusText, body);
1639
2155
  }
2156
+ const contentLength = res.headers.get("content-length");
2157
+ if (contentLength === "0" || contentLength === "4")
2158
+ return null;
1640
2159
  return res.json();
2160
+ } catch (e) {
2161
+ if (e instanceof DOMException && e.name === "AbortError") {
2162
+ throw new TodosTimeoutError(this.timeout);
2163
+ }
2164
+ throw e;
1641
2165
  } finally {
1642
2166
  clearTimeout(timer);
1643
2167
  }
1644
2168
  }
2169
+ async _get(path, query = "") {
2170
+ return this._fetchWithRetry(`${path}${query}`);
2171
+ }
2172
+ async _post(path, body) {
2173
+ return this._fetchWithRetry(path, {
2174
+ method: "POST",
2175
+ body: body ? JSON.stringify(body) : undefined
2176
+ });
2177
+ }
2178
+ async _patch(path, body) {
2179
+ return this._fetchWithRetry(path, {
2180
+ method: "PATCH",
2181
+ body: JSON.stringify(body)
2182
+ });
2183
+ }
2184
+ async _delete(path) {
2185
+ return this._fetchWithRetry(path, { method: "DELETE" });
2186
+ }
2187
+ _sleep(ms) {
2188
+ return new Promise((resolve) => setTimeout(resolve, ms));
2189
+ }
1645
2190
  async getHealth() {
1646
- return this.fetch("/api/health");
2191
+ return this._get("/api/health");
1647
2192
  }
1648
2193
  async isAlive() {
1649
2194
  try {
1650
- await this.fetch("/api/stats");
2195
+ await this._get("/api/stats");
1651
2196
  return true;
1652
2197
  } catch {
1653
2198
  return false;
1654
2199
  }
1655
2200
  }
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}`);
2201
+ async getStats() {
2202
+ return this._get("/api/stats");
2203
+ }
2204
+ async getReport(options) {
2205
+ return this._get("/api/report", buildQuery({ days: options?.days, project_id: options?.projectId }));
2206
+ }
2207
+ async doctor() {
2208
+ return this._get("/api/doctor");
2209
+ }
2210
+ async activity(limit) {
2211
+ return this._get("/api/activity", limit ? `?limit=${limit}` : "");
1663
2212
  }
1664
2213
  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}`);
2214
+ return this.tasks.list(filter);
1671
2215
  }
1672
2216
  async getTask(id) {
1673
- return this.fetch(`/api/tasks/${id}`);
2217
+ return this.tasks.get(id);
1674
2218
  }
1675
- async getTaskAttachments(id) {
1676
- return this.fetch(`/api/tasks/${id}/attachments`);
2219
+ async createTask(data) {
2220
+ return this.tasks.create(data);
1677
2221
  }
1678
- async getTaskHistory(id) {
1679
- return this.fetch(`/api/tasks/${id}/history`);
2222
+ async updateTask(id, data) {
2223
+ return this.tasks.update(id, data);
1680
2224
  }
1681
- async getTaskProgress(id) {
1682
- return this.fetch(`/api/tasks/${id}/progress`);
2225
+ async deleteTask(id) {
2226
+ await this.tasks.delete(id);
1683
2227
  }
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
- });
2228
+ async startTask(id, agentId) {
2229
+ return this.tasks.start(id, agentId);
1690
2230
  }
1691
2231
  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
- });
2232
+ return this.tasks.complete(id, agentId);
1704
2233
  }
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
- });
2234
+ async failTask(id, options = {}) {
2235
+ return this.tasks.fail(id, options);
1711
2236
  }
1712
- async deleteTask(id) {
1713
- await this.fetch(`/api/tasks/${id}`, { method: "DELETE" });
2237
+ async logProgress(taskId, message, pctComplete, agentId) {
2238
+ return this.tasks.logProgress(taskId, message, pctComplete, agentId);
1714
2239
  }
1715
- async getStats() {
1716
- return this.fetch("/api/stats");
2240
+ async getStatus(projectId, agentId) {
2241
+ return this.tasks.status({ project_id: projectId, agent_id: agentId });
1717
2242
  }
1718
2243
  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}`);
2244
+ const res = await this.tasks.active(projectId);
2245
+ return res.active;
1723
2246
  }
1724
2247
  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
- });
2248
+ return this.tasks.changedSince(since, projectId);
1736
2249
  }
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
- });
2250
+ async getStaleTasks(minutes, projectId) {
2251
+ return this.tasks.stale({ minutes, project_id: projectId });
1743
2252
  }
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
- });
2253
+ async getContext(options = {}) {
2254
+ return this.tasks.context(options);
1750
2255
  }
1751
2256
  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}`);
2257
+ return this.tasks.export(filter);
1761
2258
  }
1762
- async getProjects() {
1763
- return this.fetch("/api/projects");
2259
+ async claimNextTask(agentId, projectId) {
2260
+ return this.tasks.claim(agentId, projectId);
1764
2261
  }
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
- }
2262
+ async getTaskHistory(id) {
2263
+ return this.tasks.getHistory(id);
2264
+ }
2265
+ async getTaskAttachments(id) {
2266
+ return this.tasks.getAttachments(id);
1788
2267
  }
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}`);
2268
+ async getTaskProgress(id) {
2269
+ return this.tasks.getProgress(id);
1796
2270
  }
1797
2271
  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
- }
2272
+ yield* this.tasks.subscribe(options);
2273
+ }
2274
+ async getProjects() {
2275
+ return this.projects.list();
1830
2276
  }
1831
2277
  }
1832
2278
  function createClient(options) {
1833
2279
  return new TodosClient(options);
1834
2280
  }
1835
-
1836
2281
  // src/index.ts
1837
2282
  init_database();
1838
2283
 
1839
- // src/db/tasks.ts
2284
+ // src/db/task-crud.ts
1840
2285
  init_types();
1841
2286
  init_database();
1842
2287
 
@@ -2207,184 +2652,111 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
2207
2652
  const remaining = Math.ceil(config.min_work_seconds - elapsedSeconds);
2208
2653
  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
2654
  }
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);
2655
+ }
2656
+ if (agent && config.max_completions_per_window && config.window_minutes) {
2657
+ const windowStart = new Date(Date.now() - config.window_minutes * 60 * 1000).toISOString();
2658
+ const result = db.query(`SELECT COUNT(*) as count FROM tasks
2659
+ WHERE completed_at > ? AND (assigned_to = ? OR agent_id = ?)`).get(windowStart, agent, agent);
2660
+ if (result.count >= config.max_completions_per_window) {
2661
+ 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
2662
  }
2352
- return next.toISOString();
2353
2663
  }
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;
2664
+ if (agent && config.cooldown_seconds) {
2665
+ const result = db.query(`SELECT MAX(completed_at) as last_completed FROM tasks
2666
+ WHERE completed_at IS NOT NULL AND (assigned_to = ? OR agent_id = ?) AND id != ?`).get(agent, agent, task.id);
2667
+ if (result.last_completed) {
2668
+ const elapsedSeconds = (Date.now() - new Date(result.last_completed).getTime()) / 1000;
2669
+ if (elapsedSeconds < config.cooldown_seconds) {
2670
+ const remaining = Math.ceil(config.cooldown_seconds - elapsedSeconds);
2671
+ throw new CompletionGuardError(`Cooldown: last completion was ${Math.floor(elapsedSeconds)}s ago. Wait ${remaining}s between completions.`, remaining);
2672
+ }
2364
2673
  }
2365
- const next = new Date(base);
2366
- next.setDate(next.getDate() + daysToAdd);
2367
- return next.toISOString();
2368
2674
  }
2369
- throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
2675
+ }
2676
+
2677
+ // src/db/audit.ts
2678
+ init_database();
2679
+ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
2680
+ const d = db || getDatabase();
2681
+ const id = uuid();
2682
+ const timestamp = now();
2683
+ d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
2684
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
2685
+ return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
2686
+ }
2687
+ function getTaskHistory(taskId, db) {
2688
+ const d = db || getDatabase();
2689
+ return d.query("SELECT * FROM task_history WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
2690
+ }
2691
+ function getRecentActivity(limit = 50, db) {
2692
+ const d = db || getDatabase();
2693
+ return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
2694
+ }
2695
+ function getRecap(hours = 8, projectId, db) {
2696
+ const d = db || getDatabase();
2697
+ const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
2698
+ const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
2699
+ const pf = projectId ? " AND project_id = ?" : "";
2700
+ const tpf = projectId ? " AND t.project_id = ?" : "";
2701
+ 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);
2702
+ 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);
2703
+ 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();
2704
+ 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();
2705
+ 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);
2706
+ 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);
2707
+ return {
2708
+ hours,
2709
+ since,
2710
+ completed: completed.map((r) => ({
2711
+ ...r,
2712
+ duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
2713
+ })),
2714
+ created,
2715
+ in_progress,
2716
+ blocked,
2717
+ stale,
2718
+ agents
2719
+ };
2370
2720
  }
2371
2721
 
2372
2722
  // src/db/webhooks.ts
2373
2723
  init_database();
2374
2724
  var MAX_RETRY_ATTEMPTS = 3;
2375
2725
  var RETRY_BASE_DELAY_MS = 1000;
2726
+ function isPrivateOrInternal(ip) {
2727
+ const parts = ip.split(".").map(Number);
2728
+ if (parts.length !== 4)
2729
+ return true;
2730
+ const a = parts[0];
2731
+ const b = parts[1];
2732
+ if (parts.some((p) => isNaN(p) || p < 0 || p > 255))
2733
+ return true;
2734
+ if (a === 10)
2735
+ return true;
2736
+ if (a === 127)
2737
+ return true;
2738
+ if (a === 169 && b === 254)
2739
+ return true;
2740
+ if (a === 172 && b >= 16 && b <= 31)
2741
+ return true;
2742
+ if (a === 192 && b === 168)
2743
+ return true;
2744
+ if (a === 0)
2745
+ return true;
2746
+ return false;
2747
+ }
2376
2748
  function validateWebhookUrl(urlString) {
2377
2749
  try {
2378
2750
  const url = new URL(urlString);
2379
2751
  if (url.protocol !== "https:") {
2380
- throw new Error("Webhook URLs must use HTTPS");
2752
+ return { valid: false, error: "Webhook URLs must use HTTPS" };
2381
2753
  }
2382
2754
  const hostname = url.hostname.toLowerCase();
2383
2755
  if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0") {
2384
- throw new Error("Webhook URLs cannot target localhost");
2756
+ return { valid: false, error: "Webhook URLs cannot target localhost" };
2385
2757
  }
2386
2758
  if (hostname === "169.254.169.254" || hostname.startsWith("169.254.")) {
2387
- throw new Error("Webhook URLs cannot target cloud metadata endpoints");
2759
+ return { valid: false, error: "Webhook URLs cannot target cloud metadata endpoints" };
2388
2760
  }
2389
2761
  const privateRanges = [
2390
2762
  /^10\./,
@@ -2397,1008 +2769,1308 @@ function validateWebhookUrl(urlString) {
2397
2769
  ];
2398
2770
  for (const range of privateRanges) {
2399
2771
  if (range.test(hostname)) {
2400
- throw new Error("Webhook URLs cannot target private IP ranges");
2772
+ return { valid: false, error: "Webhook URLs cannot target private IP ranges" };
2401
2773
  }
2402
2774
  }
2775
+ return { valid: true };
2403
2776
  } catch (e) {
2404
2777
  if (e instanceof Error && e.message.startsWith("Webhook URLs")) {
2405
- throw e;
2778
+ return { valid: false, error: e.message };
2779
+ }
2780
+ return { valid: false, error: `Invalid webhook URL: ${urlString}` };
2781
+ }
2782
+ }
2783
+ async function resolveAndCheckIp(hostname) {
2784
+ try {
2785
+ const resolved = await Bun.dns.lookup(hostname);
2786
+ if (!resolved)
2787
+ return { allowed: false, error: `Could not resolve hostname: ${hostname}` };
2788
+ const addresses = Array.isArray(resolved) ? resolved : [resolved];
2789
+ for (const addr of addresses) {
2790
+ const ip = typeof addr === "string" ? addr : addr.address;
2791
+ if (isPrivateOrInternal(ip)) {
2792
+ return { allowed: false, error: `Hostname ${hostname} resolves to blocked address ${ip}` };
2793
+ }
2794
+ }
2795
+ const first = addresses[0];
2796
+ return { allowed: true, ip: typeof first === "string" ? first : first?.address ?? "" };
2797
+ } catch {
2798
+ return { allowed: true, ip: "" };
2799
+ }
2800
+ }
2801
+ var activeDeliveries = 0;
2802
+ var MAX_CONCURRENT_DELIVERIES = 20;
2803
+ function rowToWebhook(row) {
2804
+ return {
2805
+ ...row,
2806
+ events: JSON.parse(row.events || "[]"),
2807
+ active: !!row.active,
2808
+ project_id: row.project_id || null,
2809
+ task_list_id: row.task_list_id || null,
2810
+ agent_id: row.agent_id || null,
2811
+ task_id: row.task_id || null
2812
+ };
2813
+ }
2814
+ function createWebhook(input, db) {
2815
+ const urlValidation = validateWebhookUrl(input.url);
2816
+ if (!urlValidation.valid) {
2817
+ throw new Error(`Invalid webhook URL: ${urlValidation.error}`);
2818
+ }
2819
+ const d = db || getDatabase();
2820
+ const id = uuid();
2821
+ d.run(`INSERT INTO webhooks (id, url, events, secret, project_id, task_list_id, agent_id, task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2822
+ id,
2823
+ input.url,
2824
+ JSON.stringify(input.events || []),
2825
+ input.secret || null,
2826
+ input.project_id || null,
2827
+ input.task_list_id || null,
2828
+ input.agent_id || null,
2829
+ input.task_id || null,
2830
+ now()
2831
+ ]);
2832
+ return getWebhook(id, d);
2833
+ }
2834
+ function getWebhook(id, db) {
2835
+ const d = db || getDatabase();
2836
+ const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
2837
+ return row ? rowToWebhook(row) : null;
2838
+ }
2839
+ function listWebhooks(db) {
2840
+ const d = db || getDatabase();
2841
+ return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
2842
+ }
2843
+ function deleteWebhook(id, db) {
2844
+ const d = db || getDatabase();
2845
+ return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
2846
+ }
2847
+ function listDeliveries(webhookId, limit = 50, db) {
2848
+ const d = db || getDatabase();
2849
+ if (webhookId) {
2850
+ return d.query("SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY created_at DESC LIMIT ?").all(webhookId, limit);
2851
+ }
2852
+ return d.query("SELECT * FROM webhook_deliveries ORDER BY created_at DESC LIMIT ?").all(limit);
2853
+ }
2854
+ function logDelivery(d, webhookId, event, payload, statusCode, response, attempt) {
2855
+ const id = uuid();
2856
+ 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()]);
2857
+ }
2858
+ function matchesScope(wh, payload) {
2859
+ if (wh.project_id && payload.project_id !== wh.project_id)
2860
+ return false;
2861
+ if (wh.task_list_id && payload.task_list_id !== wh.task_list_id)
2862
+ return false;
2863
+ if (wh.agent_id && payload.agent_id !== wh.agent_id && payload.assigned_to !== wh.agent_id)
2864
+ return false;
2865
+ if (wh.task_id && payload.id !== wh.task_id)
2866
+ return false;
2867
+ return true;
2868
+ }
2869
+ async function deliverWebhook(wh, event, body, attempt, db) {
2870
+ try {
2871
+ const url = new URL(wh.url);
2872
+ const hostname = url.hostname.toLowerCase();
2873
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
2874
+ logDelivery(db, wh.id, event, body, null, "Blocked: webhook URL points to localhost", attempt);
2875
+ return;
2876
+ }
2877
+ const ipCheck = await resolveAndCheckIp(hostname);
2878
+ if (!ipCheck.allowed) {
2879
+ logDelivery(db, wh.id, event, body, null, `Blocked: ${ipCheck.error}`, attempt);
2880
+ return;
2881
+ }
2882
+ } catch {
2883
+ logDelivery(db, wh.id, event, body, null, `Invalid URL at delivery time: ${wh.url}`, attempt);
2884
+ return;
2885
+ }
2886
+ if (activeDeliveries >= MAX_CONCURRENT_DELIVERIES) {
2887
+ logDelivery(db, wh.id, event, body, null, "Dropped: too many concurrent deliveries", attempt);
2888
+ return;
2889
+ }
2890
+ activeDeliveries++;
2891
+ try {
2892
+ const headers = { "Content-Type": "application/json" };
2893
+ if (wh.secret) {
2894
+ const encoder = new TextEncoder;
2895
+ const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
2896
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
2897
+ headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
2898
+ }
2899
+ const resp = await fetch(wh.url, { method: "POST", headers, body });
2900
+ const respText = await resp.text().catch(() => "");
2901
+ logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
2902
+ if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
2903
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
2904
+ setTimeout(() => {
2905
+ deliverWebhook(wh, event, body, attempt + 1, db).catch((retryErr) => {
2906
+ console.error(`[webhook] Retry failed for webhook ${wh.id}:`, retryErr);
2907
+ });
2908
+ }, delay);
2909
+ }
2910
+ } catch (err) {
2911
+ const errorMsg = err instanceof Error ? err.message : String(err);
2912
+ logDelivery(db, wh.id, event, body, null, errorMsg.slice(0, 1000), attempt);
2913
+ console.error(`[webhook] Delivery failed for webhook ${wh.id} (attempt ${attempt}):`, errorMsg);
2914
+ if (attempt < MAX_RETRY_ATTEMPTS) {
2915
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
2916
+ setTimeout(() => {
2917
+ deliverWebhook(wh, event, body, attempt + 1, db).catch((retryErr) => {
2918
+ console.error(`[webhook] Retry failed for webhook ${wh.id}:`, retryErr);
2919
+ });
2920
+ }, delay);
2406
2921
  }
2407
- throw new Error(`Invalid webhook URL: ${urlString}`);
2922
+ } finally {
2923
+ activeDeliveries--;
2924
+ }
2925
+ }
2926
+ async function dispatchWebhook(event, payload, db) {
2927
+ const d = db || getDatabase();
2928
+ const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
2929
+ const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
2930
+ for (const wh of webhooks) {
2931
+ if (!matchesScope(wh, payloadObj))
2932
+ continue;
2933
+ const body = JSON.stringify({ event, payload, timestamp: now() });
2934
+ deliverWebhook(wh, event, body, 1, d).catch((err) => {
2935
+ console.error(`[webhook] Dispatch failed for webhook ${wh.id}:`, err);
2936
+ });
2937
+ }
2938
+ }
2939
+
2940
+ // src/db/checklists.ts
2941
+ init_database();
2942
+ function rowToItem(row) {
2943
+ return { ...row, checked: !!row.checked };
2944
+ }
2945
+ function getChecklist(taskId, db) {
2946
+ const d = db || getDatabase();
2947
+ const rows = d.query("SELECT * FROM task_checklists WHERE task_id = ? ORDER BY position, created_at").all(taskId);
2948
+ return rows.map(rowToItem);
2949
+ }
2950
+ function addChecklistItem(input, db) {
2951
+ const d = db || getDatabase();
2952
+ const id = uuid();
2953
+ const timestamp = now();
2954
+ let position = input.position;
2955
+ if (position === undefined) {
2956
+ const maxRow = d.query("SELECT MAX(position) as max_pos FROM task_checklists WHERE task_id = ?").get(input.task_id);
2957
+ position = (maxRow?.max_pos ?? -1) + 1;
2408
2958
  }
2959
+ 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]);
2960
+ return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
2961
+ }
2962
+ function checkChecklistItem(id, checked, db) {
2963
+ const d = db || getDatabase();
2964
+ const timestamp = now();
2965
+ const result = d.run("UPDATE task_checklists SET checked = ?, updated_at = ? WHERE id = ?", [checked ? 1 : 0, timestamp, id]);
2966
+ if (result.changes === 0)
2967
+ return null;
2968
+ return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
2969
+ }
2970
+ function updateChecklistItemText(id, text, db) {
2971
+ const d = db || getDatabase();
2972
+ const timestamp = now();
2973
+ const result = d.run("UPDATE task_checklists SET text = ?, updated_at = ? WHERE id = ?", [text, timestamp, id]);
2974
+ if (result.changes === 0)
2975
+ return null;
2976
+ return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
2977
+ }
2978
+ function removeChecklistItem(id, db) {
2979
+ const d = db || getDatabase();
2980
+ const result = d.run("DELETE FROM task_checklists WHERE id = ?", [id]);
2981
+ return result.changes > 0;
2982
+ }
2983
+ function clearChecklist(taskId, db) {
2984
+ const d = db || getDatabase();
2985
+ const result = d.run("DELETE FROM task_checklists WHERE task_id = ?", [taskId]);
2986
+ return result.changes;
2987
+ }
2988
+ function getChecklistStats(taskId, db) {
2989
+ const d = db || getDatabase();
2990
+ const row = d.query("SELECT COUNT(*) as total, SUM(checked) as checked FROM task_checklists WHERE task_id = ?").get(taskId);
2991
+ return { total: row?.total ?? 0, checked: row?.checked ?? 0 };
2409
2992
  }
2410
- function rowToWebhook(row) {
2993
+
2994
+ // src/db/task-crud.ts
2995
+ function rowToTask(row) {
2411
2996
  return {
2412
2997
  ...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
2998
+ tags: JSON.parse(row.tags || "[]"),
2999
+ metadata: JSON.parse(row.metadata || "{}"),
3000
+ status: row.status,
3001
+ priority: row.priority,
3002
+ requires_approval: !!row.requires_approval
2419
3003
  };
2420
3004
  }
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);
3005
+ function insertTaskTags(taskId, tags, db) {
3006
+ if (tags.length === 0)
3007
+ return;
3008
+ const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
3009
+ for (const tag of tags) {
3010
+ if (tag)
3011
+ stmt.run(taskId, tag);
3012
+ }
2437
3013
  }
2438
- function getWebhook(id, db) {
3014
+ function replaceTaskTags(taskId, tags, db) {
3015
+ db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
3016
+ insertTaskTags(taskId, tags, db);
3017
+ }
3018
+ function createTask(input, db) {
2439
3019
  const d = db || getDatabase();
2440
- const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
2441
- return row ? rowToWebhook(row) : null;
3020
+ const timestamp = now();
3021
+ const tags = input.tags || [];
3022
+ const assignedBy = input.assigned_by || input.agent_id;
3023
+ const assignedFromProject = input.assigned_from_project || null;
3024
+ let id = uuid();
3025
+ for (let attempt = 0;attempt < 3; attempt++) {
3026
+ try {
3027
+ 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)
3028
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3029
+ id,
3030
+ null,
3031
+ input.project_id || null,
3032
+ input.parent_id || null,
3033
+ input.plan_id || null,
3034
+ input.task_list_id || null,
3035
+ input.title,
3036
+ input.description || null,
3037
+ input.status || "pending",
3038
+ input.priority || "medium",
3039
+ input.agent_id || null,
3040
+ input.assigned_to || null,
3041
+ input.session_id || null,
3042
+ input.working_dir || null,
3043
+ JSON.stringify(tags),
3044
+ JSON.stringify(input.metadata || {}),
3045
+ timestamp,
3046
+ timestamp,
3047
+ input.due_at || null,
3048
+ input.estimated_minutes || null,
3049
+ input.requires_approval ? 1 : 0,
3050
+ null,
3051
+ null,
3052
+ input.recurrence_rule || null,
3053
+ input.recurrence_parent_id || null,
3054
+ input.spawns_template_id || null,
3055
+ input.reason || null,
3056
+ input.spawned_from_session || null,
3057
+ assignedBy || null,
3058
+ assignedFromProject || null,
3059
+ input.task_type || null
3060
+ ]);
3061
+ break;
3062
+ } catch (e) {
3063
+ if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
3064
+ id = uuid();
3065
+ continue;
3066
+ }
3067
+ throw e;
3068
+ }
3069
+ }
3070
+ if (tags.length > 0) {
3071
+ insertTaskTags(id, tags, d);
3072
+ }
3073
+ const task = getTask(id, d);
3074
+ 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(() => {});
3075
+ return task;
2442
3076
  }
2443
- function listWebhooks(db) {
3077
+ function getTask(id, db) {
2444
3078
  const d = db || getDatabase();
2445
- return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
3079
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
3080
+ if (!row)
3081
+ return null;
3082
+ return rowToTask(row);
2446
3083
  }
2447
- function deleteWebhook(id, db) {
3084
+ function getTaskWithRelations(id, db) {
2448
3085
  const d = db || getDatabase();
2449
- return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
3086
+ const task = getTask(id, d);
3087
+ if (!task)
3088
+ return null;
3089
+ const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
3090
+ const subtasks = subtaskRows.map(rowToTask);
3091
+ const depRows = d.query(`SELECT t.* FROM tasks t
3092
+ JOIN task_dependencies td ON td.depends_on = t.id
3093
+ WHERE td.task_id = ?`).all(id);
3094
+ const dependencies = depRows.map(rowToTask);
3095
+ const blockedByRows = d.query(`SELECT t.* FROM tasks t
3096
+ JOIN task_dependencies td ON td.task_id = t.id
3097
+ WHERE td.depends_on = ?`).all(id);
3098
+ const blocked_by = blockedByRows.map(rowToTask);
3099
+ const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
3100
+ const parent = task.parent_id ? getTask(task.parent_id, d) : null;
3101
+ const checklist = getChecklist(id, d);
3102
+ return {
3103
+ ...task,
3104
+ subtasks,
3105
+ dependencies,
3106
+ blocked_by,
3107
+ comments,
3108
+ parent,
3109
+ checklist
3110
+ };
2450
3111
  }
2451
- function listDeliveries(webhookId, limit = 50, db) {
3112
+ function listTasks(filter = {}, db) {
2452
3113
  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);
3114
+ const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
3115
+ clearExpiredLocks2(d);
3116
+ const conditions = [];
3117
+ const params = [];
3118
+ if (filter.project_id) {
3119
+ conditions.push("project_id = ?");
3120
+ params.push(filter.project_id);
2455
3121
  }
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("");
3122
+ if (filter.ids && filter.ids.length > 0) {
3123
+ conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
3124
+ params.push(...filter.ids);
3125
+ }
3126
+ if (filter.parent_id !== undefined) {
3127
+ if (filter.parent_id === null) {
3128
+ conditions.push("parent_id IS NULL");
3129
+ } else {
3130
+ conditions.push("parent_id = ?");
3131
+ params.push(filter.parent_id);
2481
3132
  }
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);
3133
+ }
3134
+ if (filter.status) {
3135
+ if (Array.isArray(filter.status)) {
3136
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
3137
+ params.push(...filter.status);
3138
+ } else {
3139
+ conditions.push("status = ?");
3140
+ params.push(filter.status);
2490
3141
  }
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);
3142
+ }
3143
+ if (filter.priority) {
3144
+ if (Array.isArray(filter.priority)) {
3145
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
3146
+ params.push(...filter.priority);
3147
+ } else {
3148
+ conditions.push("priority = ?");
3149
+ params.push(filter.priority);
3150
+ }
3151
+ }
3152
+ if (filter.assigned_to) {
3153
+ conditions.push("assigned_to = ?");
3154
+ params.push(filter.assigned_to);
3155
+ }
3156
+ if (filter.agent_id) {
3157
+ conditions.push("agent_id = ?");
3158
+ params.push(filter.agent_id);
3159
+ }
3160
+ if (filter.session_id) {
3161
+ conditions.push("session_id = ?");
3162
+ params.push(filter.session_id);
3163
+ }
3164
+ if (filter.tags && filter.tags.length > 0) {
3165
+ const placeholders = filter.tags.map(() => "?").join(",");
3166
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
3167
+ params.push(...filter.tags);
3168
+ }
3169
+ if (filter.plan_id) {
3170
+ conditions.push("plan_id = ?");
3171
+ params.push(filter.plan_id);
3172
+ }
3173
+ if (filter.task_list_id) {
3174
+ conditions.push("task_list_id = ?");
3175
+ params.push(filter.task_list_id);
3176
+ }
3177
+ if (filter.has_recurrence === true) {
3178
+ conditions.push("recurrence_rule IS NOT NULL");
3179
+ } else if (filter.has_recurrence === false) {
3180
+ conditions.push("recurrence_rule IS NULL");
3181
+ }
3182
+ if (filter.task_type) {
3183
+ if (Array.isArray(filter.task_type)) {
3184
+ conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
3185
+ params.push(...filter.task_type);
3186
+ } else {
3187
+ conditions.push("task_type = ?");
3188
+ params.push(filter.task_type);
3189
+ }
3190
+ }
3191
+ const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
3192
+ if (filter.cursor) {
3193
+ try {
3194
+ const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
3195
+ conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
3196
+ params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
3197
+ } catch {}
3198
+ }
3199
+ if (!filter.include_archived) {
3200
+ conditions.push("archived_at IS NULL");
3201
+ }
3202
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3203
+ let limitClause = "";
3204
+ if (filter.limit) {
3205
+ limitClause = " LIMIT ?";
3206
+ params.push(filter.limit);
3207
+ if (!filter.cursor && filter.offset) {
3208
+ limitClause += " OFFSET ?";
3209
+ params.push(filter.offset);
2499
3210
  }
2500
3211
  }
3212
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
3213
+ return rows.map(rowToTask);
2501
3214
  }
2502
- async function dispatchWebhook(event, payload, db) {
3215
+ function countTasks(filter = {}, db) {
2503
3216
  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(() => {});
3217
+ const conditions = [];
3218
+ const params = [];
3219
+ if (filter.project_id) {
3220
+ conditions.push("project_id = ?");
3221
+ params.push(filter.project_id);
2511
3222
  }
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);
3223
+ if (filter.ids && filter.ids.length > 0) {
3224
+ conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
3225
+ params.push(...filter.ids);
2559
3226
  }
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;
3227
+ if (filter.parent_id !== undefined) {
3228
+ if (filter.parent_id === null) {
3229
+ conditions.push("parent_id IS NULL");
3230
+ } else {
3231
+ conditions.push("parent_id = ?");
3232
+ params.push(filter.parent_id);
3233
+ }
3234
+ }
3235
+ if (filter.status) {
3236
+ if (Array.isArray(filter.status)) {
3237
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
3238
+ params.push(...filter.status);
3239
+ } else {
3240
+ conditions.push("status = ?");
3241
+ params.push(filter.status);
3242
+ }
3243
+ }
3244
+ if (filter.priority) {
3245
+ if (Array.isArray(filter.priority)) {
3246
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
3247
+ params.push(...filter.priority);
3248
+ } else {
3249
+ conditions.push("priority = ?");
3250
+ params.push(filter.priority);
3251
+ }
3252
+ }
3253
+ if (filter.assigned_to) {
3254
+ conditions.push("assigned_to = ?");
3255
+ params.push(filter.assigned_to);
3256
+ }
3257
+ if (filter.agent_id) {
3258
+ conditions.push("agent_id = ?");
3259
+ params.push(filter.agent_id);
3260
+ }
3261
+ if (filter.session_id) {
3262
+ conditions.push("session_id = ?");
3263
+ params.push(filter.session_id);
3264
+ }
3265
+ if (filter.tags && filter.tags.length > 0) {
3266
+ const placeholders = filter.tags.map(() => "?").join(",");
3267
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
3268
+ params.push(...filter.tags);
3269
+ }
3270
+ if (filter.plan_id) {
3271
+ conditions.push("plan_id = ?");
3272
+ params.push(filter.plan_id);
3273
+ }
3274
+ if (filter.task_list_id) {
3275
+ conditions.push("task_list_id = ?");
3276
+ params.push(filter.task_list_id);
3277
+ }
3278
+ if (!filter.include_archived) {
3279
+ conditions.push("archived_at IS NULL");
3280
+ }
3281
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3282
+ const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
3283
+ return row.count;
2580
3284
  }
2581
- function updateTemplate(id, updates, db) {
3285
+ function updateTask(id, input, db) {
2582
3286
  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);
3287
+ const task = getTask(id, d);
3288
+ if (!task)
3289
+ throw new TaskNotFoundError(id);
3290
+ if (task.version !== input.version) {
3291
+ throw new VersionConflictError(id, input.version, task.version);
2607
3292
  }
2608
- if (updates.title_pattern !== undefined) {
2609
- sets.push("title_pattern = ?");
2610
- values.push(updates.title_pattern);
3293
+ const sets = ["version = version + 1", "updated_at = ?"];
3294
+ const params = [now()];
3295
+ if (input.title !== undefined) {
3296
+ sets.push("title = ?");
3297
+ params.push(input.title);
2611
3298
  }
2612
- if (updates.description !== undefined) {
3299
+ if (input.description !== undefined) {
2613
3300
  sets.push("description = ?");
2614
- values.push(updates.description);
3301
+ params.push(input.description);
2615
3302
  }
2616
- if (updates.priority !== undefined) {
3303
+ if (input.status !== undefined) {
3304
+ if (input.status === "completed") {
3305
+ checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
3306
+ }
3307
+ sets.push("status = ?");
3308
+ params.push(input.status);
3309
+ if (input.status === "completed") {
3310
+ sets.push("completed_at = ?");
3311
+ params.push(now());
3312
+ }
3313
+ }
3314
+ if (input.priority !== undefined) {
2617
3315
  sets.push("priority = ?");
2618
- values.push(updates.priority);
3316
+ params.push(input.priority);
2619
3317
  }
2620
- if (updates.tags !== undefined) {
3318
+ if (input.assigned_to !== undefined) {
3319
+ sets.push("assigned_to = ?");
3320
+ params.push(input.assigned_to);
3321
+ }
3322
+ if (input.tags !== undefined) {
2621
3323
  sets.push("tags = ?");
2622
- values.push(JSON.stringify(updates.tags));
3324
+ params.push(JSON.stringify(input.tags));
3325
+ }
3326
+ if (input.metadata !== undefined) {
3327
+ sets.push("metadata = ?");
3328
+ params.push(JSON.stringify(input.metadata));
3329
+ }
3330
+ if (input.plan_id !== undefined) {
3331
+ sets.push("plan_id = ?");
3332
+ params.push(input.plan_id);
3333
+ }
3334
+ if (input.task_list_id !== undefined) {
3335
+ sets.push("task_list_id = ?");
3336
+ params.push(input.task_list_id);
3337
+ }
3338
+ if (input.due_at !== undefined) {
3339
+ sets.push("due_at = ?");
3340
+ params.push(input.due_at);
3341
+ }
3342
+ if (input.estimated_minutes !== undefined) {
3343
+ sets.push("estimated_minutes = ?");
3344
+ params.push(input.estimated_minutes);
3345
+ }
3346
+ if (input.requires_approval !== undefined) {
3347
+ sets.push("requires_approval = ?");
3348
+ params.push(input.requires_approval ? 1 : 0);
3349
+ }
3350
+ if (input.approved_by !== undefined) {
3351
+ sets.push("approved_by = ?");
3352
+ params.push(input.approved_by);
3353
+ sets.push("approved_at = ?");
3354
+ params.push(now());
3355
+ }
3356
+ if (input.recurrence_rule !== undefined) {
3357
+ sets.push("recurrence_rule = ?");
3358
+ params.push(input.recurrence_rule);
3359
+ }
3360
+ if (input.task_type !== undefined) {
3361
+ sets.push("task_type = ?");
3362
+ params.push(input.task_type ?? null);
2623
3363
  }
2624
- if (updates.variables !== undefined) {
2625
- sets.push("variables = ?");
2626
- values.push(JSON.stringify(updates.variables));
3364
+ params.push(id, input.version);
3365
+ const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
3366
+ if (result.changes === 0) {
3367
+ const current = getTask(id, d);
3368
+ throw new VersionConflictError(id, input.version, current?.version ?? -1);
2627
3369
  }
2628
- if (updates.project_id !== undefined) {
2629
- sets.push("project_id = ?");
2630
- values.push(updates.project_id);
3370
+ if (input.tags !== undefined) {
3371
+ replaceTaskTags(id, input.tags, d);
2631
3372
  }
2632
- if (updates.plan_id !== undefined) {
2633
- sets.push("plan_id = ?");
2634
- values.push(updates.plan_id);
3373
+ const agentId = task.assigned_to || task.agent_id || null;
3374
+ if (input.status !== undefined && input.status !== task.status)
3375
+ logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
3376
+ if (input.priority !== undefined && input.priority !== task.priority)
3377
+ logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
3378
+ if (input.title !== undefined && input.title !== task.title)
3379
+ logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
3380
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
3381
+ logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
3382
+ if (input.approved_by !== undefined)
3383
+ logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
3384
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
3385
+ dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
2635
3386
  }
2636
- if (updates.metadata !== undefined) {
2637
- sets.push("metadata = ?");
2638
- values.push(JSON.stringify(updates.metadata));
3387
+ if (input.status !== undefined && input.status !== task.status) {
3388
+ dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
2639
3389
  }
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
3390
  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
3391
+ ...task,
3392
+ ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
3393
+ tags: input.tags ?? task.tags,
3394
+ metadata: input.metadata ?? task.metadata,
3395
+ version: task.version + 1,
3396
+ updated_at: now(),
3397
+ completed_at: input.status === "completed" ? now() : task.completed_at,
3398
+ requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
3399
+ approved_by: input.approved_by ?? task.approved_by,
3400
+ approved_at: input.approved_by ? now() : task.approved_at
2658
3401
  };
2659
3402
  }
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) {
3403
+ function deleteTask(id, db) {
2693
3404
  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 };
3405
+ const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
3406
+ return result.changes > 0;
2700
3407
  }
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);
3408
+ // src/db/task-lifecycle.ts
3409
+ init_types();
3410
+ init_database();
3411
+
3412
+ // src/lib/recurrence.ts
3413
+ var DAY_NAMES = {
3414
+ sunday: 0,
3415
+ sun: 0,
3416
+ monday: 1,
3417
+ mon: 1,
3418
+ tuesday: 2,
3419
+ tue: 2,
3420
+ wednesday: 3,
3421
+ wed: 3,
3422
+ thursday: 4,
3423
+ thu: 4,
3424
+ friday: 5,
3425
+ fri: 5,
3426
+ saturday: 6,
3427
+ sat: 6
3428
+ };
3429
+ function parseRecurrenceRule(rule) {
3430
+ const normalized = rule.trim().toLowerCase();
3431
+ if (normalized === "every weekday" || normalized === "every weekdays") {
3432
+ return { type: "specific_days", days: [1, 2, 3, 4, 5] };
3433
+ }
3434
+ if (normalized === "every day" || normalized === "daily") {
3435
+ return { type: "interval", interval: 1, unit: "day" };
3436
+ }
3437
+ if (normalized === "every week" || normalized === "weekly") {
3438
+ return { type: "interval", interval: 1, unit: "week" };
3439
+ }
3440
+ if (normalized === "every month" || normalized === "monthly") {
3441
+ return { type: "interval", interval: 1, unit: "month" };
3442
+ }
3443
+ const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
3444
+ if (intervalMatch) {
3445
+ return {
3446
+ type: "interval",
3447
+ interval: parseInt(intervalMatch[1], 10),
3448
+ unit: intervalMatch[2]
3449
+ };
3450
+ }
3451
+ const daysMatch = normalized.match(/^every\s+(.+)$/);
3452
+ if (daysMatch) {
3453
+ const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
3454
+ const days = [];
3455
+ for (const part of dayParts) {
3456
+ const dayNum = DAY_NAMES[part];
3457
+ if (dayNum !== undefined) {
3458
+ days.push(dayNum);
3459
+ }
3460
+ }
3461
+ if (days.length > 0) {
3462
+ return { type: "specific_days", days: days.sort((a, b) => a - b) };
3463
+ }
3464
+ }
3465
+ 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
3466
  }
2709
- function evaluateCondition(condition, variables) {
2710
- if (!condition || condition.trim() === "")
3467
+ function isValidRecurrenceRule(rule) {
3468
+ try {
3469
+ parseRecurrenceRule(rule);
2711
3470
  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;
3471
+ } catch {
3472
+ return false;
2724
3473
  }
2725
- const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
2726
- if (falsyMatch) {
2727
- const varName = falsyMatch[1];
2728
- const val = variables[varName];
2729
- return !val || val === "" || val === "false";
3474
+ }
3475
+ function nextOccurrence(rule, from) {
3476
+ const parsed = parseRecurrenceRule(rule);
3477
+ const base = from || new Date;
3478
+ if (parsed.type === "interval") {
3479
+ const next = new Date(base);
3480
+ if (parsed.unit === "day") {
3481
+ next.setDate(next.getDate() + parsed.interval);
3482
+ } else if (parsed.unit === "week") {
3483
+ next.setDate(next.getDate() + parsed.interval * 7);
3484
+ } else if (parsed.unit === "month") {
3485
+ next.setMonth(next.getMonth() + parsed.interval);
3486
+ }
3487
+ return next.toISOString();
2730
3488
  }
2731
- const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
2732
- if (truthyMatch) {
2733
- const varName = truthyMatch[1];
2734
- const val = variables[varName];
2735
- return !!val && val !== "" && val !== "false";
3489
+ if (parsed.type === "specific_days") {
3490
+ const currentDay = base.getDay();
3491
+ const days = parsed.days;
3492
+ let daysToAdd = Infinity;
3493
+ for (const day of days) {
3494
+ let diff = day - currentDay;
3495
+ if (diff <= 0)
3496
+ diff += 7;
3497
+ if (diff < daysToAdd)
3498
+ daysToAdd = diff;
3499
+ }
3500
+ const next = new Date(base);
3501
+ next.setDate(next.getDate() + daysToAdd);
3502
+ return next.toISOString();
2736
3503
  }
2737
- return true;
3504
+ throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
2738
3505
  }
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
- }))
3506
+
3507
+ // src/db/templates.ts
3508
+ init_database();
3509
+ function rowToTemplate(row) {
3510
+ return {
3511
+ ...row,
3512
+ tags: JSON.parse(row.tags || "[]"),
3513
+ variables: JSON.parse(row.variables || "[]"),
3514
+ metadata: JSON.parse(row.metadata || "{}"),
3515
+ priority: row.priority || "medium",
3516
+ version: row.version ?? 1
2766
3517
  };
2767
3518
  }
2768
- function importTemplate(json, db) {
3519
+ function rowToTemplateTask(row) {
3520
+ return {
3521
+ ...row,
3522
+ tags: JSON.parse(row.tags || "[]"),
3523
+ depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
3524
+ metadata: JSON.parse(row.metadata || "{}"),
3525
+ priority: row.priority || "medium",
3526
+ condition: row.condition ?? null,
3527
+ include_template_id: row.include_template_id ?? null
3528
+ };
3529
+ }
3530
+ function resolveTemplateId(id, d) {
3531
+ return resolvePartialId(d, "task_templates", id);
3532
+ }
3533
+ function createTemplate(input, db) {
2769
3534
  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);
3535
+ const id = uuid();
3536
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
3537
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3538
+ id,
3539
+ input.name,
3540
+ input.title_pattern,
3541
+ input.description || null,
3542
+ input.priority || "medium",
3543
+ JSON.stringify(input.tags || []),
3544
+ JSON.stringify(input.variables || []),
3545
+ input.project_id || null,
3546
+ input.plan_id || null,
3547
+ JSON.stringify(input.metadata || {}),
3548
+ now()
3549
+ ]);
3550
+ if (input.tasks && input.tasks.length > 0) {
3551
+ addTemplateTasks(id, input.tasks, d);
3552
+ }
3553
+ return getTemplate(id, d);
2793
3554
  }
2794
- function getTemplateVersion(id, version, db) {
3555
+ function getTemplate(id, db) {
2795
3556
  const d = db || getDatabase();
2796
3557
  const resolved = resolveTemplateId(id, d);
2797
3558
  if (!resolved)
2798
3559
  return null;
2799
- const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
2800
- return row || null;
3560
+ const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
3561
+ return row ? rowToTemplate(row) : null;
2801
3562
  }
2802
- function listTemplateVersions(id, db) {
3563
+ function listTemplates(db) {
3564
+ const d = db || getDatabase();
3565
+ return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
3566
+ }
3567
+ function deleteTemplate(id, db) {
2803
3568
  const d = db || getDatabase();
2804
3569
  const resolved = resolveTemplateId(id, d);
2805
3570
  if (!resolved)
2806
- return [];
2807
- return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
3571
+ return false;
3572
+ return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
2808
3573
  }
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
- }
3574
+ function updateTemplate(id, updates, db) {
3575
+ const d = db || getDatabase();
3576
+ const resolved = resolveTemplateId(id, d);
3577
+ if (!resolved)
3578
+ return null;
3579
+ const current = getTemplateWithTasks(resolved, d);
3580
+ if (current) {
3581
+ const snapshot = JSON.stringify({
3582
+ name: current.name,
3583
+ title_pattern: current.title_pattern,
3584
+ description: current.description,
3585
+ priority: current.priority,
3586
+ tags: current.tags,
3587
+ variables: current.variables,
3588
+ project_id: current.project_id,
3589
+ plan_id: current.plan_id,
3590
+ metadata: current.metadata,
3591
+ tasks: current.tasks
3592
+ });
3593
+ d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
2815
3594
  }
2816
- const missing = [];
2817
- for (const v of templateVars) {
2818
- if (v.required && merged[v.name] === undefined) {
2819
- missing.push(v.name);
2820
- }
3595
+ const sets = ["version = version + 1"];
3596
+ const values = [];
3597
+ if (updates.name !== undefined) {
3598
+ sets.push("name = ?");
3599
+ values.push(updates.name);
2821
3600
  }
2822
- if (missing.length > 0) {
2823
- throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
3601
+ if (updates.title_pattern !== undefined) {
3602
+ sets.push("title_pattern = ?");
3603
+ values.push(updates.title_pattern);
2824
3604
  }
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);
3605
+ if (updates.description !== undefined) {
3606
+ sets.push("description = ?");
3607
+ values.push(updates.description);
2831
3608
  }
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}`);
3609
+ if (updates.priority !== undefined) {
3610
+ sets.push("priority = ?");
3611
+ values.push(updates.priority);
2842
3612
  }
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];
3613
+ if (updates.tags !== undefined) {
3614
+ sets.push("tags = ?");
3615
+ values.push(JSON.stringify(updates.tags));
2849
3616
  }
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);
3617
+ if (updates.variables !== undefined) {
3618
+ sets.push("variables = ?");
3619
+ values.push(JSON.stringify(updates.variables));
2885
3620
  }
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
- }
3621
+ if (updates.project_id !== undefined) {
3622
+ sets.push("project_id = ?");
3623
+ values.push(updates.project_id);
2901
3624
  }
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
- }
3625
+ if (updates.plan_id !== undefined) {
3626
+ sets.push("plan_id = ?");
3627
+ values.push(updates.plan_id);
2935
3628
  }
3629
+ if (updates.metadata !== undefined) {
3630
+ sets.push("metadata = ?");
3631
+ values.push(JSON.stringify(updates.metadata));
3632
+ }
3633
+ values.push(resolved);
3634
+ d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
3635
+ return getTemplate(resolved, d);
3636
+ }
3637
+ function taskFromTemplate(templateId, overrides = {}, db) {
3638
+ const t = getTemplate(templateId, db);
3639
+ if (!t)
3640
+ throw new Error(`Template not found: ${templateId}`);
3641
+ const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
2936
3642
  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
3643
+ title: cleanOverrides.title || t.title_pattern,
3644
+ description: cleanOverrides.description ?? t.description ?? undefined,
3645
+ priority: cleanOverrides.priority ?? t.priority,
3646
+ tags: cleanOverrides.tags ?? t.tags,
3647
+ project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
3648
+ plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
3649
+ metadata: cleanOverrides.metadata ?? t.metadata,
3650
+ ...cleanOverrides
2943
3651
  };
2944
3652
  }
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) {
3653
+ function addTemplateTasks(templateId, tasks, db) {
2957
3654
  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;
3655
+ const template = getTemplate(templateId, d);
3656
+ if (!template)
3657
+ throw new Error(`Template not found: ${templateId}`);
3658
+ d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
3659
+ const results = [];
3660
+ for (let i = 0;i < tasks.length; i++) {
3661
+ const task = tasks[i];
3662
+ const id = uuid();
3663
+ 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)
3664
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3665
+ id,
3666
+ templateId,
3667
+ i,
3668
+ task.title_pattern,
3669
+ task.description || null,
3670
+ task.priority || "medium",
3671
+ JSON.stringify(task.tags || []),
3672
+ task.task_type || null,
3673
+ task.condition || null,
3674
+ task.include_template_id || null,
3675
+ JSON.stringify(task.depends_on || []),
3676
+ JSON.stringify(task.metadata || {}),
3677
+ now()
3678
+ ]);
3679
+ const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
3680
+ if (row)
3681
+ results.push(rowToTemplateTask(row));
2964
3682
  }
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));
3683
+ return results;
2975
3684
  }
2976
- function updateChecklistItemText(id, text, db) {
3685
+ function getTemplateWithTasks(id, db) {
2977
3686
  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)
3687
+ const template = getTemplate(id, d);
3688
+ if (!template)
2981
3689
  return null;
2982
- return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
3690
+ const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
3691
+ const tasks = rows.map(rowToTemplateTask);
3692
+ return { ...template, tasks };
2983
3693
  }
2984
- function removeChecklistItem(id, db) {
3694
+ function getTemplateTasks(templateId, db) {
2985
3695
  const d = db || getDatabase();
2986
- const result = d.run("DELETE FROM task_checklists WHERE id = ?", [id]);
2987
- return result.changes > 0;
3696
+ const resolved = resolveTemplateId(templateId, d);
3697
+ if (!resolved)
3698
+ return [];
3699
+ const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
3700
+ return rows.map(rowToTemplateTask);
2988
3701
  }
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;
3702
+ function evaluateCondition(condition, variables) {
3703
+ if (!condition || condition.trim() === "")
3704
+ return true;
3705
+ const trimmed = condition.trim();
3706
+ const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
3707
+ if (eqMatch) {
3708
+ const varName = eqMatch[1];
3709
+ const expected = eqMatch[2].trim();
3710
+ return (variables[varName] ?? "") === expected;
3711
+ }
3712
+ const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
3713
+ if (neqMatch) {
3714
+ const varName = neqMatch[1];
3715
+ const expected = neqMatch[2].trim();
3716
+ return (variables[varName] ?? "") !== expected;
3717
+ }
3718
+ const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
3719
+ if (falsyMatch) {
3720
+ const varName = falsyMatch[1];
3721
+ const val = variables[varName];
3722
+ return !val || val === "" || val === "false";
3723
+ }
3724
+ const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
3725
+ if (truthyMatch) {
3726
+ const varName = truthyMatch[1];
3727
+ const val = variables[varName];
3728
+ return !!val && val !== "" && val !== "false";
3729
+ }
3730
+ return true;
2993
3731
  }
2994
- function getChecklistStats(taskId, db) {
3732
+ function exportTemplate(id, db) {
2995
3733
  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) {
3734
+ const template = getTemplateWithTasks(id, d);
3735
+ if (!template)
3736
+ throw new Error(`Template not found: ${id}`);
3002
3737
  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
3738
+ name: template.name,
3739
+ title_pattern: template.title_pattern,
3740
+ description: template.description,
3741
+ priority: template.priority,
3742
+ tags: template.tags,
3743
+ variables: template.variables,
3744
+ project_id: template.project_id,
3745
+ plan_id: template.plan_id,
3746
+ metadata: template.metadata,
3747
+ tasks: template.tasks.map((t) => ({
3748
+ position: t.position,
3749
+ title_pattern: t.title_pattern,
3750
+ description: t.description,
3751
+ priority: t.priority,
3752
+ tags: t.tags,
3753
+ task_type: t.task_type,
3754
+ condition: t.condition,
3755
+ include_template_id: t.include_template_id,
3756
+ depends_on_positions: t.depends_on_positions,
3757
+ metadata: t.metadata
3758
+ }))
3009
3759
  };
3010
3760
  }
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) {
3761
+ function importTemplate(json, db) {
3025
3762
  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;
3763
+ const taskInputs = (json.tasks || []).map((t) => ({
3764
+ title_pattern: t.title_pattern,
3765
+ description: t.description ?? undefined,
3766
+ priority: t.priority,
3767
+ tags: t.tags,
3768
+ task_type: t.task_type ?? undefined,
3769
+ condition: t.condition ?? undefined,
3770
+ include_template_id: t.include_template_id ?? undefined,
3771
+ depends_on: t.depends_on_positions,
3772
+ metadata: t.metadata
3773
+ }));
3774
+ return createTemplate({
3775
+ name: json.name,
3776
+ title_pattern: json.title_pattern,
3777
+ description: json.description ?? undefined,
3778
+ priority: json.priority,
3779
+ tags: json.tags,
3780
+ variables: json.variables,
3781
+ project_id: json.project_id ?? undefined,
3782
+ plan_id: json.plan_id ?? undefined,
3783
+ metadata: json.metadata,
3784
+ tasks: taskInputs
3785
+ }, d);
3082
3786
  }
3083
- function getTask(id, db) {
3787
+ function getTemplateVersion(id, version, db) {
3084
3788
  const d = db || getDatabase();
3085
- const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
3086
- if (!row)
3789
+ const resolved = resolveTemplateId(id, d);
3790
+ if (!resolved)
3087
3791
  return null;
3088
- return rowToTask(row);
3792
+ const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
3793
+ return row || null;
3089
3794
  }
3090
- function getTaskWithRelations(id, db) {
3795
+ function listTemplateVersions(id, db) {
3091
3796
  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
- };
3797
+ const resolved = resolveTemplateId(id, d);
3798
+ if (!resolved)
3799
+ return [];
3800
+ return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
3117
3801
  }
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);
3802
+ function resolveVariables(templateVars, provided) {
3803
+ const merged = { ...provided };
3804
+ for (const v of templateVars) {
3805
+ if (merged[v.name] === undefined && v.default !== undefined) {
3806
+ merged[v.name] = v.default;
3142
3807
  }
3143
3808
  }
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);
3809
+ const missing = [];
3810
+ for (const v of templateVars) {
3811
+ if (v.required && merged[v.name] === undefined) {
3812
+ missing.push(v.name);
3151
3813
  }
3152
3814
  }
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);
3815
+ if (missing.length > 0) {
3816
+ throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
3169
3817
  }
3170
- if (filter.plan_id) {
3171
- conditions.push("plan_id = ?");
3172
- params.push(filter.plan_id);
3818
+ return merged;
3819
+ }
3820
+ function substituteVars(text, variables) {
3821
+ let result = text;
3822
+ for (const [key, val] of Object.entries(variables)) {
3823
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
3173
3824
  }
3174
- if (filter.task_list_id) {
3175
- conditions.push("task_list_id = ?");
3176
- params.push(filter.task_list_id);
3825
+ return result;
3826
+ }
3827
+ function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
3828
+ const d = db || getDatabase();
3829
+ const template = getTemplateWithTasks(templateId, d);
3830
+ if (!template)
3831
+ throw new Error(`Template not found: ${templateId}`);
3832
+ const visited = _visitedTemplateIds || new Set;
3833
+ if (visited.has(template.id)) {
3834
+ throw new Error(`Circular template reference detected: ${template.id}`);
3177
3835
  }
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");
3836
+ visited.add(template.id);
3837
+ const resolved = resolveVariables(template.variables, variables);
3838
+ if (template.tasks.length === 0) {
3839
+ const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
3840
+ const task = createTask(input, d);
3841
+ return [task];
3182
3842
  }
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);
3843
+ const createdTasks = [];
3844
+ const positionToId = new Map;
3845
+ const skippedPositions = new Set;
3846
+ for (const tt of template.tasks) {
3847
+ if (tt.include_template_id) {
3848
+ const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
3849
+ createdTasks.push(...includedTasks);
3850
+ if (includedTasks.length > 0) {
3851
+ positionToId.set(tt.position, includedTasks[0].id);
3852
+ } else {
3853
+ skippedPositions.add(tt.position);
3854
+ }
3855
+ continue;
3190
3856
  }
3857
+ if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
3858
+ skippedPositions.add(tt.position);
3859
+ continue;
3860
+ }
3861
+ let title = tt.title_pattern;
3862
+ let desc = tt.description;
3863
+ title = substituteVars(title, resolved);
3864
+ if (desc)
3865
+ desc = substituteVars(desc, resolved);
3866
+ const task = createTask({
3867
+ title,
3868
+ description: desc ?? undefined,
3869
+ priority: tt.priority,
3870
+ tags: tt.tags,
3871
+ task_type: tt.task_type ?? undefined,
3872
+ project_id: projectId,
3873
+ task_list_id: taskListId,
3874
+ metadata: tt.metadata
3875
+ }, d);
3876
+ createdTasks.push(task);
3877
+ positionToId.set(tt.position, task.id);
3191
3878
  }
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);
3879
+ for (const tt of template.tasks) {
3880
+ if (skippedPositions.has(tt.position))
3881
+ continue;
3882
+ if (tt.include_template_id)
3883
+ continue;
3884
+ const deps = tt.depends_on_positions;
3885
+ for (const depPos of deps) {
3886
+ if (skippedPositions.has(depPos))
3887
+ continue;
3888
+ const taskId = positionToId.get(tt.position);
3889
+ const depId = positionToId.get(depPos);
3890
+ if (taskId && depId) {
3891
+ addDependency(taskId, depId, d);
3892
+ }
3211
3893
  }
3212
3894
  }
3213
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
3214
- return rows.map(rowToTask);
3895
+ return createdTasks;
3215
3896
  }
3216
- function countTasks(filter = {}, db) {
3897
+ function previewTemplate(templateId, variables, db) {
3217
3898
  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);
3899
+ const template = getTemplateWithTasks(templateId, d);
3900
+ if (!template)
3901
+ throw new Error(`Template not found: ${templateId}`);
3902
+ const resolved = resolveVariables(template.variables, variables);
3903
+ const tasks = [];
3904
+ if (template.tasks.length === 0) {
3905
+ tasks.push({
3906
+ position: 0,
3907
+ title: substituteVars(template.title_pattern, resolved),
3908
+ description: template.description ? substituteVars(template.description, resolved) : null,
3909
+ priority: template.priority,
3910
+ tags: template.tags,
3911
+ task_type: null,
3912
+ depends_on_positions: []
3913
+ });
3914
+ } else {
3915
+ for (const tt of template.tasks) {
3916
+ if (tt.condition && !evaluateCondition(tt.condition, resolved))
3917
+ continue;
3918
+ tasks.push({
3919
+ position: tt.position,
3920
+ title: substituteVars(tt.title_pattern, resolved),
3921
+ description: tt.description ? substituteVars(tt.description, resolved) : null,
3922
+ priority: tt.priority,
3923
+ tags: tt.tags,
3924
+ task_type: tt.task_type,
3925
+ depends_on_positions: tt.depends_on_positions
3926
+ });
3248
3927
  }
3249
3928
  }
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);
3929
+ return {
3930
+ template_id: template.id,
3931
+ template_name: template.name,
3932
+ description: template.description,
3933
+ variables: template.variables,
3934
+ resolved_variables: resolved,
3935
+ tasks
3936
+ };
3937
+ }
3938
+
3939
+ // src/db/task-graph.ts
3940
+ init_types();
3941
+ init_database();
3942
+ function addDependency(taskId, dependsOn, db) {
3943
+ const d = db || getDatabase();
3944
+ if (!getTask(taskId, d))
3945
+ throw new TaskNotFoundError(taskId);
3946
+ if (!getTask(dependsOn, d))
3947
+ throw new TaskNotFoundError(dependsOn);
3948
+ if (wouldCreateCycle(taskId, dependsOn, d)) {
3949
+ throw new DependencyCycleError(taskId, dependsOn);
3261
3950
  }
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);
3951
+ d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
3952
+ }
3953
+ function removeDependency(taskId, dependsOn, db) {
3954
+ const d = db || getDatabase();
3955
+ const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
3956
+ return result.changes > 0;
3957
+ }
3958
+ function getTaskDependencies(taskId, db) {
3959
+ const d = db || getDatabase();
3960
+ return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
3961
+ }
3962
+ function getTaskDependents(taskId, db) {
3963
+ const d = db || getDatabase();
3964
+ return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
3965
+ }
3966
+ function cloneTask(taskId, overrides, db) {
3967
+ const d = db || getDatabase();
3968
+ const source = getTask(taskId, d);
3969
+ if (!source)
3970
+ throw new TaskNotFoundError(taskId);
3971
+ const input = {
3972
+ title: overrides?.title ?? source.title,
3973
+ description: overrides?.description ?? source.description ?? undefined,
3974
+ priority: overrides?.priority ?? source.priority,
3975
+ project_id: overrides?.project_id ?? source.project_id ?? undefined,
3976
+ parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
3977
+ plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
3978
+ task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
3979
+ status: overrides?.status ?? "pending",
3980
+ agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
3981
+ assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
3982
+ tags: overrides?.tags ?? source.tags,
3983
+ metadata: overrides?.metadata ?? source.metadata,
3984
+ estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
3985
+ recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
3986
+ };
3987
+ return createTask(input, d);
3988
+ }
3989
+ function getTaskGraph(taskId, direction = "both", db) {
3990
+ const d = db || getDatabase();
3991
+ const task = getTask(taskId, d);
3992
+ if (!task)
3993
+ throw new TaskNotFoundError(taskId);
3994
+ function toNode(t) {
3995
+ const deps = getTaskDependencies(t.id, d);
3996
+ const hasUnfinishedDeps = deps.some((dep) => {
3997
+ const depTask = getTask(dep.depends_on, d);
3998
+ return depTask && depTask.status !== "completed";
3999
+ });
4000
+ return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
3266
4001
  }
3267
- if (filter.plan_id) {
3268
- conditions.push("plan_id = ?");
3269
- params.push(filter.plan_id);
4002
+ function buildUp(id, visited) {
4003
+ if (visited.has(id))
4004
+ return [];
4005
+ visited.add(id);
4006
+ const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
4007
+ return deps.map((dep) => {
4008
+ const depTask = getTask(dep.depends_on, d);
4009
+ if (!depTask)
4010
+ return null;
4011
+ return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
4012
+ }).filter(Boolean);
3270
4013
  }
3271
- if (filter.task_list_id) {
3272
- conditions.push("task_list_id = ?");
3273
- params.push(filter.task_list_id);
4014
+ function buildDown(id, visited) {
4015
+ if (visited.has(id))
4016
+ return [];
4017
+ visited.add(id);
4018
+ const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
4019
+ return dependents.map((dep) => {
4020
+ const depTask = getTask(dep.task_id, d);
4021
+ if (!depTask)
4022
+ return null;
4023
+ return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
4024
+ }).filter(Boolean);
3274
4025
  }
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;
4026
+ const rootNode = toNode(task);
4027
+ const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
4028
+ const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
4029
+ return { task: rootNode, depends_on, blocks };
3278
4030
  }
3279
- function updateTask(id, input, db) {
4031
+ function moveTask(taskId, target, db) {
3280
4032
  const d = db || getDatabase();
3281
- const task = getTask(id, d);
4033
+ const task = getTask(taskId, d);
3282
4034
  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 = ?"];
4035
+ throw new TaskNotFoundError(taskId);
4036
+ const sets = ["updated_at = ?", "version = version + 1"];
3288
4037
  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) {
4038
+ if (target.task_list_id !== undefined) {
3329
4039
  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(() => {});
4040
+ params.push(target.task_list_id);
3380
4041
  }
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(() => {});
4042
+ if (target.project_id !== undefined) {
4043
+ sets.push("project_id = ?");
4044
+ params.push(target.project_id);
3383
4045
  }
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
- };
4046
+ if (target.plan_id !== undefined) {
4047
+ sets.push("plan_id = ?");
4048
+ params.push(target.plan_id);
4049
+ }
4050
+ params.push(taskId);
4051
+ d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
4052
+ return getTask(taskId, d);
3396
4053
  }
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;
4054
+ function wouldCreateCycle(taskId, dependsOn, db) {
4055
+ const visited = new Set;
4056
+ const queue = [dependsOn];
4057
+ while (queue.length > 0) {
4058
+ const current = queue.shift();
4059
+ if (current === taskId)
4060
+ return true;
4061
+ if (visited.has(current))
4062
+ continue;
4063
+ visited.add(current);
4064
+ const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
4065
+ for (const dep of deps) {
4066
+ queue.push(dep.depends_on);
4067
+ }
4068
+ }
4069
+ return false;
3401
4070
  }
4071
+
4072
+ // src/db/task-lifecycle.ts
4073
+ var MAX_SPAWN_DEPTH = 10;
3402
4074
  function getBlockingDeps(id, db) {
3403
4075
  const d = db || getDatabase();
3404
4076
  const deps = getTaskDependencies(id, d);
@@ -3453,14 +4125,21 @@ function completeTask(id, agentId, db, options) {
3453
4125
  completionMeta._completion = { confidence: options.confidence };
3454
4126
  }
3455
4127
  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
4128
  const timestamp = now();
3461
4129
  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]);
4130
+ const tx = d.transaction(() => {
4131
+ if (hasMeta) {
4132
+ const meta2 = { ...task.metadata, ...completionMeta };
4133
+ const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
4134
+ if (metaResult.changes === 0) {
4135
+ const current = getTask(id, d);
4136
+ throw new VersionConflictError(id, task.version, current?.version ?? -1);
4137
+ }
4138
+ }
4139
+ d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
4140
+ WHERE id = ?`, [timestamp, confidence, timestamp, id]);
4141
+ });
4142
+ tx();
3464
4143
  logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
3465
4144
  dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
3466
4145
  let spawnedTask = null;
@@ -3469,15 +4148,21 @@ function completeTask(id, agentId, db, options) {
3469
4148
  }
3470
4149
  let spawnedFromTemplate = null;
3471
4150
  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 {}
4151
+ const spawnDepth = task.metadata?._spawn_depth || 0;
4152
+ if (spawnDepth >= MAX_SPAWN_DEPTH) {
4153
+ console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
4154
+ } else {
4155
+ try {
4156
+ const input = taskFromTemplate(task.spawns_template_id, {
4157
+ project_id: task.project_id ?? undefined,
4158
+ plan_id: task.plan_id ?? undefined,
4159
+ task_list_id: task.task_list_id ?? undefined,
4160
+ assigned_to: task.assigned_to ?? undefined
4161
+ }, d);
4162
+ input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
4163
+ spawnedFromTemplate = createTask(input, d);
4164
+ } catch {}
4165
+ }
3481
4166
  }
3482
4167
  const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
3483
4168
  if (spawnedTask) {
@@ -3542,141 +4227,6 @@ function unlockTask(id, agentId, db) {
3542
4227
  WHERE id = ?`, [timestamp, id]);
3543
4228
  return true;
3544
4229
  }
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
4230
  function claimNextTask(agentId, filters, db) {
3681
4231
  const d = db || getDatabase();
3682
4232
  const tx = d.transaction(() => {
@@ -3849,10 +4399,6 @@ function getStaleTasks(staleMinutes = 30, filters, db) {
3849
4399
  const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
3850
4400
  return rows.map(rowToTask);
3851
4401
  }
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
4402
  function stealTask(agentId, opts, db) {
3857
4403
  const d = db || getDatabase();
3858
4404
  const staleMinutes = opts?.stale_minutes ?? 30;
@@ -3883,6 +4429,32 @@ function claimOrSteal(agentId, filters, db) {
3883
4429
  });
3884
4430
  return tx();
3885
4431
  }
4432
+ function spawnNextRecurrence(completedTask, db) {
4433
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
4434
+ let title = completedTask.title;
4435
+ if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
4436
+ title = title.slice(completedTask.short_id.length + 2);
4437
+ }
4438
+ const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
4439
+ return createTask({
4440
+ title,
4441
+ description: completedTask.description ?? undefined,
4442
+ priority: completedTask.priority,
4443
+ project_id: completedTask.project_id ?? undefined,
4444
+ task_list_id: completedTask.task_list_id ?? undefined,
4445
+ plan_id: completedTask.plan_id ?? undefined,
4446
+ assigned_to: completedTask.assigned_to ?? undefined,
4447
+ tags: completedTask.tags,
4448
+ metadata: completedTask.metadata,
4449
+ estimated_minutes: completedTask.estimated_minutes ?? undefined,
4450
+ recurrence_rule: completedTask.recurrence_rule,
4451
+ recurrence_parent_id: recurrenceParentId,
4452
+ due_at: dueAt
4453
+ }, db);
4454
+ }
4455
+ // src/db/task-status.ts
4456
+ init_types();
4457
+ init_database();
3886
4458
  function getStatus(filters, agentId, options, db) {
3887
4459
  const d = db || getDatabase();
3888
4460
  const pending = countTasks({ ...filters, status: "pending" }, d);
@@ -4011,23 +4583,6 @@ function redistributeStaleTasks(agentId, options, db) {
4011
4583
  const claimed = released.length > 0 ? claimNextTask(agentId, options?.project_id ? { project_id: options.project_id } : undefined, d) : null;
4012
4584
  return { released, claimed };
4013
4585
  }
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
4586
  function getTaskStats(filters, db) {
4032
4587
  const d = db || getDatabase();
4033
4588
  const conditions = [];
@@ -4062,6 +4617,8 @@ function getTaskStats(filters, db) {
4062
4617
  const completion_rate = totalRow.count > 0 ? Math.round(completed / totalRow.count * 100) : 0;
4063
4618
  return { total: totalRow.count, by_status, by_priority, completion_rate, by_agent };
4064
4619
  }
4620
+ // src/db/task-relations.ts
4621
+ init_database();
4065
4622
  function bulkCreateTasks(inputs, db) {
4066
4623
  const d = db || getDatabase();
4067
4624
  const tempIdToRealId = new Map;
@@ -4155,6 +4712,10 @@ function getOverdueTasks(projectId, db) {
4155
4712
  const rows = d.query(query).all(...params);
4156
4713
  return rows.map(rowToTask);
4157
4714
  }
4715
+ function logCost(taskId, tokens, usd, db) {
4716
+ const d = db || getDatabase();
4717
+ d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
4718
+ }
4158
4719
  // src/db/plans.ts
4159
4720
  init_types();
4160
4721
  init_database();
@@ -6266,7 +6827,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
6266
6827
  }
6267
6828
  return { pushed, pulled, errors };
6268
6829
  }
6269
- // ../../../../node_modules/@hasna/cloud/dist/index.js
6830
+ // node_modules/@hasna/cloud/dist/index.js
6270
6831
  import { createRequire } from "module";
6271
6832
  import { Database as Database2 } from "bun:sqlite";
6272
6833
  import {
@@ -6290,9 +6851,9 @@ import { homedir as homedir5, platform } from "os";
6290
6851
  var __create = Object.create;
6291
6852
  var __getProtoOf = Object.getPrototypeOf;
6292
6853
  var __defProp2 = Object.defineProperty;
6293
- var __getOwnPropNames = Object.getOwnPropertyNames;
6294
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6295
- function __accessProp(key) {
6854
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
6855
+ var __hasOwnProp2 = Object.prototype.hasOwnProperty;
6856
+ function __accessProp2(key) {
6296
6857
  return this[key];
6297
6858
  }
6298
6859
  var __toESMCache_node;
@@ -6307,10 +6868,10 @@ var __toESM = (mod, isNodeMode, target) => {
6307
6868
  }
6308
6869
  target = mod != null ? __create(__getProtoOf(mod)) : {};
6309
6870
  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))
6871
+ for (let key of __getOwnPropNames2(mod))
6872
+ if (!__hasOwnProp2.call(to, key))
6312
6873
  __defProp2(to, key, {
6313
- get: __accessProp.bind(mod, key),
6874
+ get: __accessProp2.bind(mod, key),
6314
6875
  enumerable: true
6315
6876
  });
6316
6877
  if (canCache)
@@ -6318,9 +6879,9 @@ var __toESM = (mod, isNodeMode, target) => {
6318
6879
  return to;
6319
6880
  };
6320
6881
  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);
6882
+ var __returnValue2 = (v) => v;
6883
+ function __exportSetter2(name, newValue) {
6884
+ this[name] = __returnValue2.bind(null, newValue);
6324
6885
  }
6325
6886
  var __export2 = (target, all) => {
6326
6887
  for (var name in all)
@@ -6328,7 +6889,7 @@ var __export2 = (target, all) => {
6328
6889
  get: all[name],
6329
6890
  enumerable: true,
6330
6891
  configurable: true,
6331
- set: __exportSetter.bind(all, name)
6892
+ set: __exportSetter2.bind(all, name)
6332
6893
  });
6333
6894
  };
6334
6895
  var __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -16459,6 +17020,45 @@ var PG_MIGRATIONS = [
16459
17020
  ALTER TABLE task_dependencies ADD COLUMN external_project_id TEXT;
16460
17021
  ALTER TABLE task_dependencies ADD COLUMN external_task_id TEXT;
16461
17022
  INSERT INTO _migrations (id) VALUES (43) ON CONFLICT DO NOTHING;
17023
+ `,
17024
+ `
17025
+ CREATE TABLE IF NOT EXISTS task_checkpoints (
17026
+ id TEXT PRIMARY KEY,
17027
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
17028
+ agent_id TEXT,
17029
+ step TEXT NOT NULL,
17030
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed', 'skipped')),
17031
+ data TEXT DEFAULT '{}',
17032
+ error TEXT,
17033
+ attempt INTEGER NOT NULL DEFAULT 1,
17034
+ max_attempts INTEGER NOT NULL DEFAULT 1,
17035
+ started_at TIMESTAMPTZ,
17036
+ completed_at TIMESTAMPTZ,
17037
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
17038
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
17039
+ );
17040
+ CREATE INDEX IF NOT EXISTS idx_task_checkpoints_task ON task_checkpoints(task_id);
17041
+ CREATE INDEX IF NOT EXISTS idx_task_checkpoints_status ON task_checkpoints(status);
17042
+
17043
+ CREATE TABLE IF NOT EXISTS task_heartbeats (
17044
+ id TEXT PRIMARY KEY,
17045
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
17046
+ agent_id TEXT,
17047
+ step TEXT,
17048
+ message TEXT,
17049
+ progress REAL CHECK(progress >= 0 AND progress <= 1),
17050
+ meta TEXT DEFAULT '{}',
17051
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
17052
+ );
17053
+ CREATE INDEX IF NOT EXISTS idx_task_heartbeats_task ON task_heartbeats(task_id);
17054
+ CREATE INDEX IF NOT EXISTS idx_task_heartbeats_agent ON task_heartbeats(agent_id);
17055
+
17056
+ ALTER TABLE tasks ADD COLUMN runner_id TEXT;
17057
+ ALTER TABLE tasks ADD COLUMN runner_started_at TIMESTAMPTZ;
17058
+ ALTER TABLE tasks ADD COLUMN runner_completed_at TIMESTAMPTZ;
17059
+ ALTER TABLE tasks ADD COLUMN current_step TEXT;
17060
+ ALTER TABLE tasks ADD COLUMN total_steps INTEGER;
17061
+ INSERT INTO _migrations (id) VALUES (44) ON CONFLICT DO NOTHING;
16462
17062
  `
16463
17063
  ];
16464
17064
 
@@ -16772,15 +17372,24 @@ function renderBurndownChart(total, days) {
16772
17372
  `);
16773
17373
  }
16774
17374
  // src/lib/github.ts
16775
- import { execSync } from "child_process";
17375
+ import { execFileSync } from "child_process";
16776
17376
  function parseGitHubUrl(url) {
16777
- const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
17377
+ 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
17378
  if (!match)
16779
17379
  return null;
16780
17380
  return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
16781
17381
  }
17382
+ function isSafeOwnerRepo(value) {
17383
+ return /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value) && value.length <= 39;
17384
+ }
16782
17385
  function fetchGitHubIssue(owner, repo, number) {
16783
- const json = execSync(`gh api repos/${owner}/${repo}/issues/${number}`, { encoding: "utf-8", timeout: 15000 });
17386
+ if (!isSafeOwnerRepo(owner))
17387
+ throw new Error(`Invalid GitHub owner: ${owner}`);
17388
+ if (!isSafeOwnerRepo(repo))
17389
+ throw new Error(`Invalid GitHub repo: ${repo}`);
17390
+ if (!Number.isInteger(number) || number < 1)
17391
+ throw new Error(`Invalid issue number: ${number}`);
17392
+ const json = execFileSync("gh", ["api", `repos/${owner}/${repo}/issues/${number}`], { encoding: "utf-8", timeout: 15000 });
16784
17393
  const data = JSON.parse(json);
16785
17394
  return {
16786
17395
  number: data.number,