@hasna/todos 0.9.68 → 0.9.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2599,6 +2599,58 @@ var init_database = __esm(() => {
2599
2599
  ALTER TABLE task_comments ADD COLUMN type TEXT DEFAULT 'comment' CHECK(type IN ('comment', 'progress', 'note'));
2600
2600
  ALTER TABLE task_comments ADD COLUMN progress_pct INTEGER CHECK(progress_pct IS NULL OR (progress_pct >= 0 AND progress_pct <= 100));
2601
2601
  INSERT OR IGNORE INTO _migrations (id) VALUES (14);
2602
+ `,
2603
+ `
2604
+ CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
2605
+ task_id UNINDEXED,
2606
+ title,
2607
+ description,
2608
+ tags,
2609
+ tokenize='unicode61 remove_diacritics 2'
2610
+ );
2611
+
2612
+ INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
2613
+ SELECT t.rowid, t.id, t.title, COALESCE(t.description, ''),
2614
+ COALESCE((SELECT GROUP_CONCAT(tag, ' ') FROM task_tags WHERE task_id = t.id), '')
2615
+ FROM tasks t;
2616
+
2617
+ CREATE TRIGGER IF NOT EXISTS tasks_fts_ai AFTER INSERT ON tasks BEGIN
2618
+ INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
2619
+ VALUES (new.rowid, new.id, new.title, COALESCE(new.description, ''), '');
2620
+ END;
2621
+
2622
+ CREATE TRIGGER IF NOT EXISTS tasks_fts_ad AFTER DELETE ON tasks BEGIN
2623
+ DELETE FROM tasks_fts WHERE rowid = old.rowid;
2624
+ END;
2625
+
2626
+ CREATE TRIGGER IF NOT EXISTS tasks_fts_au AFTER UPDATE OF title, description ON tasks BEGIN
2627
+ DELETE FROM tasks_fts WHERE rowid = old.rowid;
2628
+ INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
2629
+ SELECT new.rowid, new.id, new.title, COALESCE(new.description, ''),
2630
+ COALESCE((SELECT GROUP_CONCAT(tag, ' ') FROM task_tags WHERE task_id = new.id), '');
2631
+ END;
2632
+
2633
+ CREATE TRIGGER IF NOT EXISTS task_tags_fts_ai AFTER INSERT ON task_tags BEGIN
2634
+ DELETE FROM tasks_fts WHERE rowid = (SELECT rowid FROM tasks WHERE id = new.task_id);
2635
+ INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
2636
+ SELECT t.rowid, t.id, t.title, COALESCE(t.description, ''),
2637
+ COALESCE((SELECT GROUP_CONCAT(tag, ' ') FROM task_tags WHERE task_id = t.id), '')
2638
+ FROM tasks t WHERE t.id = new.task_id;
2639
+ END;
2640
+
2641
+ CREATE TRIGGER IF NOT EXISTS task_tags_fts_ad AFTER DELETE ON task_tags BEGIN
2642
+ DELETE FROM tasks_fts WHERE rowid = (SELECT rowid FROM tasks WHERE id = old.task_id);
2643
+ INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
2644
+ SELECT t.rowid, t.id, t.title, COALESCE(t.description, ''),
2645
+ COALESCE((SELECT GROUP_CONCAT(tag, ' ') FROM task_tags WHERE task_id = t.id), '')
2646
+ FROM tasks t WHERE t.id = old.task_id;
2647
+ END;
2648
+
2649
+ INSERT OR IGNORE INTO _migrations (id) VALUES (15);
2650
+ `,
2651
+ `
2652
+ ALTER TABLE tasks ADD COLUMN spawns_template_id TEXT REFERENCES task_templates(id) ON DELETE SET NULL;
2653
+ INSERT OR IGNORE INTO _migrations (id) VALUES (16);
2602
2654
  `
2603
2655
  ];
2604
2656
  });
@@ -3184,6 +3236,73 @@ var init_webhooks = __esm(() => {
3184
3236
  init_database();
3185
3237
  });
3186
3238
 
3239
+ // src/db/templates.ts
3240
+ var exports_templates = {};
3241
+ __export(exports_templates, {
3242
+ taskFromTemplate: () => taskFromTemplate,
3243
+ listTemplates: () => listTemplates,
3244
+ getTemplate: () => getTemplate,
3245
+ deleteTemplate: () => deleteTemplate,
3246
+ createTemplate: () => createTemplate
3247
+ });
3248
+ function rowToTemplate(row) {
3249
+ return {
3250
+ ...row,
3251
+ tags: JSON.parse(row.tags || "[]"),
3252
+ metadata: JSON.parse(row.metadata || "{}"),
3253
+ priority: row.priority || "medium"
3254
+ };
3255
+ }
3256
+ function createTemplate(input, db) {
3257
+ const d = db || getDatabase();
3258
+ const id = uuid();
3259
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
3260
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3261
+ id,
3262
+ input.name,
3263
+ input.title_pattern,
3264
+ input.description || null,
3265
+ input.priority || "medium",
3266
+ JSON.stringify(input.tags || []),
3267
+ input.project_id || null,
3268
+ input.plan_id || null,
3269
+ JSON.stringify(input.metadata || {}),
3270
+ now()
3271
+ ]);
3272
+ return getTemplate(id, d);
3273
+ }
3274
+ function getTemplate(id, db) {
3275
+ const d = db || getDatabase();
3276
+ const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(id);
3277
+ return row ? rowToTemplate(row) : null;
3278
+ }
3279
+ function listTemplates(db) {
3280
+ const d = db || getDatabase();
3281
+ return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
3282
+ }
3283
+ function deleteTemplate(id, db) {
3284
+ const d = db || getDatabase();
3285
+ return d.run("DELETE FROM task_templates WHERE id = ?", [id]).changes > 0;
3286
+ }
3287
+ function taskFromTemplate(templateId, overrides = {}, db) {
3288
+ const t = getTemplate(templateId, db);
3289
+ if (!t)
3290
+ throw new Error(`Template not found: ${templateId}`);
3291
+ return {
3292
+ title: overrides.title || t.title_pattern,
3293
+ description: overrides.description ?? t.description ?? undefined,
3294
+ priority: overrides.priority ?? t.priority,
3295
+ tags: overrides.tags ?? t.tags,
3296
+ project_id: overrides.project_id ?? t.project_id ?? undefined,
3297
+ plan_id: overrides.plan_id ?? t.plan_id ?? undefined,
3298
+ metadata: overrides.metadata ?? t.metadata,
3299
+ ...overrides
3300
+ };
3301
+ }
3302
+ var init_templates = __esm(() => {
3303
+ init_database();
3304
+ });
3305
+
3187
3306
  // src/db/tasks.ts
3188
3307
  var exports_tasks = {};
3189
3308
  __export(exports_tasks, {
@@ -3250,8 +3369,8 @@ function createTask(input, db) {
3250
3369
  const tags = input.tags || [];
3251
3370
  const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
3252
3371
  const title = shortId ? `${shortId}: ${input.title}` : input.title;
3253
- 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)
3254
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3372
+ 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)
3373
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3255
3374
  id,
3256
3375
  shortId,
3257
3376
  input.project_id || null,
@@ -3276,7 +3395,8 @@ function createTask(input, db) {
3276
3395
  null,
3277
3396
  null,
3278
3397
  input.recurrence_rule || null,
3279
- input.recurrence_parent_id || null
3398
+ input.recurrence_parent_id || null,
3399
+ input.spawns_template_id || null
3280
3400
  ]);
3281
3401
  if (tags.length > 0) {
3282
3402
  insertTaskTags(id, tags, d);
@@ -3383,19 +3503,25 @@ function listTasks(filter = {}, db) {
3383
3503
  } else if (filter.has_recurrence === false) {
3384
3504
  conditions.push("recurrence_rule IS NULL");
3385
3505
  }
3506
+ const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
3507
+ if (filter.cursor) {
3508
+ try {
3509
+ const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
3510
+ conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
3511
+ params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
3512
+ } catch {}
3513
+ }
3386
3514
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3387
3515
  let limitClause = "";
3388
3516
  if (filter.limit) {
3389
3517
  limitClause = " LIMIT ?";
3390
3518
  params.push(filter.limit);
3391
- if (filter.offset) {
3519
+ if (!filter.cursor && filter.offset) {
3392
3520
  limitClause += " OFFSET ?";
3393
3521
  params.push(filter.offset);
3394
3522
  }
3395
3523
  }
3396
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY
3397
- CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
3398
- created_at DESC${limitClause}`).all(...params);
3524
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
3399
3525
  return rows.map(rowToTask);
