@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/mcp/index.js CHANGED
@@ -533,6 +533,58 @@ var init_database = __esm(() => {
533
533
  ALTER TABLE task_comments ADD COLUMN type TEXT DEFAULT 'comment' CHECK(type IN ('comment', 'progress', 'note'));
534
534
  ALTER TABLE task_comments ADD COLUMN progress_pct INTEGER CHECK(progress_pct IS NULL OR (progress_pct >= 0 AND progress_pct <= 100));
535
535
  INSERT OR IGNORE INTO _migrations (id) VALUES (14);
536
+ `,
537
+ `
538
+ CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
539
+ task_id UNINDEXED,
540
+ title,
541
+ description,
542
+ tags,
543
+ tokenize='unicode61 remove_diacritics 2'
544
+ );
545
+
546
+ INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
547
+ SELECT t.rowid, t.id, t.title, COALESCE(t.description, ''),
548
+ COALESCE((SELECT GROUP_CONCAT(tag, ' ') FROM task_tags WHERE task_id = t.id), '')
549
+ FROM tasks t;
550
+
551
+ CREATE TRIGGER IF NOT EXISTS tasks_fts_ai AFTER INSERT ON tasks BEGIN
552
+ INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
553
+ VALUES (new.rowid, new.id, new.title, COALESCE(new.description, ''), '');
554
+ END;
555
+
556
+ CREATE TRIGGER IF NOT EXISTS tasks_fts_ad AFTER DELETE ON tasks BEGIN
557
+ DELETE FROM tasks_fts WHERE rowid = old.rowid;
558
+ END;
559
+
560
+ CREATE TRIGGER IF NOT EXISTS tasks_fts_au AFTER UPDATE OF title, description ON tasks BEGIN
561
+ DELETE FROM tasks_fts WHERE rowid = old.rowid;
562
+ INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
563
+ SELECT new.rowid, new.id, new.title, COALESCE(new.description, ''),
564
+ COALESCE((SELECT GROUP_CONCAT(tag, ' ') FROM task_tags WHERE task_id = new.id), '');
565
+ END;
566
+
567
+ CREATE TRIGGER IF NOT EXISTS task_tags_fts_ai AFTER INSERT ON task_tags BEGIN
568
+ DELETE FROM tasks_fts WHERE rowid = (SELECT rowid FROM tasks WHERE id = new.task_id);
569
+ INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
570
+ SELECT t.rowid, t.id, t.title, COALESCE(t.description, ''),
571
+ COALESCE((SELECT GROUP_CONCAT(tag, ' ') FROM task_tags WHERE task_id = t.id), '')
572
+ FROM tasks t WHERE t.id = new.task_id;
573
+ END;
574
+
575
+ CREATE TRIGGER IF NOT EXISTS task_tags_fts_ad AFTER DELETE ON task_tags BEGIN
576
+ DELETE FROM tasks_fts WHERE rowid = (SELECT rowid FROM tasks WHERE id = old.task_id);
577
+ INSERT INTO tasks_fts(rowid, task_id, title, description, tags)
578
+ SELECT t.rowid, t.id, t.title, COALESCE(t.description, ''),
579
+ COALESCE((SELECT GROUP_CONCAT(tag, ' ') FROM task_tags WHERE task_id = t.id), '')
580
+ FROM tasks t WHERE t.id = old.task_id;
581
+ END;
582
+
583
+ INSERT OR IGNORE INTO _migrations (id) VALUES (15);
584
+ `,
585
+ `
586
+ ALTER TABLE tasks ADD COLUMN spawns_template_id TEXT REFERENCES task_templates(id) ON DELETE SET NULL;
587
+ INSERT OR IGNORE INTO _migrations (id) VALUES (16);
536
588
  `
537
589
  ];
538
590
  });
@@ -615,6 +667,73 @@ var init_webhooks = __esm(() => {
615
667
  init_database();
616
668
  });
617
669
 
670
+ // src/db/templates.ts
671
+ var exports_templates = {};
672
+ __export(exports_templates, {
673
+ taskFromTemplate: () => taskFromTemplate,
674
+ listTemplates: () => listTemplates,
675
+ getTemplate: () => getTemplate,
676
+ deleteTemplate: () => deleteTemplate,
677
+ createTemplate: () => createTemplate
678
+ });
679
+ function rowToTemplate(row) {
680
+ return {
681
+ ...row,
682
+ tags: JSON.parse(row.tags || "[]"),
683
+ metadata: JSON.parse(row.metadata || "{}"),
684
+ priority: row.priority || "medium"
685
+ };
686
+ }
687
+ function createTemplate(input, db) {
688
+ const d = db || getDatabase();
689
+ const id = uuid();
690
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
691
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
692
+ id,
693
+ input.name,
694
+ input.title_pattern,
695
+ input.description || null,
696
+ input.priority || "medium",
697
+ JSON.stringify(input.tags || []),
698
+ input.project_id || null,
699
+ input.plan_id || null,
700
+ JSON.stringify(input.metadata || {}),
701
+ now()
702
+ ]);
703
+ return getTemplate(id, d);
704
+ }
705
+ function getTemplate(id, db) {
706
+ const d = db || getDatabase();
707
+ const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(id);
708
+ return row ? rowToTemplate(row) : null;
709
+ }
710
+ function listTemplates(db) {
711
+ const d = db || getDatabase();
712
+ return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
713
+ }
714
+ function deleteTemplate(id, db) {
715
+ const d = db || getDatabase();
716
+ return d.run("DELETE FROM task_templates WHERE id = ?", [id]).changes > 0;
717
+ }
718
+ function taskFromTemplate(templateId, overrides = {}, db) {
719
+ const t = getTemplate(templateId, db);
720
+ if (!t)
721
+ throw new Error(`Template not found: ${templateId}`);
722
+ return {
723
+ title: overrides.title || t.title_pattern,
724
+ description: overrides.description ?? t.description ?? undefined,
725
+ priority: overrides.priority ?? t.priority,
726
+ tags: overrides.tags ?? t.tags,
727
+ project_id: overrides.project_id ?? t.project_id ?? undefined,
728
+ plan_id: overrides.plan_id ?? t.plan_id ?? undefined,
729
+ metadata: overrides.metadata ?? t.metadata,
730
+ ...overrides
731
+ };
732
+ }
733
+ var init_templates = __esm(() => {
734
+ init_database();
735
+ });
736
+
618
737
  // src/db/agents.ts
619
738
  var exports_agents = {};
620
739
  __export(exports_agents, {
@@ -758,73 +877,6 @@ var init_agents = __esm(() => {
758
877
  init_database();
759
878
  });
760
879
 
761
- // src/db/templates.ts
762
- var exports_templates = {};
763
- __export(exports_templates, {
764
- taskFromTemplate: () => taskFromTemplate,
765
- listTemplates: () => listTemplates,
766
- getTemplate: () => getTemplate,
767
- deleteTemplate: () => deleteTemplate,
768
- createTemplate: () => createTemplate
769
- });
770
- function rowToTemplate(row) {
771
- return {
772
- ...row,
773
- tags: JSON.parse(row.tags || "[]"),
774
- metadata: JSON.parse(row.metadata || "{}"),
775
- priority: row.priority || "medium"
776
- };
777
- }
778
- function createTemplate(input, db) {
779
- const d = db || getDatabase();
780
- const id = uuid();
781
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
782
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
783
- id,
784
- input.name,
785
- input.title_pattern,
786
- input.description || null,
787
- input.priority || "medium",
788
- JSON.stringify(input.tags || []),
789
- input.project_id || null,
790
- input.plan_id || null,
791
- JSON.stringify(input.metadata || {}),
792
- now()
793
- ]);
794
- return getTemplate(id, d);
795
- }
796
- function getTemplate(id, db) {
797
- const d = db || getDatabase();
798
- const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(id);
799
- return row ? rowToTemplate(row) : null;
800
- }
801
- function listTemplates(db) {
802
- const d = db || getDatabase();
803
- return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
804
- }
805
- function deleteTemplate(id, db) {
806
- const d = db || getDatabase();
807
- return d.run("DELETE FROM task_templates WHERE id = ?", [id]).changes > 0;
808
- }
809
- function taskFromTemplate(templateId, overrides = {}, db) {
810
- const t = getTemplate(templateId, db);
811
- if (!t)
812
- throw new Error(`Template not found: ${templateId}`);
813
- return {
814
- title: overrides.title || t.title_pattern,
815
- description: overrides.description ?? t.description ?? undefined,
816
- priority: overrides.priority ?? t.priority,
817
- tags: overrides.tags ?? t.tags,
818
- project_id: overrides.project_id ?? t.project_id ?? undefined,
819
- plan_id: overrides.plan_id ?? t.plan_id ?? undefined,
820
- metadata: overrides.metadata ?? t.metadata,
821
- ...overrides
822
- };
823
- }
824
- var init_templates = __esm(() => {
825
- init_database();
826
- });
827
-
828
880
  // src/mcp/index.ts
829
881
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
830
882
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -5222,6 +5274,7 @@ function nextOccurrence(rule, from) {
5222
5274
 
5223
5275
  // src/db/tasks.ts
5224
5276
  init_webhooks();
5277
+ init_templates();
5225
5278
  function rowToTask(row) {
5226
5279
  return {
5227
5280
  ...row,
@@ -5252,8 +5305,8 @@ function createTask(input, db) {
5252
5305
  const tags = input.tags || [];
5253
5306
  const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
5254
5307
  const title = shortId ? `${shortId}: ${input.title}` : input.title;
5255
- 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)
5256
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5308
+ 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)
5309
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5257
5310
  id,
5258
5311
  shortId,
5259
5312
  input.project_id || null,
@@ -5278,7 +5331,8 @@ function createTask(input, db) {
5278
5331
  null,
5279
5332
  null,
5280
5333
  input.recurrence_rule || null,
5281
- input.recurrence_parent_id || null
5334
+ input.recurrence_parent_id || null,
5335
+ input.spawns_template_id || null
5282
5336
  ]);
5283
5337
  if (tags.length > 0) {
5284
5338
  insertTaskTags(id, tags, d);
@@ -5385,19 +5439,25 @@ function listTasks(filter = {}, db) {
5385
5439
  } else if (filter.has_recurrence === false) {
5386
5440
  conditions.push("recurrence_rule IS NULL");
5387
5441
  }
5442
+ const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
5443
+ if (filter.cursor) {
5444
+ try {
5445
+ const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
5446
+ conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
5447
+ params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
5448
+ } catch {}
5449
+ }
5388
5450
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5389
5451
  let limitClause = "";
5390
5452
  if (filter.limit) {
5391
5453
  limitClause = " LIMIT ?";
5392
5454
  params.push(filter.limit);
5393
- if (filter.offset) {
5455
+ if (!filter.cursor && filter.offset) {
5394
5456
  limitClause += " OFFSET ?";
5395
5457
  params.push(filter.offset);
5396
5458
  }
5397
5459
  }
5398
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY
5399
- CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
5400
- created_at DESC${limitClause}`).all(...params);
5460
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
5401
5461
  return rows.map(rowToTask);
