@hasna/todos 0.9.69 → 0.9.71

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,63 @@ 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);
588
+ `,
589
+ `
590
+ ALTER TABLE agents ADD COLUMN session_id TEXT;
591
+ ALTER TABLE agents ADD COLUMN working_dir TEXT;
592
+ INSERT OR IGNORE INTO _migrations (id) VALUES (17);
536
593
  `
537
594
  ];
538
595
  });
@@ -615,6 +672,73 @@ var init_webhooks = __esm(() => {
615
672
  init_database();
616
673
  });
617
674
 
675
+ // src/db/templates.ts
676
+ var exports_templates = {};
677
+ __export(exports_templates, {
678
+ taskFromTemplate: () => taskFromTemplate,
679
+ listTemplates: () => listTemplates,
680
+ getTemplate: () => getTemplate,
681
+ deleteTemplate: () => deleteTemplate,
682
+ createTemplate: () => createTemplate
683
+ });
684
+ function rowToTemplate(row) {
685
+ return {
686
+ ...row,
687
+ tags: JSON.parse(row.tags || "[]"),
688
+ metadata: JSON.parse(row.metadata || "{}"),
689
+ priority: row.priority || "medium"
690
+ };
691
+ }
692
+ function createTemplate(input, db) {
693
+ const d = db || getDatabase();
694
+ const id = uuid();
695
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
696
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
697
+ id,
698
+ input.name,
699
+ input.title_pattern,
700
+ input.description || null,
701
+ input.priority || "medium",
702
+ JSON.stringify(input.tags || []),
703
+ input.project_id || null,
704
+ input.plan_id || null,
705
+ JSON.stringify(input.metadata || {}),
706
+ now()
707
+ ]);
708
+ return getTemplate(id, d);
709
+ }
710
+ function getTemplate(id, db) {
711
+ const d = db || getDatabase();
712
+ const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(id);
713
+ return row ? rowToTemplate(row) : null;
714
+ }
715
+ function listTemplates(db) {
716
+ const d = db || getDatabase();
717
+ return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
718
+ }
719
+ function deleteTemplate(id, db) {
720
+ const d = db || getDatabase();
721
+ return d.run("DELETE FROM task_templates WHERE id = ?", [id]).changes > 0;
722
+ }
723
+ function taskFromTemplate(templateId, overrides = {}, db) {
724
+ const t = getTemplate(templateId, db);
725
+ if (!t)
726
+ throw new Error(`Template not found: ${templateId}`);
727
+ return {
728
+ title: overrides.title || t.title_pattern,
729
+ description: overrides.description ?? t.description ?? undefined,
730
+ priority: overrides.priority ?? t.priority,
731
+ tags: overrides.tags ?? t.tags,
732
+ project_id: overrides.project_id ?? t.project_id ?? undefined,
733
+ plan_id: overrides.plan_id ?? t.plan_id ?? undefined,
734
+ metadata: overrides.metadata ?? t.metadata,
735
+ ...overrides
736
+ };
737
+ }
738
+ var init_templates = __esm(() => {
739
+ init_database();
740
+ });
741
+
618
742
  // src/db/agents.ts
619
743
  var exports_agents = {};
620
744
  __export(exports_agents, {
@@ -622,6 +746,7 @@ __export(exports_agents, {
622
746
  updateAgent: () => updateAgent,
623
747
  registerAgent: () => registerAgent,
624
748
  listAgents: () => listAgents,
749
+ isAgentConflict: () => isAgentConflict,
625
750
  getOrgChart: () => getOrgChart,
626
751
  getDirectReports: () => getDirectReports,
627
752
  getAgentByName: () => getAgentByName,
@@ -643,13 +768,44 @@ function registerAgent(input, db) {
643
768
  const normalizedName = input.name.trim().toLowerCase();
644
769
  const existing = getAgentByName(normalizedName, d);
645
770
  if (existing) {
646
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), existing.id]);
771
+ const lastSeenMs = new Date(existing.last_seen_at).getTime();
772
+ const isActive = Date.now() - lastSeenMs < AGENT_ACTIVE_WINDOW_MS;
773
+ const sameSession = input.session_id && existing.session_id && input.session_id === existing.session_id;
774
+ const differentSession = input.session_id && existing.session_id && input.session_id !== existing.session_id;
775
+ if (isActive && differentSession) {
776
+ const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
777
+ return {
778
+ conflict: true,
779
+ existing_id: existing.id,
780
+ existing_name: existing.name,
781
+ last_seen_at: existing.last_seen_at,
782
+ session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
783
+ working_dir: existing.working_dir,
784
+ message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id?.slice(0, 8)}\u2026, dir: ${existing.working_dir ?? "unknown"}). Are you that agent? If so, pass session_id="${existing.session_id}" to reclaim it. Otherwise choose a different name.`
785
+ };
786
+ }
787
+ const updates = ["last_seen_at = ?"];
788
+ const params = [now()];
789
+ if (input.session_id && !sameSession) {
790
+ updates.push("session_id = ?");
791
+ params.push(input.session_id);
792
+ }
793
+ if (input.working_dir) {
794
+ updates.push("working_dir = ?");
795
+ params.push(input.working_dir);
796
+ }
797
+ if (input.description) {
798
+ updates.push("description = ?");
799
+ params.push(input.description);
800
+ }
801
+ params.push(existing.id);
802
+ d.run(`UPDATE agents SET ${updates.join(", ")} WHERE id = ?`, params);
647
803
  return getAgent(existing.id, d);