3400
3526
  }
3401
3527
  function countTasks(filter = {}, db) {
@@ -3640,10 +3766,25 @@ function completeTask(id, agentId, db, options) {
3640
3766
  if (task.recurrence_rule && !options?.skip_recurrence) {
3641
3767
  spawnedTask = spawnNextRecurrence(task, d);
3642
3768
  }
3769
+ let spawnedFromTemplate = null;
3770
+ if (task.spawns_template_id) {
3771
+ try {
3772
+ const input = taskFromTemplate(task.spawns_template_id, {
3773
+ project_id: task.project_id ?? undefined,
3774
+ plan_id: task.plan_id ?? undefined,
3775
+ task_list_id: task.task_list_id ?? undefined,
3776
+ assigned_to: task.assigned_to ?? undefined
3777
+ }, d);
3778
+ spawnedFromTemplate = createTask(input, d);
3779
+ } catch {}
3780
+ }
3643
3781
  const meta = hasEvidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
3644
3782
  if (spawnedTask) {
3645
3783
  meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
3646
3784
  }
3785
+ if (spawnedFromTemplate) {
3786
+ meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
3787
+ }
3647
3788
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
3648
3789
  }
3649
3790
  function lockTask(id, agentId, db) {
@@ -3855,11 +3996,21 @@ function getNextTask(agentId, filters, db) {
3855
3996
  }
3856
3997
  conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')");
3857
3998
  const where = conditions.join(" AND ");
3999
+ let recentProjectIds = [];
4000
+ if (agentId) {
4001
+ const recentRows = d.query(`SELECT DISTINCT project_id FROM tasks WHERE assigned_to = ? AND status = 'completed' AND project_id IS NOT NULL ORDER BY completed_at DESC LIMIT 3`).all(agentId);
4002
+ recentProjectIds = recentRows.map((r) => r.project_id);
4003
+ }
3858
4004
  let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
3859
4005
  if (agentId) {
3860
4006
  sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
3861
4007
  params.push(agentId);
3862
4008
  }
4009
+ if (recentProjectIds.length > 0) {
4010
+ const placeholders = recentProjectIds.map(() => "?").join(",");
4011
+ sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
4012
+ params.push(...recentProjectIds);
4013
+ }
3863
4014
  sql += `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, created_at ASC LIMIT 1`;
3864
4015
  const row = d.query(sql).get(...params);
3865
4016
  return row ? rowToTask(row) : null;
@@ -4195,6 +4346,7 @@ var init_tasks = __esm(() => {
4195
4346
  init_audit();
4196
4347
  init_recurrence();
4197
4348
  init_webhooks();
4349
+ init_templates();
4198
4350
  });
4199
4351
 
4200
4352
  // src/db/agents.ts
@@ -4541,68 +4693,95 @@ function rowToTask2(row) {
4541
4693
  requires_approval: Boolean(row.requires_approval)
4542
4694
  };
4543
4695
  }
4696
+ function hasFts(db) {
4697
+ try {
4698
+ const result = db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='tasks_fts'").get();
4699
+ return result !== null;
4700
+ } catch {
4701
+ return false;
4702
+ }
4703
+ }
4704
+ function escapeFtsQuery(q) {
4705
+ return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
4706
+ }
4544
4707
  function searchTasks(options, projectId, taskListId, db) {
4545
4708
  const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
4546
4709
  const d = db || getDatabase();
4547
4710
  clearExpiredLocks(d);
4548
- const pattern = `%${opts.query}%`;
4549
- let sql = `SELECT * FROM tasks WHERE (title LIKE ? OR description LIKE ? OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = tasks.id AND tag LIKE ?))`;
4550
- const params = [pattern, pattern, pattern];
4711
+ const params = [];
4712
+ let sql;
4713
+ if (hasFts(d) && opts.query.trim()) {
4714
+ const ftsQuery = escapeFtsQuery(opts.query);
4715
+ sql = `SELECT t.* FROM tasks t
4716
+ INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
4717
+ WHERE tasks_fts MATCH ?`;
4718
+ params.push(ftsQuery);
4719
+ } else {
4720
+ const pattern = `%${opts.query}%`;
4721
+ sql = `SELECT * FROM tasks t WHERE (t.title LIKE ? OR t.description LIKE ? OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?))`;
4722
+ params.push(pattern, pattern, pattern);
4723
+ }
4551
4724
  if (opts.project_id) {
4552
- sql += " AND project_id = ?";
4725
+ sql += " AND t.project_id = ?";
4553
4726
  params.push(opts.project_id);
4554
4727
  }
4555
4728
  if (opts.task_list_id) {
4556
- sql += " AND task_list_id = ?";
4729
+ sql += " AND t.task_list_id = ?";
4557
4730
  params.push(opts.task_list_id);
4558
4731
  }
4559
4732
  if (opts.status) {
4560
4733
  if (Array.isArray(opts.status)) {
4561
- sql += ` AND status IN (${opts.status.map(() => "?").join(",")})`;
4734
+ sql += ` AND t.status IN (${opts.status.map(() => "?").join(",")})`;
4562
4735
  params.push(...opts.status);
4563
4736
  } else {
4564
- sql += " AND status = ?";
4737
+ sql += " AND t.status = ?";
4565
4738
  params.push(opts.status);
4566
4739
  }
4567
4740
  }
4568
4741
  if (opts.priority) {
4569
4742
  if (Array.isArray(opts.priority)) {
4570
- sql += ` AND priority IN (${opts.priority.map(() => "?").join(",")})`;
4743
+ sql += ` AND t.priority IN (${opts.priority.map(() => "?").join(",")})`;
4571
4744
  params.push(...opts.priority);
4572
4745
  } else {
4573
- sql += " AND priority = ?";
4746
+ sql += " AND t.priority = ?";
4574
4747
  params.push(opts.priority);
4575
4748
  }
4576
4749
  }
4577
4750
  if (opts.assigned_to) {
4578
- sql += " AND assigned_to = ?";
4751
+ sql += " AND t.assigned_to = ?";
4579
4752
  params.push(opts.assigned_to);
4580
4753
  }
4581
4754
  if (opts.agent_id) {
4582
- sql += " AND agent_id = ?";
4755
+ sql += " AND t.agent_id = ?";
4583
4756
  params.push(opts.agent_id);
4584
4757
  }
4585
4758
  if (opts.created_after) {
4586
- sql += " AND created_at > ?";
4759
+ sql += " AND t.created_at > ?";
4587
4760
  params.push(opts.created_after);
4588
4761
  }
4589
4762
  if (opts.updated_after) {
4590
- sql += " AND updated_at > ?";
4763
+ sql += " AND t.updated_at > ?";
4591
4764
  params.push(opts.updated_after);
4592
4765
  }
4593
4766
  if (opts.has_dependencies === true) {
4594
- sql += " AND id IN (SELECT task_id FROM task_dependencies)";
4767
+ sql += " AND t.id IN (SELECT task_id FROM task_dependencies)";
4595
4768
  } else if (opts.has_dependencies === false) {
4596
- sql += " AND id NOT IN (SELECT task_id FROM task_dependencies)";
4769
+ sql += " AND t.id NOT IN (SELECT task_id FROM task_dependencies)";
4597
4770
  }
4598
4771
  if (opts.is_blocked === true) {
4599
- sql += " AND id IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
4772
+ sql += " AND t.id IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
4600
4773
  } else if (opts.is_blocked === false) {
4601
- sql += " AND id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
4774
+ sql += " AND t.id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
4775
+ }
4776
+ if (hasFts(d) && opts.query.trim()) {
4777
+ sql += ` ORDER BY bm25(tasks_fts),
4778
+ CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
4779
+ t.created_at DESC`;
4780
+ } else {
4781
+ sql += ` ORDER BY
4782
+ CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
4783
+ t.created_at DESC`;
4602
4784
  }
4603
- sql += ` ORDER BY
4604
- CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
4605
- created_at DESC`;
4606
4785
  const rows = d.query(sql).all(...params);
4607
4786
  return rows.map(rowToTask2);
4608
4787
  }
@@ -5142,73 +5321,6 @@ var init_sync = __esm(() => {
5142
5321
  init_config();
5143
5322
  });
5144
5323
 
5145
- // src/db/templates.ts
5146
- var exports_templates = {};
5147
- __export(exports_templates, {
5148
- taskFromTemplate: () => taskFromTemplate,
5149
- listTemplates: () => listTemplates,
5150
- getTemplate: () => getTemplate,
5151
- deleteTemplate: () => deleteTemplate,
5152
- createTemplate: () => createTemplate
5153
- });
5154
- function rowToTemplate(row) {
5155
- return {
5156
- ...row,
5157
- tags: JSON.parse(row.tags || "[]"),
5158
- metadata: JSON.parse(row.metadata || "{}"),
5159
- priority: row.priority || "medium"
5160
- };
5161
- }
5162
- function createTemplate(input, db) {
5163
- const d = db || getDatabase();
5164
- const id = uuid();
5165
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
5166
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5167
- id,
5168
- input.name,
5169
- input.title_pattern,
5170
- input.description || null,
5171
- input.priority || "medium",
5172
- JSON.stringify(input.tags || []),
5173
- input.project_id || null,
5174
- input.plan_id || null,
5175
- JSON.stringify(input.metadata || {}),
5176
- now()
5177
- ]);
5178
- return getTemplate(id, d);
5179
- }
5180
- function getTemplate(id, db) {
5181
- const d = db || getDatabase();
5182
- const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(id);
5183
- return row ? rowToTemplate(row) : null;
5184
- }
5185
- function listTemplates(db) {
5186
- const d = db || getDatabase();
5187
- return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
5188
- }
5189
- function deleteTemplate(id, db) {
5190
- const d = db || getDatabase();
5191
- return d.run("DELETE FROM task_templates WHERE id = ?", [id]).changes > 0;
5192
- }
5193
- function taskFromTemplate(templateId, overrides = {}, db) {
5194
- const t = getTemplate(templateId, db);
5195
- if (!t)
5196
- throw new Error(`Template not found: ${templateId}`);
5197
- return {
5198
- title: overrides.title || t.title_pattern,
5199
- description: overrides.description ?? t.description ?? undefined,
5200
- priority: overrides.priority ?? t.priority,
5201
- tags: overrides.tags ?? t.tags,
5202
- project_id: overrides.project_id ?? t.project_id ?? undefined,
5203
- plan_id: overrides.plan_id ?? t.plan_id ?? undefined,
5204
- metadata: overrides.metadata ?? t.metadata,
5205
- ...overrides
5206
- };
5207
- }
5208
- var init_templates = __esm(() => {
5209
- init_database();
5210
- });
5211
-
5212
5324
  // node_modules/zod/v3/helpers/util.js