5402
5462
  }
5403
5463
  function countTasks(filter = {}, db) {
@@ -5642,10 +5702,25 @@ function completeTask(id, agentId, db, options) {
5642
5702
  if (task.recurrence_rule && !options?.skip_recurrence) {
5643
5703
  spawnedTask = spawnNextRecurrence(task, d);
5644
5704
  }
5705
+ let spawnedFromTemplate = null;
5706
+ if (task.spawns_template_id) {
5707
+ try {
5708
+ const input = taskFromTemplate(task.spawns_template_id, {
5709
+ project_id: task.project_id ?? undefined,
5710
+ plan_id: task.plan_id ?? undefined,
5711
+ task_list_id: task.task_list_id ?? undefined,
5712
+ assigned_to: task.assigned_to ?? undefined
5713
+ }, d);
5714
+ spawnedFromTemplate = createTask(input, d);
5715
+ } catch {}
5716
+ }
5645
5717
  const meta = hasEvidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
5646
5718
  if (spawnedTask) {
5647
5719
  meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
5648
5720
  }
5721
+ if (spawnedFromTemplate) {
5722
+ meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
5723
+ }
5649
5724
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
5650
5725
  }
5651
5726
  function lockTask(id, agentId, db) {
@@ -5853,11 +5928,21 @@ function getNextTask(agentId, filters, db) {
5853
5928
  }
5854
5929
  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')");
5855
5930
  const where = conditions.join(" AND ");
5931
+ let recentProjectIds = [];
5932
+ if (agentId) {
5933
+ 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);
5934
+ recentProjectIds = recentRows.map((r) => r.project_id);
5935
+ }
5856
5936
  let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
5857
5937
  if (agentId) {
5858
5938
  sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
5859
5939
  params.push(agentId);
5860
5940
  }
5941
+ if (recentProjectIds.length > 0) {
5942
+ const placeholders = recentProjectIds.map(() => "?").join(",");
5943
+ sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
5944
+ params.push(...recentProjectIds);
5945
+ }
5861
5946
  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`;
5862
5947
  const row = d.query(sql).get(...params);
5863
5948
  return row ? rowToTask(row) : null;
@@ -6363,68 +6448,95 @@ function rowToTask2(row) {
6363
6448
  requires_approval: Boolean(row.requires_approval)
6364
6449
  };
6365
6450
  }
6451
+ function hasFts(db) {
6452
+ try {
6453
+ const result = db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='tasks_fts'").get();
6454
+ return result !== null;
6455
+ } catch {
6456
+ return false;
6457
+ }
6458
+ }
6459
+ function escapeFtsQuery(q) {
6460
+ return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
6461
+ }
6366
6462
  function searchTasks(options, projectId, taskListId, db) {
6367
6463
  const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
6368
6464
  const d = db || getDatabase();
6369
6465
  clearExpiredLocks(d);
6370
- const pattern = `%${opts.query}%`;
6371
- 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 ?))`;
6372
- const params = [pattern, pattern, pattern];
6466
+ const params = [];
6467
+ let sql;
6468
+ if (hasFts(d) && opts.query.trim()) {
6469
+ const ftsQuery = escapeFtsQuery(opts.query);
6470
+ sql = `SELECT t.* FROM tasks t
6471
+ INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
6472
+ WHERE tasks_fts MATCH ?`;
6473
+ params.push(ftsQuery);
6474
+ } else {
6475
+ const pattern = `%${opts.query}%`;
6476
+ 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 ?))`;
6477
+ params.push(pattern, pattern, pattern);
6478
+ }
6373
6479
  if (opts.project_id) {
6374
- sql += " AND project_id = ?";
6480
+ sql += " AND t.project_id = ?";
6375
6481
  params.push(opts.project_id);
6376
6482
  }
6377
6483
  if (opts.task_list_id) {
6378
- sql += " AND task_list_id = ?";
6484
+ sql += " AND t.task_list_id = ?";
6379
6485
  params.push(opts.task_list_id);
6380
6486
  }
6381
6487
  if (opts.status) {
6382
6488
  if (Array.isArray(opts.status)) {
6383
- sql += ` AND status IN (${opts.status.map(() => "?").join(",")})`;
6489
+ sql += ` AND t.status IN (${opts.status.map(() => "?").join(",")})`;
6384
6490
  params.push(...opts.status);
6385
6491
  } else {
6386
- sql += " AND status = ?";
6492
+ sql += " AND t.status = ?";
6387
6493
  params.push(opts.status);
6388
6494
  }
6389
6495
  }
6390
6496
  if (opts.priority) {
6391
6497
  if (Array.isArray(opts.priority)) {
6392
- sql += ` AND priority IN (${opts.priority.map(() => "?").join(",")})`;
6498
+ sql += ` AND t.priority IN (${opts.priority.map(() => "?").join(",")})`;
6393
6499
  params.push(...opts.priority);
6394
6500
  } else {
6395
- sql += " AND priority = ?";
6501
+ sql += " AND t.priority = ?";
6396
6502
  params.push(opts.priority);
6397
6503
  }
6398
6504
  }
6399
6505
  if (opts.assigned_to) {
6400
- sql += " AND assigned_to = ?";
6506
+ sql += " AND t.assigned_to = ?";
6401
6507
  params.push(opts.assigned_to);
6402
6508
  }
6403
6509
  if (opts.agent_id) {
6404
- sql += " AND agent_id = ?";
6510
+ sql += " AND t.agent_id = ?";
6405
6511
  params.push(opts.agent_id);
6406
6512
  }
6407
6513
  if (opts.created_after) {
6408
- sql += " AND created_at > ?";
6514
+ sql += " AND t.created_at > ?";
6409
6515
  params.push(opts.created_after);
6410
6516
  }
6411
6517
  if (opts.updated_after) {
6412
- sql += " AND updated_at > ?";
6518
+ sql += " AND t.updated_at > ?";
6413
6519
  params.push(opts.updated_after);
6414
6520
  }
6415
6521
  if (opts.has_dependencies === true) {
6416
- sql += " AND id IN (SELECT task_id FROM task_dependencies)";
6522
+ sql += " AND t.id IN (SELECT task_id FROM task_dependencies)";
6417
6523
  } else if (opts.has_dependencies === false) {
6418
- sql += " AND id NOT IN (SELECT task_id FROM task_dependencies)";
6524
+ sql += " AND t.id NOT IN (SELECT task_id FROM task_dependencies)";
6419
6525
  }
6420
6526
  if (opts.is_blocked === true) {
6421
- 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')";
6527
+ 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')";
6422
6528
  } else if (opts.is_blocked === false) {
6423
- 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')";
6529
+ 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')";
6530
+ }
6531
+ if (hasFts(d) && opts.query.trim()) {
6532
+ sql += ` ORDER BY bm25(tasks_fts),
6533
+ CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
6534
+ t.created_at DESC`;
6535
+ } else {
6536
+ sql += ` ORDER BY
6537
+ CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
6538
+ t.created_at DESC`;
6424
6539
  }
6425
- sql += ` ORDER BY
6426
- CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
6427
- created_at DESC`;
6428
6540
  const rows = d.query(sql).all(...params);
6429
6541
  return rows.map(rowToTask2);
6430
6542
  }
@@ -6974,7 +7086,9 @@ var MINIMAL_TOOLS = new Set([
6974
7086
  "get_task",
6975
7087
  "start_task",
6976
7088
  "add_comment",
6977
- "get_next_task"
7089
+ "get_next_task",
7090
+ "bootstrap",
7091
+ "get_tasks_changed_since"
6978
7092
  ]);
6979
7093
  var STANDARD_EXCLUDED = new Set([
6980
7094
  "get_org_chart",
@@ -7055,15 +7169,17 @@ function formatTask(task) {
7055
7169
  const recur = task.recurrence_rule ? ` [\u21BB]` : "";
7056
7170
  return `${id} ${task.status.padEnd(11)} ${task.priority.padEnd(8)} ${task.title}${assigned}${lock}${recur}`;
7057
7171
  }
7058
- function formatTaskDetail(task) {
7172
+ function formatTaskDetail(task, maxDescriptionChars) {
7059
7173
  const parts = [
7060
7174
  `ID: ${task.id}`,
7061
7175
  `Title: ${task.title}`,
7062
7176
  `Status: ${task.status}`,
7063
7177
  `Priority: ${task.priority}`
7064
7178
  ];
7065
- if (task.description)
7066
- parts.push(`Description: ${task.description}`);
7179
+ if (task.description) {
7180
+ const desc = maxDescriptionChars && task.description.length > maxDescriptionChars ? task.description.slice(0, maxDescriptionChars) + "\u2026" : task.description;
7181
+ parts.push(`Description: ${desc}`);
7182
+ }
7067
7183
  if (task.assigned_to)
7068
7184
  parts.push(`Assigned to: ${task.assigned_to}`);
7069
7185
  if (task.agent_id)
@@ -7107,7 +7223,8 @@ if (shouldRegisterTool("create_task")) {
7107
7223
  metadata: exports_external.record(exports_external.unknown()).optional(),
7108
7224
  estimated_minutes: exports_external.number().optional(),
7109
7225
  requires_approval: exports_external.boolean().optional(),
7110
- recurrence_rule: exports_external.string().optional()
7226
+ recurrence_rule: exports_external.string().optional(),
7227
+ spawns_template_id: exports_external.string().optional().describe("Template ID to auto-create as next task when this task is completed (pipeline/handoff chains)")
7111
7228
  }, async (params) => {
7112
7229
  try {
7113
7230
  const resolved = { ...params };
@@ -7127,7 +7244,7 @@ if (shouldRegisterTool("create_task")) {
7127
7244
  });
7128
7245
  }
7129
7246
  if (shouldRegisterTool("list_tasks")) {
7130
- server.tool("list_tasks", "List tasks with optional filters and pagination.", {
7247
+ server.tool("list_tasks", "List tasks with optional filters and pagination. Default limit is 50 \u2014 use offset to page through results.", {
7131
7248
  project_id: exports_external.string().optional(),
7132
7249
  status: exports_external.union([
7133
7250
  exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]),
@@ -7145,11 +7262,15 @@ if (shouldRegisterTool("list_tasks")) {
7145
7262
  due_today: exports_external.boolean().optional(),
7146
7263
  overdue: exports_external.boolean().optional(),
7147
7264
  limit: exports_external.number().optional(),
7148
- offset: exports_external.number().optional()
7265
+ offset: exports_external.number().optional(),
7266
+ summary_only: exports_external.boolean().optional().describe("When true, return only id, short_id, title, status, priority \u2014 minimal tokens for navigation"),
7267
+ 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.")
7149
7268
  }, async (params) => {
7150
7269
  try {
7151
- const { due_today, overdue, ...rest } = params;
7270
+ const { due_today, overdue, summary_only, ...rest } = params;
7152
7271
  const resolved = { ...rest };
7272
+ if (resolved.limit === undefined)
7273
+ resolved.limit = 50;
7153
7274
  if (resolved.project_id)
7154
7275
  resolved.project_id = resolveId(resolved.project_id, "projects");
7155
7276
  if (resolved.plan_id)
@@ -7171,6 +7292,9 @@ if (shouldRegisterTool("list_tasks")) {
7171
7292
  return { content: [{ type: "text", text: total > 0 ? `No tasks in this page (total: ${total}).` : "No tasks found." }] };
7172
7293
  }
7173
7294
  const text = tasks.map((t) => {
7295
+ if (summary_only) {
7296
+ return `${t.short_id || t.id.slice(0, 8)} [${t.status}] ${t.priority} ${t.title}`;
7297
+ }
7174
7298
  const lock = t.locked_by ? ` [locked by ${t.locked_by}]` : "";
7175
7299
  const assigned = t.assigned_to ? ` -> ${t.assigned_to}` : "";
7176
7300
  const due = t.due_at ? ` due:${t.due_at.slice(0, 10)}` : "";
@@ -7178,10 +7302,23 @@ if (shouldRegisterTool("list_tasks")) {
7178
7302
  return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned}${lock}${due}${recur}`;