648
804
  }
649
805
  const id = shortUuid();
650
806
  const timestamp = now();
651
- d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, reports_to, org_id, metadata, created_at, last_seen_at)
652
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
807
+ d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir)
808
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
653
809
  id,
654
810
  normalizedName,
655
811
  input.description || null,
@@ -661,10 +817,15 @@ function registerAgent(input, db) {
661
817
  input.org_id || null,
662
818
  JSON.stringify(input.metadata || {}),
663
819
  timestamp,
664
- timestamp
820
+ timestamp,
821
+ input.session_id || null,
822
+ input.working_dir || null
665
823
  ]);
666
824
  return getAgent(id, d);
667
825
  }
826
+ function isAgentConflict(result) {
827
+ return result.conflict === true;
828
+ }
668
829
  function getAgent(id, db) {
669
830
  const d = db || getDatabase();
670
831
  const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
@@ -754,75 +915,10 @@ function getOrgChart(db) {
754
915
  }
755
916
  return buildTree(null);
756
917
  }
918
+ var AGENT_ACTIVE_WINDOW_MS;
757
919
  var init_agents = __esm(() => {
758
920
  init_database();
759
- });
760
-
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();
921
+ AGENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
826
922
  });
827
923
 
828
924
  // src/mcp/index.ts