5213
5325
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
5214
5326
  const t = typeof data;
@@ -9256,15 +9368,17 @@ function formatTask(task) {
9256
9368
  const recur = task.recurrence_rule ? ` [\u21BB]` : "";
9257
9369
  return `${id} ${task.status.padEnd(11)} ${task.priority.padEnd(8)} ${task.title}${assigned}${lock}${recur}`;
9258
9370
  }
9259
- function formatTaskDetail(task) {
9371
+ function formatTaskDetail(task, maxDescriptionChars) {
9260
9372
  const parts = [
9261
9373
  `ID: ${task.id}`,
9262
9374
  `Title: ${task.title}`,
9263
9375
  `Status: ${task.status}`,
9264
9376
  `Priority: ${task.priority}`
9265
9377
  ];
9266
- if (task.description)
9267
- parts.push(`Description: ${task.description}`);
9378
+ if (task.description) {
9379
+ const desc = maxDescriptionChars && task.description.length > maxDescriptionChars ? task.description.slice(0, maxDescriptionChars) + "\u2026" : task.description;
9380
+ parts.push(`Description: ${desc}`);
9381
+ }
9268
9382
  if (task.assigned_to)
9269
9383
  parts.push(`Assigned to: ${task.assigned_to}`);
9270
9384
  if (task.agent_id)
@@ -9322,7 +9436,9 @@ var init_mcp = __esm(() => {
9322
9436
  "get_task",
9323
9437
  "start_task",
9324
9438
  "add_comment",
9325
- "get_next_task"
9439
+ "get_next_task",
9440
+ "bootstrap",
9441
+ "get_tasks_changed_since"
9326
9442
  ]);
9327
9443
  STANDARD_EXCLUDED = new Set([
9328
9444
  "get_org_chart",
@@ -9356,7 +9472,8 @@ var init_mcp = __esm(() => {
9356
9472
  metadata: exports_external.record(exports_external.unknown()).optional(),
9357
9473
  estimated_minutes: exports_external.number().optional(),
9358
9474
  requires_approval: exports_external.boolean().optional(),
9359
- recurrence_rule: exports_external.string().optional()
9475
+ recurrence_rule: exports_external.string().optional(),
9476
+ spawns_template_id: exports_external.string().optional().describe("Template ID to auto-create as next task when this task is completed (pipeline/handoff chains)")
9360
9477
  }, async (params) => {
9361
9478
  try {
9362
9479
  const resolved = { ...params };
@@ -9376,7 +9493,7 @@ var init_mcp = __esm(() => {
9376
9493
  });
9377
9494
  }
9378
9495
  if (shouldRegisterTool("list_tasks")) {
9379
- server.tool("list_tasks", "List tasks with optional filters and pagination.", {
9496
+ server.tool("list_tasks", "List tasks with optional filters and pagination. Default limit is 50 \u2014 use offset to page through results.", {
9380
9497
  project_id: exports_external.string().optional(),
9381
9498
  status: exports_external.union([
9382
9499
  exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]),
@@ -9394,11 +9511,15 @@ var init_mcp = __esm(() => {
9394
9511
  due_today: exports_external.boolean().optional(),
9395
9512
  overdue: exports_external.boolean().optional(),
9396
9513
  limit: exports_external.number().optional(),
9397
- offset: exports_external.number().optional()
9514
+ offset: exports_external.number().optional(),
9515
+ summary_only: exports_external.boolean().optional().describe("When true, return only id, short_id, title, status, priority \u2014 minimal tokens for navigation"),
9516
+ cursor: exports_external.string().optional().describe("Opaque cursor from a prior response for stable pagination. Use next_cursor from the previous page. Mutually exclusive with offset.")
9398
9517
  }, async (params) => {
9399
9518
  try {
9400
- const { due_today, overdue, ...rest } = params;
9519
+ const { due_today, overdue, summary_only, ...rest } = params;
9401
9520
  const resolved = { ...rest };
9521
+ if (resolved.limit === undefined)
9522
+ resolved.limit = 50;
9402
9523
  if (resolved.project_id)
9403
9524
  resolved.project_id = resolveId(resolved.project_id, "projects");
9404
9525
  if (resolved.plan_id)
@@ -9420,6 +9541,9 @@ var init_mcp = __esm(() => {
9420
9541
  return { content: [{ type: "text", text: total > 0 ? `No tasks in this page (total: ${total}).` : "No tasks found." }] };
9421
9542
  }
9422
9543
  const text = tasks.map((t) => {
9544
+ if (summary_only) {
9545
+ return `${t.short_id || t.id.slice(0, 8)} [${t.status}] ${t.priority} ${t.title}`;
9546
+ }
9423
9547
  const lock = t.locked_by ? ` [locked by ${t.locked_by}]` : "";
9424
9548
  const assigned = t.assigned_to ? ` -> ${t.assigned_to}` : "";
9425
9549
  const due = t.due_at ? ` due:${t.due_at.slice(0, 10)}` : "";
@@ -9427,10 +9551,23 @@ var init_mcp = __esm(() => {
9427
9551
  return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned}${lock}${due}${recur}`;
9428
9552
  }).join(`
9429
9553
  `);
9430
- const pagination = resolved.limit ? `
9431
- (showing ${tasks.length} of ${total}, offset: ${resolved.offset || 0})` : "";
9554
+ const currentOffset = resolved.offset || 0;
9555
+ const hasMore = total > (resolved.cursor ? tasks.length : currentOffset + tasks.length);
9556
+ let paginationNote = `
9557
+ (showing ${tasks.length} of ${total}`;
9558
+ if (hasMore) {
9559
+ if (resolved.cursor || tasks.length > 0) {
9560
+ const last = tasks[tasks.length - 1];
9561
+ const priorityRank = { critical: 0, high: 1, medium: 2, low: 3 }[last.priority] ?? 3;
9562
+ const cursorPayload = Buffer.from(JSON.stringify({ p: priorityRank, c: last.created_at, i: last.id })).toString("base64");
9563
+ paginationNote += ` \u2014 next_cursor: ${cursorPayload}`;
9564
+ } else {
9565
+ paginationNote += ` \u2014 use offset: ${currentOffset + tasks.length} to get next page`;
9566
+ }
9567
+ }
9568
+ paginationNote += ")";
9432
9569
  return { content: [{ type: "text", text: `${tasks.length} task(s):
9433
- ${text}${pagination}` }] };
9570
+ ${text}${paginationNote}` }] };
9434
9571
  } catch (e) {
9435
9572
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9436
9573
  }
@@ -9438,14 +9575,15 @@ ${text}${pagination}` }] };
9438
9575
  }
9439
9576
  if (shouldRegisterTool("get_task")) {
9440
9577
  server.tool("get_task", "Get full task details with subtasks, deps, and comments.", {
9441
- id: exports_external.string()
9442
- }, async ({ id }) => {
9578
+ id: exports_external.string(),
9579
+ max_description_chars: exports_external.number().optional().describe("Truncate description to this many characters (default: unlimited). Use 300-500 for quick checks.")
9580
+ }, async ({ id, max_description_chars }) => {
9443
9581
  try {
9444
9582
  const resolvedId = resolveId(id);
9445
9583
  const task = getTaskWithRelations(resolvedId);
9446
9584
  if (!task)
9447
9585
  return { content: [{ type: "text", text: `Task not found: ${id}` }], isError: true };
9448
- const parts = [formatTaskDetail(task)];
9586
+ const parts = [formatTaskDetail(task, max_description_chars)];
9449
9587
  if (task.subtasks.length > 0) {
9450
9588
  parts.push(`
9451
9589
  Subtasks (${task.subtasks.length}):`);
@@ -10673,7 +10811,7 @@ ${lines.join(`
10673
10811
  return { content: [{ type: "text", text: "No tasks available \u2014 all pending tasks are blocked, locked, or none exist." }] };
10674
10812
  }
10675
10813
  return { content: [{ type: "text", text: `next: ${formatTask(task)}
10676
- ${formatTaskDetail(task)}` }] };
10814
+ ${formatTaskDetail(task, 300)}` }] };
10677
10815
  } catch (e) {
10678
10816
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
10679
10817
  }
@@ -10759,7 +10897,7 @@ ${text}` }] };
10759
10897
  return { content: [{ type: "text", text: "No tasks available to claim." }] };
10760
10898
  }
10761
10899
  return { content: [{ type: "text", text: `claimed: ${formatTask(task)}
10762
- ${formatTaskDetail(task)}` }] };
10900
+ ${formatTaskDetail(task, 300)}` }] };
10763
10901
  } catch (e) {
10764
10902
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
10765
10903
  }
@@ -10844,6 +10982,8 @@ No pending tasks available.`);
10844
10982
  }
10845
10983
  }
10846
10984
  }
10985
+ lines.push(`
10986
+ as_of: ${new Date().toISOString()} (pass to get_tasks_changed_since for incremental polling)`);
10847
10987
  return { content: [{ type: "text", text: lines.join(`
10848
10988
  `) }] };
10849
10989
  } catch (e) {
@@ -10908,6 +11048,29 @@ No pending tasks available.`);
10908
11048
  }
10909
11049
  });
10910
11050
  }
11051
+ if (shouldRegisterTool("get_health")) {
11052
+ server.tool("get_health", "Check todos DB health. Returns status and issue summary.", {
11053
+ project_id: exports_external.string().optional()
11054
+ }, async ({ project_id }) => {
11055
+ try {
11056
+ const checks = [];
11057
+ const all = listTasks({});
11058
+ checks.push({ name: "tasks", status: "ok", value: `${all.length} total` });
11059
+ const stale = getStaleTasks(30, project_id ? { project_id: resolveId(project_id, "projects") } : undefined);
11060
+ checks.push({ name: "stale", status: stale.length > 0 ? "warn" : "ok", value: `${stale.length} stuck in_progress >30min` });
11061
+ const nowStr = new Date().toISOString();
11062
+ const overdue = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < nowStr);
11063
+ checks.push({ name: "overdue_recurring", status: overdue.length > 0 ? "warn" : "ok", value: `${overdue.length} overdue` });
11064
+ const status = checks.some((c) => c.status === "error") ? "error" : checks.some((c) => c.status === "warn") ? "warn" : "ok";
11065
+ const text = `Status: ${status}
11066
+ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c.value}`).join(`
11067
+ `)}`;
11068
+ return { content: [{ type: "text", text }] };
11069
+ } catch (e) {
11070
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11071
+ }
11072
+ });
11073
+ }
10911
11074
  if (shouldRegisterTool("get_context")) {
10912
11075
  server.tool("get_context", "Get a compact task summary for agent prompt injection. Returns formatted text.", {
10913
11076
  agent_id: exports_external.string().optional(),
@@ -10932,6 +11095,58 @@ No pending tasks available.`);
10932
11095
  }
10933
11096
  if (next)
10934
11097
  lines.push(`Next up: ${next.short_id || next.id.slice(0, 8)} [${next.priority}] ${next.title}`);
11098
+ lines.push(`as_of: ${new Date().toISOString()}`);
11099
+ return { content: [{ type: "text", text: lines.join(`
11100
+ `) }] };
11101
+ } catch (e) {
11102
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11103
+ }
11104
+ });
11105
+ }
11106
+ if (shouldRegisterTool("bootstrap")) {
11107
+ server.tool("bootstrap", "Single call for session start. Returns agent's in-progress task (if resuming), next claimable task, and project health \u2014 no side effects. Replaces 3-4 round trips at cold start.", {
11108
+ agent_id: exports_external.string().optional().describe("Your agent ID \u2014 used to find your active tasks and preferred next task"),
11109
+ project_id: exports_external.string().optional()
11110
+ }, async ({ agent_id, project_id }) => {
11111
+ try {
11112
+ const filters = {};
11113
+ if (project_id)
11114
+ filters.project_id = resolveId(project_id, "projects");
11115
+ const f = Object.keys(filters).length > 0 ? filters : undefined;
11116
+ const status = getStatus(f, agent_id);
11117
+ const next = getNextTask(agent_id, f);
11118
+ const lines = [];
11119
+ const myActive = agent_id ? status.active_work.filter((w) => w.assigned_to === agent_id || w.locked_by === agent_id) : [];
11120
+ if (myActive.length > 0) {
11121
+ lines.push(`## Resuming`);
11122
+ for (const w of myActive) {
11123
+ lines.push(`[${w.short_id || w.id.slice(0, 8)}] ${w.priority} \u2014 ${w.title}`);
11124
+ }
11125
+ lines.push("");
11126
+ }
11127
+ if (next) {
11128
+ lines.push(`## Next task to claim`);
11129
+ lines.push(`[${next.short_id || next.id.slice(0, 8)}] ${next.priority} \u2014 ${next.title}`);
11130
+ if (next.description)
11131
+ lines.push(next.description.slice(0, 300) + (next.description.length > 300 ? "\u2026" : ""));
11132
+ lines.push(` call: claim_next_task(agent_id: "${agent_id || "<your-id>"}")`);
11133
+ lines.push("");
11134
+ } else {
11135
+ lines.push(`## No tasks available to claim`);
11136
+ lines.push("");
11137
+ }
11138
+ lines.push(`## Health`);
11139
+ lines.push(`${status.pending} pending | ${status.in_progress} active | ${status.completed} done`);
11140
+ if (status.stale_count > 0)
11141
+ lines.push(`\u26A0 ${status.stale_count} stale task(s)`);
11142
+ if (status.overdue_recurring > 0)
11143
+ lines.push(`\uD83D\uDD01 ${status.overdue_recurring} overdue recurring`);
11144
+ if (status.active_work.length > 0) {
11145
+ const others = agent_id ? status.active_work.filter((w) => w.assigned_to !== agent_id && w.locked_by !== agent_id) : status.active_work;
11146
+ if (others.length > 0) {
11147
+ lines.push(`Other agents active: ${others.slice(0, 3).map((w) => `${w.short_id || w.id.slice(0, 8)} (${w.assigned_to || "?"})`).join(", ")}`);
11148
+ }
11149
+ }
10935
11150
  return { content: [{ type: "text", text: lines.join(`
10936
11151
  `) }] };
10937
11152
  } catch (e) {
@@ -11001,6 +11216,8 @@ No pending tasks available.`);
11001
11216
  "get_stale_tasks",
11002
11217
  "get_status",
11003
11218
  "get_context",
11219
+ "get_health",
11220
+ "bootstrap",
11004
11221
  "decompose_task",
11005
11222
  "set_task_status",
11006
11223
  "set_task_priority",
@@ -11018,9 +11235,9 @@ No pending tasks available.`);
11018
11235
  create_task: `Create a new task.
11019
11236
  Params: title(string, req), description(string), priority(low|medium|high|critical, default:medium), status(pending|in_progress|completed|failed|cancelled, default:pending), project_id(string), parent_id(string \u2014 creates subtask), plan_id(string), task_list_id(string), agent_id(string), assigned_to(string), tags(string[]), metadata(object), estimated_minutes(number), requires_approval(boolean), recurrence_rule(string \u2014 e.g. 'every day', 'every weekday', 'every 2 weeks', 'every monday'), session_id(string), working_dir(string)
11020
11237
  Example: {title: 'Daily standup', recurrence_rule: 'every weekday', priority: 'medium'}`,
11021
- list_tasks: `List tasks with optional filters. Supports pagination.
11022
- Params: status(string|string[]), priority(string|string[]), project_id(string), plan_id(string), task_list_id(string), assigned_to(string), tags(string[]), has_recurrence(boolean \u2014 true=only recurring, false=only non-recurring), limit(number), offset(number)
11023
- Example: {status: ['pending', 'in_progress'], has_recurrence: true, limit: 20}`,
11238
+ list_tasks: `List tasks with optional filters. Default limit is 50 to avoid context overflow \u2014 always paginate with offset for large lists.
11239
+ Params: status(string|string[]), priority(string|string[]), project_id(string), plan_id(string), task_list_id(string), assigned_to(string), tags(string[]), has_recurrence(boolean \u2014 true=only recurring, false=only non-recurring), limit(number, default 50), offset(number)
11240
+ Example: {status: ['pending', 'in_progress'], limit: 50, offset: 0}`,
11024
11241
  get_task: `Get full task details with subtasks, deps, and comments.
11025
11242
  Params: id(string, req \u2014 task ID, short_id like 'APP-00001', or partial ID)
11026
11243
  Example: {id: 'a1b2c3d4'}`,
@@ -11170,8 +11387,8 @@ No pending tasks available.`);
11170
11387
  get_active_work: `See all in-progress tasks and who is working on them.
11171
11388
  Params: project_id(string, optional), task_list_id(string, optional)
11172
11389
  Example: {project_id: 'a1b2c3d4'}`,
11173
- get_tasks_changed_since: `Get tasks modified after a timestamp \u2014 incremental delta sync.
11174
- Params: since(string, req \u2014 ISO date), project_id(string, optional), task_list_id(string, optional)
11390
+ get_tasks_changed_since: `PREFERRED POLLING PATTERN: Get only tasks modified after a timestamp \u2014 much cheaper than re-fetching everything. Save the as_of timestamp from bootstrap/get_status/get_context and pass it here on your next check.
11391
+ Params: since(string, req \u2014 ISO date from prior as_of), project_id(string, optional), task_list_id(string, optional)
11175
11392
  Example: {since: '2026-03-14T10:00:00Z'}`,
11176
11393
  get_stale_tasks: `Find stale in_progress tasks with no recent activity.
11177
11394
  Params: stale_minutes(number, default:30), project_id(string, optional), task_list_id(string, optional)
@@ -11179,6 +11396,9 @@ No pending tasks available.`);
11179
11396
  get_status: `Get a full project health snapshot \u2014 pending/in_progress/completed counts, active work, next recommended task, stale task count, overdue recurring tasks. Saves 4+ round trips at session start.
11180
11397
  Params: agent_id(string, optional \u2014 prefers tasks assigned to this agent for next_task), project_id(string, optional), task_list_id(string, optional)
11181
11398
  Example: {agent_id: 'a1b2c3d4', project_id: 'e5f6g7h8'}`,
11399
+ bootstrap: `CALL THIS FIRST at session start. Returns your in-progress task (if resuming), next claimable task with description, and project health \u2014 all in one call, no side effects. Eliminates 3-4 round trips.
11400
+ Params: agent_id(string, optional but recommended), project_id(string, optional)
11401
+ Example: {agent_id: 'a1b2c3d4'}`,
11182
11402
  decompose_task: `Break a task into subtasks in one call. Subtasks inherit project/plan/list from parent.
11183
11403
  Params: parent_id(string, req), subtasks(array, req \u2014 [{title, description, priority, assigned_to, estimated_minutes, tags}]), depends_on_prev(boolean \u2014 chain subtasks sequentially)
11184
11404
  Example: {parent_id: 'a1b2c3d4', subtasks: [{title: 'Research'}, {title: 'Implement'}, {title: 'Test'}], depends_on_prev: true}`,