7179
7303
  }).join(`
7180
7304
  `);
7181
- const pagination = resolved.limit ? `
7182
- (showing ${tasks.length} of ${total}, offset: ${resolved.offset || 0})` : "";
7305
+ const currentOffset = resolved.offset || 0;
7306
+ const hasMore = total > (resolved.cursor ? tasks.length : currentOffset + tasks.length);
7307
+ let paginationNote = `
7308
+ (showing ${tasks.length} of ${total}`;
7309
+ if (hasMore) {
7310
+ if (resolved.cursor || tasks.length > 0) {
7311
+ const last = tasks[tasks.length - 1];
7312
+ const priorityRank = { critical: 0, high: 1, medium: 2, low: 3 }[last.priority] ?? 3;
7313
+ const cursorPayload = Buffer.from(JSON.stringify({ p: priorityRank, c: last.created_at, i: last.id })).toString("base64");
7314
+ paginationNote += ` \u2014 next_cursor: ${cursorPayload}`;
7315
+ } else {
7316
+ paginationNote += ` \u2014 use offset: ${currentOffset + tasks.length} to get next page`;
7317
+ }
7318
+ }
7319
+ paginationNote += ")";
7183
7320
  return { content: [{ type: "text", text: `${tasks.length} task(s):
7184
- ${text}${pagination}` }] };
7321
+ ${text}${paginationNote}` }] };
7185
7322
  } catch (e) {
7186
7323
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7187
7324
  }
@@ -7189,14 +7326,15 @@ ${text}${pagination}` }] };
7189
7326
  }