@@ -5222,6 +5318,7 @@ function nextOccurrence(rule, from) {
5222
5318
 
5223
5319
  // src/db/tasks.ts
5224
5320
  init_webhooks();
5321
+ init_templates();
5225
5322
  function rowToTask(row) {
5226
5323
  return {
5227
5324
  ...row,
@@ -5252,8 +5349,8 @@ function createTask(input, db) {
5252
5349
  const tags = input.tags || [];
5253
5350
  const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
5254
5351
  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, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5352
+ 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)
5353
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5257
5354
  id,
5258
5355
  shortId,
5259
5356
  input.project_id || null,
@@ -5278,7 +5375,8 @@ function createTask(input, db) {
5278
5375
  null,
5279
5376
  null,
5280
5377
  input.recurrence_rule || null,
5281
- input.recurrence_parent_id || null
5378
+ input.recurrence_parent_id || null,
5379
+ input.spawns_template_id || null
5282
5380
  ]);
5283
5381
  if (tags.length > 0) {
5284
5382
  insertTaskTags(id, tags, d);
@@ -5385,19 +5483,25 @@ function listTasks(filter = {}, db) {
5385
5483
  } else if (filter.has_recurrence === false) {
5386
5484
  conditions.push("recurrence_rule IS NULL");
5387
5485
  }
5486
+ const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
5487
+ if (filter.cursor) {
5488
+ try {
5489
+ const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
5490
+ conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
5491
+ params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
5492
+ } catch {}
5493
+ }
5388
5494
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5389
5495
  let limitClause = "";
5390
5496
  if (filter.limit) {
5391
5497
  limitClause = " LIMIT ?";
5392
5498
  params.push(filter.limit);
5393
- if (filter.offset) {
5499
+ if (!filter.cursor && filter.offset) {
5394
5500
  limitClause += " OFFSET ?";
5395
5501
  params.push(filter.offset);
5396
5502
  }
5397
5503
  }
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);
5504
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
5401
5505
  return rows.map(rowToTask);
5402
5506
  }
5403
5507
  function countTasks(filter = {}, db) {
@@ -5642,10 +5746,25 @@ function completeTask(id, agentId, db, options) {
5642
5746
  if (task.recurrence_rule && !options?.skip_recurrence) {
5643
5747
  spawnedTask = spawnNextRecurrence(task, d);
5644
5748
  }
5749
+ let spawnedFromTemplate = null;
5750
+ if (task.spawns_template_id) {
5751
+ try {
5752
+ const input = taskFromTemplate(task.spawns_template_id, {
5753
+ project_id: task.project_id ?? undefined,
5754
+ plan_id: task.plan_id ?? undefined,
5755
+ task_list_id: task.task_list_id ?? undefined,
5756
+ assigned_to: task.assigned_to ?? undefined
5757
+ }, d);
5758
+ spawnedFromTemplate = createTask(input, d);
5759
+ } catch {}
5760
+ }
5645
5761
  const meta = hasEvidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
5646
5762
  if (spawnedTask) {
5647
5763
  meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
5648
5764
  }
5765
+ if (spawnedFromTemplate) {
5766
+ meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
5767
+ }
5649
5768
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
5650
5769
  }
5651
5770
  function lockTask(id, agentId, db) {
@@ -5853,11 +5972,21 @@ function getNextTask(agentId, filters, db) {
5853
5972
  }
5854
5973
  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
5974
  const where = conditions.join(" AND ");
5975
+ let recentProjectIds = [];
5976
+ if (agentId) {
5977
+ 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);
5978
+ recentProjectIds = recentRows.map((r) => r.project_id);
5979
+ }
5856
5980
  let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
5857
5981
  if (agentId) {
5858
5982
  sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
5859
5983
  params.push(agentId);
5860
5984
  }
5985
+ if (recentProjectIds.length > 0) {
5986
+ const placeholders = recentProjectIds.map(() => "?").join(",");
5987
+ sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
5988
+ params.push(...recentProjectIds);
5989
+ }
5861
5990
  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
5991
  const row = d.query(sql).get(...params);
5863
5992
  return row ? rowToTask(row) : null;
@@ -6363,68 +6492,95 @@ function rowToTask2(row) {
6363
6492
  requires_approval: Boolean(row.requires_approval)
6364
6493
  };
6365
6494
  }
6495
+ function hasFts(db) {
6496
+ try {
6497
+ const result = db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='tasks_fts'").get();
6498
+ return result !== null;
6499
+ } catch {
6500
+ return false;
6501
+ }
6502
+ }
6503
+ function escapeFtsQuery(q) {
6504
+ return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
6505
+ }
6366
6506
  function searchTasks(options, projectId, taskListId, db) {
6367
6507
  const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
6368
6508
  const d = db || getDatabase();
6369
6509
  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];
6510
+ const params = [];
6511
+ let sql;
6512
+ if (hasFts(d) && opts.query.trim()) {
6513
+ const ftsQuery = escapeFtsQuery(opts.query);
6514
+ sql = `SELECT t.* FROM tasks t
6515
+ INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
6516
+ WHERE tasks_fts MATCH ?`;
6517
+ params.push(ftsQuery);
6518
+ } else {
6519
+ const pattern = `%${opts.query}%`;
6520
+ 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 ?))`;
6521
+ params.push(pattern, pattern, pattern);
6522
+ }
6373
6523
  if (opts.project_id) {
6374
- sql += " AND project_id = ?";
6524
+ sql += " AND t.project_id = ?";
6375
6525
  params.push(opts.project_id);
6376
6526
  }
6377
6527
  if (opts.task_list_id) {
6378
- sql += " AND task_list_id = ?";
6528
+ sql += " AND t.task_list_id = ?";
6379
6529
  params.push(opts.task_list_id);
6380
6530
  }
6381
6531
  if (opts.status) {
6382
6532
  if (Array.isArray(opts.status)) {
6383
- sql += ` AND status IN (${opts.status.map(() => "?").join(",")})`;
6533
+ sql += ` AND t.status IN (${opts.status.map(() => "?").join(",")})`;
6384
6534
  params.push(...opts.status);
6385
6535
  } else {
6386
- sql += " AND status = ?";
6536
+ sql += " AND t.status = ?";
6387
6537
  params.push(opts.status);
6388
6538
  }
6389
6539
  }
6390
6540
  if (opts.priority) {
6391
6541
  if (Array.isArray(opts.priority)) {
6392
- sql += ` AND priority IN (${opts.priority.map(() => "?").join(",")})`;
6542
+ sql += ` AND t.priority IN (${opts.priority.map(() => "?").join(",")})`;
6393
6543
  params.push(...opts.priority);
6394
6544
  } else {
6395
- sql += " AND priority = ?";
6545
+ sql += " AND t.priority = ?";
6396
6546
  params.push(opts.priority);
6397
6547
  }
6398
6548
  }
6399
6549
  if (opts.assigned_to) {
6400
- sql += " AND assigned_to = ?";
6550
+ sql += " AND t.assigned_to = ?";
6401
6551
  params.push(opts.assigned_to);
6402
6552
  }
6403
6553
  if (opts.agent_id) {
6404
- sql += " AND agent_id = ?";
6554
+ sql += " AND t.agent_id = ?";
6405
6555
  params.push(opts.agent_id);
6406
6556
  }
6407
6557
  if (opts.created_after) {
6408
- sql += " AND created_at > ?";
6558
+ sql += " AND t.created_at > ?";
6409
6559
  params.push(opts.created_after);
6410
6560
  }
6411
6561
  if (opts.updated_after) {
6412
- sql += " AND updated_at > ?";
6562
+ sql += " AND t.updated_at > ?";
6413
6563
  params.push(opts.updated_after);
6414
6564
  }
6415
6565
  if (opts.has_dependencies === true) {
6416
- sql += " AND id IN (SELECT task_id FROM task_dependencies)";
6566
+ sql += " AND t.id IN (SELECT task_id FROM task_dependencies)";
6417
6567
  } else if (opts.has_dependencies === false) {
6418
- sql += " AND id NOT IN (SELECT task_id FROM task_dependencies)";
6568
+ sql += " AND t.id NOT IN (SELECT task_id FROM task_dependencies)";
6419
6569
  }
6420
6570
  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')";
6571
+ 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
6572
  } 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')";
6573
+ 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')";
6574
+ }
6575
+ if (hasFts(d) && opts.query.trim()) {
6576
+ sql += ` ORDER BY bm25(tasks_fts),
6577
+ CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
6578
+ t.created_at DESC`;
6579
+ } else {
6580
+ sql += ` ORDER BY
6581
+ CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
6582
+ t.created_at DESC`;
6424
6583
  }
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
6584
  const rows = d.query(sql).all(...params);
6429
6585
  return rows.map(rowToTask2);
6430
6586
  }
@@ -6974,7 +7130,9 @@ var MINIMAL_TOOLS = new Set([
6974
7130
  "get_task",
6975
7131
  "start_task",
6976
7132
  "add_comment",
6977
- "get_next_task"
7133
+ "get_next_task",
7134
+ "bootstrap",
7135
+ "get_tasks_changed_since"
6978
7136
  ]);
6979
7137
  var STANDARD_EXCLUDED = new Set([
6980
7138
  "get_org_chart",
@@ -7055,15 +7213,17 @@ function formatTask(task) {
7055
7213
  const recur = task.recurrence_rule ? ` [\u21BB]` : "";
7056
7214
  return `${id} ${task.status.padEnd(11)} ${task.priority.padEnd(8)} ${task.title}${assigned}${lock}${recur}`;
7057
7215
  }
7058
- function formatTaskDetail(task) {
7216
+ function formatTaskDetail(task, maxDescriptionChars) {
7059
7217
  const parts = [
7060
7218
  `ID: ${task.id}`,
7061
7219
  `Title: ${task.title}`,
7062
7220
  `Status: ${task.status}`,
7063
7221
  `Priority: ${task.priority}`
7064
7222
  ];
7065
- if (task.description)
7066
- parts.push(`Description: ${task.description}`);
7223
+ if (task.description) {
7224
+ const desc = maxDescriptionChars && task.description.length > maxDescriptionChars ? task.description.slice(0, maxDescriptionChars) + "\u2026" : task.description;
7225
+ parts.push(`Description: ${desc}`);
7226
+ }
7067
7227
  if (task.assigned_to)
7068
7228
  parts.push(`Assigned to: ${task.assigned_to}`);
7069
7229
  if (task.agent_id)
@@ -7107,7 +7267,8 @@ if (shouldRegisterTool("create_task")) {
7107
7267
  metadata: exports_external.record(exports_external.unknown()).optional(),
7108
7268
  estimated_minutes: exports_external.number().optional(),
7109
7269
  requires_approval: exports_external.boolean().optional(),
7110
- recurrence_rule: exports_external.string().optional()
7270
+ recurrence_rule: exports_external.string().optional(),
7271
+ 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
7272
  }, async (params) => {
7112
7273
  try {
7113
7274
  const resolved = { ...params };
@@ -7127,7 +7288,7 @@ if (shouldRegisterTool("create_task")) {
7127
7288
  });
7128
7289
  }
7129
7290
  if (shouldRegisterTool("list_tasks")) {
7130
- server.tool("list_tasks", "List tasks with optional filters and pagination.", {
7291
+ server.tool("list_tasks", "List tasks with optional filters and pagination. Default limit is 50 \u2014 use offset to page through results.", {
7131
7292
  project_id: exports_external.string().optional(),
7132
7293
  status: exports_external.union([
7133
7294
  exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]),
@@ -7145,11 +7306,15 @@ if (shouldRegisterTool("list_tasks")) {
7145
7306
  due_today: exports_external.boolean().optional(),
7146
7307
  overdue: exports_external.boolean().optional(),
7147
7308
  limit: exports_external.number().optional(),
7148
- offset: exports_external.number().optional()
7309
+ offset: exports_external.number().optional(),
7310
+ summary_only: exports_external.boolean().optional().describe("When true, return only id, short_id, title, status, priority \u2014 minimal tokens for navigation"),
7311
+ 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
7312
  }, async (params) => {
7150
7313
  try {
7151
- const { due_today, overdue, ...rest } = params;
7314
+ const { due_today, overdue, summary_only, ...rest } = params;
7152
7315
  const resolved = { ...rest };
7316
+ if (resolved.limit === undefined)
7317
+ resolved.limit = 50;
7153
7318
  if (resolved.project_id)
7154
7319
  resolved.project_id = resolveId(resolved.project_id, "projects");
7155
7320
  if (resolved.plan_id)
@@ -7171,6 +7336,9 @@ if (shouldRegisterTool("list_tasks")) {
7171
7336
  return { content: [{ type: "text", text: total > 0 ? `No tasks in this page (total: ${total}).` : "No tasks found." }] };
7172
7337
  }
7173
7338
  const text = tasks.map((t) => {
7339
+ if (summary_only) {
7340
+ return `${t.short_id || t.id.slice(0, 8)} [${t.status}] ${t.priority} ${t.title}`;
7341
+ }
7174
7342
  const lock = t.locked_by ? ` [locked by ${t.locked_by}]` : "";
7175
7343
  const assigned = t.assigned_to ? ` -> ${t.assigned_to}` : "";
7176
7344
  const due = t.due_at ? ` due:${t.due_at.slice(0, 10)}` : "";
@@ -7178,10 +7346,23 @@ if (shouldRegisterTool("list_tasks")) {
7178
7346
  return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned}${lock}${due}${recur}`;
7179
7347
  }).join(`
7180
7348
  `);