7190
7327
  if (shouldRegisterTool("get_task")) {
7191
7328
  server.tool("get_task", "Get full task details with subtasks, deps, and comments.", {
7192
- id: exports_external.string()
7193
- }, async ({ id }) => {
7329
+ id: exports_external.string(),
7330
+ max_description_chars: exports_external.number().optional().describe("Truncate description to this many characters (default: unlimited). Use 300-500 for quick checks.")
7331
+ }, async ({ id, max_description_chars }) => {
7194
7332
  try {
7195
7333
  const resolvedId = resolveId(id);
7196
7334
  const task = getTaskWithRelations(resolvedId);
7197
7335
  if (!task)
7198
7336
  return { content: [{ type: "text", text: `Task not found: ${id}` }], isError: true };
7199
- const parts = [formatTaskDetail(task)];
7337
+ const parts = [formatTaskDetail(task, max_description_chars)];
7200
7338
  if (task.subtasks.length > 0) {
7201
7339
  parts.push(`
7202
7340
  Subtasks (${task.subtasks.length}):`);
@@ -8424,7 +8562,7 @@ if (shouldRegisterTool("get_next_task")) {
8424
8562
  return { content: [{ type: "text", text: "No tasks available \u2014 all pending tasks are blocked, locked, or none exist." }] };
8425
8563
  }
8426
8564
  return { content: [{ type: "text", text: `next: ${formatTask(task)}
8427
- ${formatTaskDetail(task)}` }] };
8565
+ ${formatTaskDetail(task, 300)}` }] };
8428
8566
  } catch (e) {
8429
8567
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8430
8568
  }
@@ -8510,7 +8648,7 @@ if (shouldRegisterTool("claim_next_task")) {
8510
8648
  return { content: [{ type: "text", text: "No tasks available to claim." }] };
8511
8649
  }
8512
8650
  return { content: [{ type: "text", text: `claimed: ${formatTask(task)}
8513
- ${formatTaskDetail(task)}` }] };
8651
+ ${formatTaskDetail(task, 300)}` }] };
8514
8652
  } catch (e) {
8515
8653
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8516
8654
  }
@@ -8595,6 +8733,8 @@ No pending tasks available.`);
8595
8733
  }
8596
8734
  }
8597
8735
  }
8736
+ lines.push(`
8737
+ as_of: ${new Date().toISOString()} (pass to get_tasks_changed_since for incremental polling)`);
8598
8738
  return { content: [{ type: "text", text: lines.join(`
8599
8739
  `) }] };
8600
8740
  } catch (e) {
@@ -8659,6 +8799,29 @@ if (shouldRegisterTool("set_task_priority")) {
8659
8799
  }
8660
8800
  });
8661
8801
  }
8802
+ if (shouldRegisterTool("get_health")) {
8803
+ server.tool("get_health", "Check todos DB health. Returns status and issue summary.", {
8804
+ project_id: exports_external.string().optional()
8805
+ }, async ({ project_id }) => {
8806
+ try {
8807
+ const checks = [];
8808
+ const all = listTasks({});
8809
+ checks.push({ name: "tasks", status: "ok", value: `${all.length} total` });
8810
+ const stale = getStaleTasks(30, project_id ? { project_id: resolveId(project_id, "projects") } : undefined);
8811
+ checks.push({ name: "stale", status: stale.length > 0 ? "warn" : "ok", value: `${stale.length} stuck in_progress >30min` });
8812
+ const nowStr = new Date().toISOString();
8813
+ const overdue = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < nowStr);
8814
+ checks.push({ name: "overdue_recurring", status: overdue.length > 0 ? "warn" : "ok", value: `${overdue.length} overdue` });
8815
+ const status = checks.some((c) => c.status === "error") ? "error" : checks.some((c) => c.status === "warn") ? "warn" : "ok";
8816
+ const text = `Status: ${status}
8817
+ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c.value}`).join(`
8818
+ `)}`;
8819
+ return { content: [{ type: "text", text }] };
8820
+ } catch (e) {
8821
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8822
+ }
8823
+ });
8824
+ }
8662
8825
  if (shouldRegisterTool("get_context")) {
8663
8826
  server.tool("get_context", "Get a compact task summary for agent prompt injection. Returns formatted text.", {
8664
8827
  agent_id: exports_external.string().optional(),
@@ -8683,6 +8846,58 @@ if (shouldRegisterTool("get_context")) {
8683
8846
  }
8684
8847
  if (next)
8685
8848
  lines.push(`Next up: ${next.short_id || next.id.slice(0, 8)} [${next.priority}] ${next.title}`);
8849
+ lines.push(`as_of: ${new Date().toISOString()}`);
8850
+ return { content: [{ type: "text", text: lines.join(`
8851
+ `) }] };
8852
+ } catch (e) {
8853
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8854
+ }
8855
+ });
8856
+ }
8857
+ if (shouldRegisterTool("bootstrap")) {
8858
+ 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.", {
8859
+ agent_id: exports_external.string().optional().describe("Your agent ID \u2014 used to find your active tasks and preferred next task"),
8860
+ project_id: exports_external.string().optional()
8861
+ }, async ({ agent_id, project_id }) => {
8862
+ try {
8863
+ const filters = {};
8864
+ if (project_id)
8865
+ filters.project_id = resolveId(project_id, "projects");
8866
+ const f = Object.keys(filters).length > 0 ? filters : undefined;
8867
+ const status = getStatus(f, agent_id);
8868
+ const next = getNextTask(agent_id, f);
8869
+ const lines = [];
8870
+ const myActive = agent_id ? status.active_work.filter((w) => w.assigned_to === agent_id || w.locked_by === agent_id) : [];
8871
+ if (myActive.length > 0) {
8872
+ lines.push(`## Resuming`);
8873
+ for (const w of myActive) {
8874
+ lines.push(`[${w.short_id || w.id.slice(0, 8)}] ${w.priority} \u2014 ${w.title}`);
8875
+ }
8876
+ lines.push("");
8877
+ }
8878
+ if (next) {
8879
+ lines.push(`## Next task to claim`);
8880
+ lines.push(`[${next.short_id || next.id.slice(0, 8)}] ${next.priority} \u2014 ${next.title}`);
8881
+ if (next.description)
8882
+ lines.push(next.description.slice(0, 300) + (next.description.length > 300 ? "\u2026" : ""));
8883
+ lines.push(` call: claim_next_task(agent_id: "${agent_id || "<your-id>"}")`);
8884
+ lines.push("");
8885
+ } else {
8886
+ lines.push(`## No tasks available to claim`);
8887
+ lines.push("");
8888
+ }
8889
+ lines.push(`## Health`);
8890
+ lines.push(`${status.pending} pending | ${status.in_progress} active | ${status.completed} done`);
8891
+ if (status.stale_count > 0)
8892
+ lines.push(`\u26A0 ${status.stale_count} stale task(s)`);
8893
+ if (status.overdue_recurring > 0)
8894
+ lines.push(`\uD83D\uDD01 ${status.overdue_recurring} overdue recurring`);
8895
+ if (status.active_work.length > 0) {
8896
+ const others = agent_id ? status.active_work.filter((w) => w.assigned_to !== agent_id && w.locked_by !== agent_id) : status.active_work;
8897
+ if (others.length > 0) {
8898
+ lines.push(`Other agents active: ${others.slice(0, 3).map((w) => `${w.short_id || w.id.slice(0, 8)} (${w.assigned_to || "?"})`).join(", ")}`);
8899
+ }
8900
+ }
8686
8901
  return { content: [{ type: "text", text: lines.join(`
8687
8902
  `) }] };
8688
8903
  } catch (e) {
@@ -8752,6 +8967,8 @@ if (shouldRegisterTool("search_tools")) {
8752
8967
  "get_stale_tasks",
8753
8968
  "get_status",
8754
8969
  "get_context",
8970
+ "get_health",
8971
+ "bootstrap",
8755
8972
  "decompose_task",
8756
8973
  "set_task_status",
8757
8974
  "set_task_priority",
@@ -8769,9 +8986,9 @@ if (shouldRegisterTool("describe_tools")) {
8769
8986
  create_task: `Create a new task.
8770
8987
  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)
8771
8988
  Example: {title: 'Daily standup', recurrence_rule: 'every weekday', priority: 'medium'}`,
8772
- list_tasks: `List tasks with optional filters. Supports pagination.
8773
- 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)
8774
- Example: {status: ['pending', 'in_progress'], has_recurrence: true, limit: 20}`,
8989
+ list_tasks: `List tasks with optional filters. Default limit is 50 to avoid context overflow \u2014 always paginate with offset for large lists.
8990
+ 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)
8991
+ Example: {status: ['pending', 'in_progress'], limit: 50, offset: 0}`,
8775
8992
  get_task: `Get full task details with subtasks, deps, and comments.
8776
8993
  Params: id(string, req \u2014 task ID, short_id like 'APP-00001', or partial ID)
8777
8994
  Example: {id: 'a1b2c3d4'}`,
@@ -8921,8 +9138,8 @@ if (shouldRegisterTool("describe_tools")) {
8921
9138
  get_active_work: `See all in-progress tasks and who is working on them.
8922
9139
  Params: project_id(string, optional), task_list_id(string, optional)
8923
9140
  Example: {project_id: 'a1b2c3d4'}`,
8924
- get_tasks_changed_since: `Get tasks modified after a timestamp \u2014 incremental delta sync.
8925
- Params: since(string, req \u2014 ISO date), project_id(string, optional), task_list_id(string, optional)
9141
+ 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.
9142
+ Params: since(string, req \u2014 ISO date from prior as_of), project_id(string, optional), task_list_id(string, optional)
8926
9143
  Example: {since: '2026-03-14T10:00:00Z'}`,
8927
9144
  get_stale_tasks: `Find stale in_progress tasks with no recent activity.
8928
9145
  Params: stale_minutes(number, default:30), project_id(string, optional), task_list_id(string, optional)
@@ -8930,6 +9147,9 @@ if (shouldRegisterTool("describe_tools")) {
8930
9147
  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.
8931
9148
  Params: agent_id(string, optional \u2014 prefers tasks assigned to this agent for next_task), project_id(string, optional), task_list_id(string, optional)
8932
9149
  Example: {agent_id: 'a1b2c3d4', project_id: 'e5f6g7h8'}`,
9150
+ 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.
9151
+ Params: agent_id(string, optional but recommended), project_id(string, optional)
9152
+ Example: {agent_id: 'a1b2c3d4'}`,
8933
9153
  decompose_task: `Break a task into subtasks in one call. Subtasks inherit project/plan/list from parent.
8934
9154
  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)
8935
9155
  Example: {parent_id: 'a1b2c3d4', subtasks: [{title: 'Research'}, {title: 'Implement'}, {title: 'Test'}], depends_on_prev: true}`,