7181
- const pagination = resolved.limit ? `
7182
- (showing ${tasks.length} of ${total}, offset: ${resolved.offset || 0})` : "";
7349
+ const currentOffset = resolved.offset || 0;
7350
+ const hasMore = total > (resolved.cursor ? tasks.length : currentOffset + tasks.length);
7351
+ let paginationNote = `
7352
+ (showing ${tasks.length} of ${total}`;
7353
+ if (hasMore) {
7354
+ if (resolved.cursor || tasks.length > 0) {
7355
+ const last = tasks[tasks.length - 1];
7356
+ const priorityRank = { critical: 0, high: 1, medium: 2, low: 3 }[last.priority] ?? 3;
7357
+ const cursorPayload = Buffer.from(JSON.stringify({ p: priorityRank, c: last.created_at, i: last.id })).toString("base64");
7358
+ paginationNote += ` \u2014 next_cursor: ${cursorPayload}`;
7359
+ } else {
7360
+ paginationNote += ` \u2014 use offset: ${currentOffset + tasks.length} to get next page`;
7361
+ }
7362
+ }
7363
+ paginationNote += ")";
7183
7364
  return { content: [{ type: "text", text: `${tasks.length} task(s):
7184
- ${text}${pagination}` }] };
7365
+ ${text}${paginationNote}` }] };
7185
7366
  } catch (e) {
7186
7367
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7187
7368
  }
@@ -7189,14 +7370,15 @@ ${text}${pagination}` }] };
7189
7370
  }
7190
7371
  if (shouldRegisterTool("get_task")) {
7191
7372
  server.tool("get_task", "Get full task details with subtasks, deps, and comments.", {
7192
- id: exports_external.string()
7193
- }, async ({ id }) => {
7373
+ id: exports_external.string(),
7374
+ max_description_chars: exports_external.number().optional().describe("Truncate description to this many characters (default: unlimited). Use 300-500 for quick checks.")
7375
+ }, async ({ id, max_description_chars }) => {
7194
7376
  try {
7195
7377
  const resolvedId = resolveId(id);
7196
7378
  const task = getTaskWithRelations(resolvedId);
7197
7379
  if (!task)
7198
7380
  return { content: [{ type: "text", text: `Task not found: ${id}` }], isError: true };
7199
- const parts = [formatTaskDetail(task)];
7381
+ const parts = [formatTaskDetail(task, max_description_chars)];
7200
7382
  if (task.subtasks.length > 0) {
7201
7383
  parts.push(`
7202
7384
  Subtasks (${task.subtasks.length}):`);
@@ -7670,12 +7852,21 @@ if (shouldRegisterTool("sync")) {
7670
7852
  });
7671
7853
  }
7672
7854
  if (shouldRegisterTool("register_agent")) {
7673
- server.tool("register_agent", "Register an agent (idempotent by name). Updates last_seen_at.", {
7855
+ server.tool("register_agent", "Register an agent. Pass session_id (unique per coding session) to prevent name conflicts. Returns conflict error if name is taken by an active agent in a different session.", {
7674
7856
  name: exports_external.string(),
7675
- description: exports_external.string().optional()
7676
- }, async ({ name, description }) => {
7857
+ description: exports_external.string().optional(),
7858
+ session_id: exports_external.string().optional().describe("Unique ID for this coding session (e.g. process PID + timestamp, or env var). Used to detect name collisions across sessions. Store it and pass on every register_agent call."),
7859
+ working_dir: exports_external.string().optional().describe("Working directory of this session \u2014 helps identify who holds the name in a conflict")
7860
+ }, async ({ name, description, session_id, working_dir }) => {
7677
7861
  try {
7678
- const agent = registerAgent({ name, description });
7862
+ const result = registerAgent({ name, description, session_id, working_dir });
7863
+ if (isAgentConflict(result)) {
7864
+ return {
7865
+ content: [{ type: "text", text: `CONFLICT: ${result.message}` }],
7866
+ isError: true
7867
+ };
7868
+ }
7869
+ const agent = result;
7679
7870
  return {
7680
7871
  content: [{
7681
7872
  type: "text",
@@ -7683,6 +7874,7 @@ if (shouldRegisterTool("register_agent")) {
7683
7874
  ID: ${agent.id}
7684
7875
  Name: ${agent.name}${agent.description ? `
7685
7876
  Description: ${agent.description}` : ""}
7877
+ Session: ${agent.session_id ?? "unbound"}
7686
7878
  Created: ${agent.created_at}
7687
7879
  Last seen: ${agent.last_seen_at}`
7688
7880
  }]
@@ -8424,7 +8616,7 @@ if (shouldRegisterTool("get_next_task")) {
8424
8616
  return { content: [{ type: "text", text: "No tasks available \u2014 all pending tasks are blocked, locked, or none exist." }] };
8425
8617
  }
8426
8618
  return { content: [{ type: "text", text: `next: ${formatTask(task)}
8427
- ${formatTaskDetail(task)}` }] };
8619
+ ${formatTaskDetail(task, 300)}` }] };
8428
8620
  } catch (e) {
8429
8621
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8430
8622
  }
@@ -8510,7 +8702,7 @@ if (shouldRegisterTool("claim_next_task")) {
8510
8702
  return { content: [{ type: "text", text: "No tasks available to claim." }] };
8511
8703
  }
8512
8704
  return { content: [{ type: "text", text: `claimed: ${formatTask(task)}
8513
- ${formatTaskDetail(task)}` }] };
8705
+ ${formatTaskDetail(task, 300)}` }] };
8514
8706
  } catch (e) {
8515
8707
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8516
8708
  }
@@ -8595,6 +8787,8 @@ No pending tasks available.`);
8595
8787
  }
8596
8788
  }
8597
8789
  }
8790
+ lines.push(`
8791
+ as_of: ${new Date().toISOString()} (pass to get_tasks_changed_since for incremental polling)`);
8598
8792
  return { content: [{ type: "text", text: lines.join(`
8599
8793
  `) }] };
8600
8794
  } catch (e) {
@@ -8706,6 +8900,58 @@ if (shouldRegisterTool("get_context")) {
8706
8900
  }
8707
8901
  if (next)
8708
8902
  lines.push(`Next up: ${next.short_id || next.id.slice(0, 8)} [${next.priority}] ${next.title}`);
8903
+ lines.push(`as_of: ${new Date().toISOString()}`);
8904
+ return { content: [{ type: "text", text: lines.join(`
8905
+ `) }] };
8906
+ } catch (e) {
8907
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8908
+ }
8909
+ });
8910
+ }
8911
+ if (shouldRegisterTool("bootstrap")) {
8912
+ 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.", {
8913
+ agent_id: exports_external.string().optional().describe("Your agent ID \u2014 used to find your active tasks and preferred next task"),
8914
+ project_id: exports_external.string().optional()
8915
+ }, async ({ agent_id, project_id }) => {
8916
+ try {
8917
+ const filters = {};
8918
+ if (project_id)
8919
+ filters.project_id = resolveId(project_id, "projects");
8920
+ const f = Object.keys(filters).length > 0 ? filters : undefined;
8921
+ const status = getStatus(f, agent_id);
8922
+ const next = getNextTask(agent_id, f);
8923
+ const lines = [];
8924
+ const myActive = agent_id ? status.active_work.filter((w) => w.assigned_to === agent_id || w.locked_by === agent_id) : [];
8925
+ if (myActive.length > 0) {
8926
+ lines.push(`## Resuming`);
8927
+ for (const w of myActive) {
8928
+ lines.push(`[${w.short_id || w.id.slice(0, 8)}] ${w.priority} \u2014 ${w.title}`);
8929
+ }
8930
+ lines.push("");
8931
+ }
8932
+ if (next) {
8933
+ lines.push(`## Next task to claim`);
8934
+ lines.push(`[${next.short_id || next.id.slice(0, 8)}] ${next.priority} \u2014 ${next.title}`);
8935
+ if (next.description)
8936
+ lines.push(next.description.slice(0, 300) + (next.description.length > 300 ? "\u2026" : ""));
8937
+ lines.push(` call: claim_next_task(agent_id: "${agent_id || "<your-id>"}")`);
8938
+ lines.push("");
8939
+ } else {
8940
+ lines.push(`## No tasks available to claim`);
8941
+ lines.push("");
8942
+ }
8943
+ lines.push(`## Health`);
8944
+ lines.push(`${status.pending} pending | ${status.in_progress} active | ${status.completed} done`);
8945
+ if (status.stale_count > 0)
8946
+ lines.push(`\u26A0 ${status.stale_count} stale task(s)`);
8947
+ if (status.overdue_recurring > 0)
8948
+ lines.push(`\uD83D\uDD01 ${status.overdue_recurring} overdue recurring`);
8949
+ if (status.active_work.length > 0) {
8950
+ const others = agent_id ? status.active_work.filter((w) => w.assigned_to !== agent_id && w.locked_by !== agent_id) : status.active_work;
8951
+ if (others.length > 0) {
8952
+ lines.push(`Other agents active: ${others.slice(0, 3).map((w) => `${w.short_id || w.id.slice(0, 8)} (${w.assigned_to || "?"})`).join(", ")}`);
8953
+ }
8954
+ }
8709
8955
  return { content: [{ type: "text", text: lines.join(`
8710
8956
  `) }] };
8711
8957
  } catch (e) {
@@ -8776,6 +9022,7 @@ if (shouldRegisterTool("search_tools")) {
8776
9022
  "get_status",
8777
9023
  "get_context",
8778
9024
  "get_health",
9025
+ "bootstrap",
8779
9026
  "decompose_task",
8780
9027
  "set_task_status",
8781
9028
  "set_task_priority",
@@ -8793,9 +9040,9 @@ if (shouldRegisterTool("describe_tools")) {
8793
9040
  create_task: `Create a new task.
8794
9041
  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)
8795
9042
  Example: {title: 'Daily standup', recurrence_rule: 'every weekday', priority: 'medium'}`,
8796
- list_tasks: `List tasks with optional filters. Supports pagination.
8797
- 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)
8798
- Example: {status: ['pending', 'in_progress'], has_recurrence: true, limit: 20}`,
9043
+ list_tasks: `List tasks with optional filters. Default limit is 50 to avoid context overflow \u2014 always paginate with offset for large lists.
9044
+ 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)
9045
+ Example: {status: ['pending', 'in_progress'], limit: 50, offset: 0}`,
8799
9046
  get_task: `Get full task details with subtasks, deps, and comments.
8800
9047
  Params: id(string, req \u2014 task ID, short_id like 'APP-00001', or partial ID)
8801
9048
  Example: {id: 'a1b2c3d4'}`,
@@ -8854,9 +9101,9 @@ if (shouldRegisterTool("describe_tools")) {
8854
9101
  delete_plan: `Delete a plan. Tasks in the plan are orphaned, not deleted.
8855
9102
  Params: id(string, req)
8856
9103
  Example: {id: 'a1b2c3d4'}`,
8857
- register_agent: `Register an agent (idempotent by name). Returns existing agent if name matches.
8858
- Params: name(string, req \u2014 e.g. 'maximus'), description(string)
8859
- Example: {name: 'maximus', description: 'Backend developer'}`,
9104
+ register_agent: `Register an agent. ALWAYS pass session_id (unique per session) to prevent name conflicts. Returns CONFLICT error if name is active in another session \u2014 pick a different name or reclaim with matching session_id.
9105
+ Params: name(string, req), description(string), session_id(string \u2014 unique per session, e.g. PID+timestamp), working_dir(string)
9106
+ Example: {name: 'maximus', session_id: 'abc123-1741952000', working_dir: '/workspace/platform'}`,
8860
9107
  list_agents: "List all registered agents with IDs, names, and last seen timestamps. No params.",
8861
9108
  get_agent: `Get agent details by ID or name. Provide one of id or name.
8862
9109
  Params: id(string), name(string)
@@ -8945,8 +9192,8 @@ if (shouldRegisterTool("describe_tools")) {
8945
9192
  get_active_work: `See all in-progress tasks and who is working on them.
8946
9193
  Params: project_id(string, optional), task_list_id(string, optional)
8947
9194
  Example: {project_id: 'a1b2c3d4'}`,
8948
- get_tasks_changed_since: `Get tasks modified after a timestamp \u2014 incremental delta sync.
8949
- Params: since(string, req \u2014 ISO date), project_id(string, optional), task_list_id(string, optional)
9195
+ 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.
9196
+ Params: since(string, req \u2014 ISO date from prior as_of), project_id(string, optional), task_list_id(string, optional)
8950
9197
  Example: {since: '2026-03-14T10:00:00Z'}`,
8951
9198
  get_stale_tasks: `Find stale in_progress tasks with no recent activity.
8952
9199
  Params: stale_minutes(number, default:30), project_id(string, optional), task_list_id(string, optional)
@@ -8954,6 +9201,9 @@ if (shouldRegisterTool("describe_tools")) {
8954
9201
  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.
8955
9202
  Params: agent_id(string, optional \u2014 prefers tasks assigned to this agent for next_task), project_id(string, optional), task_list_id(string, optional)
8956
9203
  Example: {agent_id: 'a1b2c3d4', project_id: 'e5f6g7h8'}`,
9204
+ 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.
9205
+ Params: agent_id(string, optional but recommended), project_id(string, optional)
9206
+ Example: {agent_id: 'a1b2c3d4'}`,
8957
9207
  decompose_task: `Break a task into subtasks in one call. Subtasks inherit project/plan/list from parent.
8958
9208
  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)
8959
9209
  Example: {parent_id: 'a1b2c3d4', subtasks: [{title: 'Research'}, {title: 'Implement'}, {title: 'Test'}], depends_on_prev: true}`,