@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/cli/index.js CHANGED
@@ -2599,6 +2599,63 @@ 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);
2654
+ `,
2655
+ `
2656
+ ALTER TABLE agents ADD COLUMN session_id TEXT;
2657
+ ALTER TABLE agents ADD COLUMN working_dir TEXT;
2658
+ INSERT OR IGNORE INTO _migrations (id) VALUES (17);
2602
2659
  `
2603
2660
  ];
2604
2661
  });
@@ -3184,6 +3241,73 @@ var init_webhooks = __esm(() => {
3184
3241
  init_database();
3185
3242
  });
3186
3243
 
3244
+ // src/db/templates.ts
3245
+ var exports_templates = {};
3246
+ __export(exports_templates, {
3247
+ taskFromTemplate: () => taskFromTemplate,
3248
+ listTemplates: () => listTemplates,
3249
+ getTemplate: () => getTemplate,
3250
+ deleteTemplate: () => deleteTemplate,
3251
+ createTemplate: () => createTemplate
3252
+ });
3253
+ function rowToTemplate(row) {
3254
+ return {
3255
+ ...row,
3256
+ tags: JSON.parse(row.tags || "[]"),
3257
+ metadata: JSON.parse(row.metadata || "{}"),
3258
+ priority: row.priority || "medium"
3259
+ };
3260
+ }
3261
+ function createTemplate(input, db) {
3262
+ const d = db || getDatabase();
3263
+ const id = uuid();
3264
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
3265
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3266
+ id,
3267
+ input.name,
3268
+ input.title_pattern,
3269
+ input.description || null,
3270
+ input.priority || "medium",
3271
+ JSON.stringify(input.tags || []),
3272
+ input.project_id || null,
3273
+ input.plan_id || null,
3274
+ JSON.stringify(input.metadata || {}),
3275
+ now()
3276
+ ]);
3277
+ return getTemplate(id, d);
3278
+ }
3279
+ function getTemplate(id, db) {
3280
+ const d = db || getDatabase();
3281
+ const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(id);
3282
+ return row ? rowToTemplate(row) : null;
3283
+ }
3284
+ function listTemplates(db) {
3285
+ const d = db || getDatabase();
3286
+ return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
3287
+ }
3288
+ function deleteTemplate(id, db) {
3289
+ const d = db || getDatabase();
3290
+ return d.run("DELETE FROM task_templates WHERE id = ?", [id]).changes > 0;
3291
+ }
3292
+ function taskFromTemplate(templateId, overrides = {}, db) {
3293
+ const t = getTemplate(templateId, db);
3294
+ if (!t)
3295
+ throw new Error(`Template not found: ${templateId}`);
3296
+ return {
3297
+ title: overrides.title || t.title_pattern,
3298
+ description: overrides.description ?? t.description ?? undefined,
3299
+ priority: overrides.priority ?? t.priority,
3300
+ tags: overrides.tags ?? t.tags,
3301
+ project_id: overrides.project_id ?? t.project_id ?? undefined,
3302
+ plan_id: overrides.plan_id ?? t.plan_id ?? undefined,
3303
+ metadata: overrides.metadata ?? t.metadata,
3304
+ ...overrides
3305
+ };
3306
+ }
3307
+ var init_templates = __esm(() => {
3308
+ init_database();
3309
+ });
3310
+
3187
3311
  // src/db/tasks.ts
3188
3312
  var exports_tasks = {};
3189
3313
  __export(exports_tasks, {
@@ -3250,8 +3374,8 @@ function createTask(input, db) {
3250
3374
  const tags = input.tags || [];
3251
3375
  const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
3252
3376
  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, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3377
+ 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)
3378
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3255
3379
  id,
3256
3380
  shortId,
3257
3381
  input.project_id || null,
@@ -3276,7 +3400,8 @@ function createTask(input, db) {
3276
3400
  null,
3277
3401
  null,
3278
3402
  input.recurrence_rule || null,
3279
- input.recurrence_parent_id || null
3403
+ input.recurrence_parent_id || null,
3404
+ input.spawns_template_id || null
3280
3405
  ]);
3281
3406
  if (tags.length > 0) {
3282
3407
  insertTaskTags(id, tags, d);
@@ -3383,19 +3508,25 @@ function listTasks(filter = {}, db) {
3383
3508
  } else if (filter.has_recurrence === false) {
3384
3509
  conditions.push("recurrence_rule IS NULL");
3385
3510
  }
3511
+ const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
3512
+ if (filter.cursor) {
3513
+ try {
3514
+ const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
3515
+ conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
3516
+ params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
3517
+ } catch {}
3518
+ }
3386
3519
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3387
3520
  let limitClause = "";
3388
3521
  if (filter.limit) {
3389
3522
  limitClause = " LIMIT ?";
3390
3523
  params.push(filter.limit);
3391
- if (filter.offset) {
3524
+ if (!filter.cursor && filter.offset) {
3392
3525
  limitClause += " OFFSET ?";
3393
3526
  params.push(filter.offset);
3394
3527
  }
3395
3528
  }
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);
3529
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
3399
3530
  return rows.map(rowToTask);
3400
3531
  }
3401
3532
  function countTasks(filter = {}, db) {
@@ -3640,10 +3771,25 @@ function completeTask(id, agentId, db, options) {
3640
3771
  if (task.recurrence_rule && !options?.skip_recurrence) {
3641
3772
  spawnedTask = spawnNextRecurrence(task, d);
3642
3773
  }
3774
+ let spawnedFromTemplate = null;
3775
+ if (task.spawns_template_id) {
3776
+ try {
3777
+ const input = taskFromTemplate(task.spawns_template_id, {
3778
+ project_id: task.project_id ?? undefined,
3779
+ plan_id: task.plan_id ?? undefined,
3780
+ task_list_id: task.task_list_id ?? undefined,
3781
+ assigned_to: task.assigned_to ?? undefined
3782
+ }, d);
3783
+ spawnedFromTemplate = createTask(input, d);
3784
+ } catch {}
3785
+ }
3643
3786
  const meta = hasEvidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
3644
3787
  if (spawnedTask) {
3645
3788
  meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
3646
3789
  }
3790
+ if (spawnedFromTemplate) {
3791
+ meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
3792
+ }
3647
3793
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
3648
3794
  }
3649
3795
  function lockTask(id, agentId, db) {
@@ -3855,11 +4001,21 @@ function getNextTask(agentId, filters, db) {
3855
4001
  }
3856
4002
  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
4003
  const where = conditions.join(" AND ");
4004
+ let recentProjectIds = [];
4005
+ if (agentId) {
4006
+ 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);
4007
+ recentProjectIds = recentRows.map((r) => r.project_id);
4008
+ }
3858
4009
  let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
3859
4010
  if (agentId) {
3860
4011
  sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
3861
4012
  params.push(agentId);
3862
4013
  }
4014
+ if (recentProjectIds.length > 0) {
4015
+ const placeholders = recentProjectIds.map(() => "?").join(",");
4016
+ sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
4017
+ params.push(...recentProjectIds);
4018
+ }
3863
4019
  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
4020
  const row = d.query(sql).get(...params);
3865
4021
  return row ? rowToTask(row) : null;
@@ -4195,6 +4351,7 @@ var init_tasks = __esm(() => {
4195
4351
  init_audit();
4196
4352
  init_recurrence();
4197
4353
  init_webhooks();
4354
+ init_templates();
4198
4355
  });
4199
4356
 
4200
4357
  // src/db/agents.ts
@@ -4204,6 +4361,7 @@ __export(exports_agents, {
4204
4361
  updateAgent: () => updateAgent,
4205
4362
  registerAgent: () => registerAgent,
4206
4363
  listAgents: () => listAgents,
4364
+ isAgentConflict: () => isAgentConflict,
4207
4365
  getOrgChart: () => getOrgChart,
4208
4366
  getDirectReports: () => getDirectReports,
4209
4367
  getAgentByName: () => getAgentByName,
@@ -4225,13 +4383,44 @@ function registerAgent(input, db) {
4225
4383
  const normalizedName = input.name.trim().toLowerCase();
4226
4384
  const existing = getAgentByName(normalizedName, d);
4227
4385
  if (existing) {
4228
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), existing.id]);
4386
+ const lastSeenMs = new Date(existing.last_seen_at).getTime();
4387
+ const isActive = Date.now() - lastSeenMs < AGENT_ACTIVE_WINDOW_MS;
4388
+ const sameSession = input.session_id && existing.session_id && input.session_id === existing.session_id;
4389
+ const differentSession = input.session_id && existing.session_id && input.session_id !== existing.session_id;
4390
+ if (isActive && differentSession) {
4391
+ const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
4392
+ return {
4393
+ conflict: true,
4394
+ existing_id: existing.id,
4395
+ existing_name: existing.name,
4396
+ last_seen_at: existing.last_seen_at,
4397
+ session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
4398
+ working_dir: existing.working_dir,
4399
+ 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.`
4400
+ };
4401
+ }
4402
+ const updates = ["last_seen_at = ?"];
4403
+ const params = [now()];
4404
+ if (input.session_id && !sameSession) {
4405
+ updates.push("session_id = ?");
4406
+ params.push(input.session_id);
4407
+ }
4408
+ if (input.working_dir) {
4409
+ updates.push("working_dir = ?");
4410
+ params.push(input.working_dir);
4411
+ }
4412
+ if (input.description) {
4413
+ updates.push("description = ?");
4414
+ params.push(input.description);
4415
+ }
4416
+ params.push(existing.id);
4417
+ d.run(`UPDATE agents SET ${updates.join(", ")} WHERE id = ?`, params);
4229
4418
  return getAgent(existing.id, d);
4230
4419
  }
4231
4420
  const id = shortUuid();
4232
4421
  const timestamp = now();
4233
- d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, reports_to, org_id, metadata, created_at, last_seen_at)
4234
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
4422
+ 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)
4423
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
4235
4424
  id,
4236
4425
  normalizedName,
4237
4426
  input.description || null,
@@ -4243,10 +4432,15 @@ function registerAgent(input, db) {
4243
4432
  input.org_id || null,
4244
4433
  JSON.stringify(input.metadata || {}),
4245
4434
  timestamp,
4246
- timestamp
4435
+ timestamp,
4436
+ input.session_id || null,
4437
+ input.working_dir || null
4247
4438
  ]);
4248
4439
  return getAgent(id, d);
4249
4440
  }
4441
+ function isAgentConflict(result) {
4442
+ return result.conflict === true;
4443
+ }
4250
4444
  function getAgent(id, db) {
4251
4445
  const d = db || getDatabase();
4252
4446
  const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
@@ -4336,8 +4530,10 @@ function getOrgChart(db) {
4336
4530
  }
4337
4531
  return buildTree(null);
4338
4532
  }
4533
+ var AGENT_ACTIVE_WINDOW_MS;
4339
4534
  var init_agents = __esm(() => {
4340
4535
  init_database();
4536
+ AGENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
4341
4537
  });
4342
4538
 
4343
4539
  // src/db/task-lists.ts
@@ -4541,68 +4737,95 @@ function rowToTask2(row) {
4541
4737
  requires_approval: Boolean(row.requires_approval)
4542
4738
  };
4543
4739
  }
4740
+ function hasFts(db) {
4741
+ try {
4742
+ const result = db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='tasks_fts'").get();
4743
+ return result !== null;
4744
+ } catch {
4745
+ return false;
4746
+ }
4747
+ }
4748
+ function escapeFtsQuery(q) {
4749
+ return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
4750
+ }
4544
4751
  function searchTasks(options, projectId, taskListId, db) {
4545
4752
  const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
4546
4753
  const d = db || getDatabase();
4547
4754
  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];
4755
+ const params = [];
4756
+ let sql;
4757
+ if (hasFts(d) && opts.query.trim()) {
4758
+ const ftsQuery = escapeFtsQuery(opts.query);
4759
+ sql = `SELECT t.* FROM tasks t
4760
+ INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
4761
+ WHERE tasks_fts MATCH ?`;
4762
+ params.push(ftsQuery);
4763
+ } else {
4764
+ const pattern = `%${opts.query}%`;
4765
+ 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 ?))`;
4766
+ params.push(pattern, pattern, pattern);
4767
+ }
4551
4768
  if (opts.project_id) {
4552
- sql += " AND project_id = ?";
4769
+ sql += " AND t.project_id = ?";
4553
4770
  params.push(opts.project_id);
4554
4771
  }
4555
4772
  if (opts.task_list_id) {
4556
- sql += " AND task_list_id = ?";
4773
+ sql += " AND t.task_list_id = ?";
4557
4774
  params.push(opts.task_list_id);
4558
4775
  }
4559
4776
  if (opts.status) {
4560
4777
  if (Array.isArray(opts.status)) {
4561
- sql += ` AND status IN (${opts.status.map(() => "?").join(",")})`;
4778
+ sql += ` AND t.status IN (${opts.status.map(() => "?").join(",")})`;
4562
4779
  params.push(...opts.status);
4563
4780
  } else {
4564
- sql += " AND status = ?";
4781
+ sql += " AND t.status = ?";
4565
4782
  params.push(opts.status);
4566
4783
  }
4567
4784
  }
4568
4785
  if (opts.priority) {
4569
4786
  if (Array.isArray(opts.priority)) {
4570
- sql += ` AND priority IN (${opts.priority.map(() => "?").join(",")})`;
4787
+ sql += ` AND t.priority IN (${opts.priority.map(() => "?").join(",")})`;
4571
4788
  params.push(...opts.priority);
4572
4789
  } else {
4573
- sql += " AND priority = ?";
4790
+ sql += " AND t.priority = ?";
4574
4791
  params.push(opts.priority);
4575
4792
  }
4576
4793
  }
4577
4794
  if (opts.assigned_to) {
4578
- sql += " AND assigned_to = ?";
4795
+ sql += " AND t.assigned_to = ?";
4579
4796
  params.push(opts.assigned_to);
4580
4797
  }
4581
4798
  if (opts.agent_id) {
4582
- sql += " AND agent_id = ?";
4799
+ sql += " AND t.agent_id = ?";
4583
4800
  params.push(opts.agent_id);
4584
4801
  }
4585
4802
  if (opts.created_after) {
4586
- sql += " AND created_at > ?";
4803
+ sql += " AND t.created_at > ?";
4587
4804
  params.push(opts.created_after);
4588
4805
  }
4589
4806
  if (opts.updated_after) {
4590
- sql += " AND updated_at > ?";
4807
+ sql += " AND t.updated_at > ?";
4591
4808
  params.push(opts.updated_after);
4592
4809
  }
4593
4810
  if (opts.has_dependencies === true) {
4594
- sql += " AND id IN (SELECT task_id FROM task_dependencies)";
4811
+ sql += " AND t.id IN (SELECT task_id FROM task_dependencies)";
4595
4812
  } else if (opts.has_dependencies === false) {
4596
- sql += " AND id NOT IN (SELECT task_id FROM task_dependencies)";
4813
+ sql += " AND t.id NOT IN (SELECT task_id FROM task_dependencies)";
4597
4814
  }
4598
4815
  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')";
4816
+ 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
4817
  } 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')";
4818
+ 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')";
4819
+ }
4820
+ if (hasFts(d) && opts.query.trim()) {
4821
+ sql += ` ORDER BY bm25(tasks_fts),
4822
+ CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
4823
+ t.created_at DESC`;
4824
+ } else {
4825
+ sql += ` ORDER BY
4826
+ CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
4827
+ t.created_at DESC`;
4602
4828
  }
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
4829
  const rows = d.query(sql).all(...params);
4607
4830
  return rows.map(rowToTask2);
4608
4831
  }
@@ -5142,73 +5365,6 @@ var init_sync = __esm(() => {
5142
5365
  init_config();
5143
5366
  });
5144
5367
 
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
5368
  // node_modules/zod/v3/helpers/util.js
5213
5369
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
5214
5370
  const t = typeof data;
@@ -9256,15 +9412,17 @@ function formatTask(task) {
9256
9412
  const recur = task.recurrence_rule ? ` [\u21BB]` : "";
9257
9413
  return `${id} ${task.status.padEnd(11)} ${task.priority.padEnd(8)} ${task.title}${assigned}${lock}${recur}`;
9258
9414
  }
9259
- function formatTaskDetail(task) {
9415
+ function formatTaskDetail(task, maxDescriptionChars) {
9260
9416
  const parts = [
9261
9417
  `ID: ${task.id}`,
9262
9418
  `Title: ${task.title}`,
9263
9419
  `Status: ${task.status}`,
9264
9420
  `Priority: ${task.priority}`
9265
9421
  ];
9266
- if (task.description)
9267
- parts.push(`Description: ${task.description}`);
9422
+ if (task.description) {
9423
+ const desc = maxDescriptionChars && task.description.length > maxDescriptionChars ? task.description.slice(0, maxDescriptionChars) + "\u2026" : task.description;
9424
+ parts.push(`Description: ${desc}`);
9425
+ }
9268
9426
  if (task.assigned_to)
9269
9427
  parts.push(`Assigned to: ${task.assigned_to}`);
9270
9428
  if (task.agent_id)
@@ -9322,7 +9480,9 @@ var init_mcp = __esm(() => {
9322
9480
  "get_task",
9323
9481
  "start_task",
9324
9482
  "add_comment",
9325
- "get_next_task"
9483
+ "get_next_task",
9484
+ "bootstrap",
9485
+ "get_tasks_changed_since"
9326
9486
  ]);
9327
9487
  STANDARD_EXCLUDED = new Set([
9328
9488
  "get_org_chart",
@@ -9356,7 +9516,8 @@ var init_mcp = __esm(() => {
9356
9516
  metadata: exports_external.record(exports_external.unknown()).optional(),
9357
9517
  estimated_minutes: exports_external.number().optional(),
9358
9518
  requires_approval: exports_external.boolean().optional(),
9359
- recurrence_rule: exports_external.string().optional()
9519
+ recurrence_rule: exports_external.string().optional(),
9520
+ 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
9521
  }, async (params) => {
9361
9522
  try {
9362
9523
  const resolved = { ...params };
@@ -9376,7 +9537,7 @@ var init_mcp = __esm(() => {
9376
9537
  });
9377
9538
  }
9378
9539
  if (shouldRegisterTool("list_tasks")) {
9379
- server.tool("list_tasks", "List tasks with optional filters and pagination.", {
9540
+ server.tool("list_tasks", "List tasks with optional filters and pagination. Default limit is 50 \u2014 use offset to page through results.", {
9380
9541
  project_id: exports_external.string().optional(),
9381
9542
  status: exports_external.union([
9382
9543
  exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]),
@@ -9394,11 +9555,15 @@ var init_mcp = __esm(() => {
9394
9555
  due_today: exports_external.boolean().optional(),
9395
9556
  overdue: exports_external.boolean().optional(),
9396
9557
  limit: exports_external.number().optional(),
9397
- offset: exports_external.number().optional()
9558
+ offset: exports_external.number().optional(),
9559
+ summary_only: exports_external.boolean().optional().describe("When true, return only id, short_id, title, status, priority \u2014 minimal tokens for navigation"),
9560
+ 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
9561
  }, async (params) => {
9399
9562
  try {
9400
- const { due_today, overdue, ...rest } = params;
9563
+ const { due_today, overdue, summary_only, ...rest } = params;
9401
9564
  const resolved = { ...rest };
9565
+ if (resolved.limit === undefined)
9566
+ resolved.limit = 50;
9402
9567
  if (resolved.project_id)
9403
9568
  resolved.project_id = resolveId(resolved.project_id, "projects");
9404
9569
  if (resolved.plan_id)
@@ -9420,6 +9585,9 @@ var init_mcp = __esm(() => {
9420
9585
  return { content: [{ type: "text", text: total > 0 ? `No tasks in this page (total: ${total}).` : "No tasks found." }] };
9421
9586
  }
9422
9587
  const text = tasks.map((t) => {
9588
+ if (summary_only) {
9589
+ return `${t.short_id || t.id.slice(0, 8)} [${t.status}] ${t.priority} ${t.title}`;
9590
+ }
9423
9591
  const lock = t.locked_by ? ` [locked by ${t.locked_by}]` : "";
9424
9592
  const assigned = t.assigned_to ? ` -> ${t.assigned_to}` : "";
9425
9593
  const due = t.due_at ? ` due:${t.due_at.slice(0, 10)}` : "";
@@ -9427,10 +9595,23 @@ var init_mcp = __esm(() => {
9427
9595
  return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned}${lock}${due}${recur}`;
9428
9596
  }).join(`
9429
9597
  `);
9430
- const pagination = resolved.limit ? `
9431
- (showing ${tasks.length} of ${total}, offset: ${resolved.offset || 0})` : "";
9598
+ const currentOffset = resolved.offset || 0;
9599
+ const hasMore = total > (resolved.cursor ? tasks.length : currentOffset + tasks.length);
9600
+ let paginationNote = `
9601
+ (showing ${tasks.length} of ${total}`;
9602
+ if (hasMore) {
9603
+ if (resolved.cursor || tasks.length > 0) {
9604
+ const last = tasks[tasks.length - 1];
9605
+ const priorityRank = { critical: 0, high: 1, medium: 2, low: 3 }[last.priority] ?? 3;
9606
+ const cursorPayload = Buffer.from(JSON.stringify({ p: priorityRank, c: last.created_at, i: last.id })).toString("base64");
9607
+ paginationNote += ` \u2014 next_cursor: ${cursorPayload}`;
9608
+ } else {
9609
+ paginationNote += ` \u2014 use offset: ${currentOffset + tasks.length} to get next page`;
9610
+ }
9611
+ }
9612
+ paginationNote += ")";
9432
9613
  return { content: [{ type: "text", text: `${tasks.length} task(s):
9433
- ${text}${pagination}` }] };
9614
+ ${text}${paginationNote}` }] };
9434
9615
  } catch (e) {
9435
9616
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9436
9617
  }
@@ -9438,14 +9619,15 @@ ${text}${pagination}` }] };
9438
9619
  }
9439
9620
  if (shouldRegisterTool("get_task")) {
9440
9621
  server.tool("get_task", "Get full task details with subtasks, deps, and comments.", {
9441
- id: exports_external.string()
9442
- }, async ({ id }) => {
9622
+ id: exports_external.string(),
9623
+ max_description_chars: exports_external.number().optional().describe("Truncate description to this many characters (default: unlimited). Use 300-500 for quick checks.")
9624
+ }, async ({ id, max_description_chars }) => {
9443
9625
  try {
9444
9626
  const resolvedId = resolveId(id);
9445
9627
  const task = getTaskWithRelations(resolvedId);
9446
9628
  if (!task)
9447
9629
  return { content: [{ type: "text", text: `Task not found: ${id}` }], isError: true };
9448
- const parts = [formatTaskDetail(task)];
9630
+ const parts = [formatTaskDetail(task, max_description_chars)];
9449
9631
  if (task.subtasks.length > 0) {
9450
9632
  parts.push(`
9451
9633
  Subtasks (${task.subtasks.length}):`);
@@ -9919,12 +10101,21 @@ ${text}` }] };
9919
10101
  });
9920
10102
  }
9921
10103
  if (shouldRegisterTool("register_agent")) {
9922
- server.tool("register_agent", "Register an agent (idempotent by name). Updates last_seen_at.", {
10104
+ 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.", {
9923
10105
  name: exports_external.string(),
9924
- description: exports_external.string().optional()
9925
- }, async ({ name, description }) => {
10106
+ description: exports_external.string().optional(),
10107
+ 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."),
10108
+ working_dir: exports_external.string().optional().describe("Working directory of this session \u2014 helps identify who holds the name in a conflict")
10109
+ }, async ({ name, description, session_id, working_dir }) => {
9926
10110
  try {
9927
- const agent = registerAgent({ name, description });
10111
+ const result = registerAgent({ name, description, session_id, working_dir });
10112
+ if (isAgentConflict(result)) {
10113
+ return {
10114
+ content: [{ type: "text", text: `CONFLICT: ${result.message}` }],
10115
+ isError: true
10116
+ };
10117
+ }
10118
+ const agent = result;
9928
10119
  return {
9929
10120
  content: [{
9930
10121
  type: "text",
@@ -9932,6 +10123,7 @@ ${text}` }] };
9932
10123
  ID: ${agent.id}
9933
10124
  Name: ${agent.name}${agent.description ? `
9934
10125
  Description: ${agent.description}` : ""}
10126
+ Session: ${agent.session_id ?? "unbound"}
9935
10127
  Created: ${agent.created_at}
9936
10128
  Last seen: ${agent.last_seen_at}`
9937
10129
  }]
@@ -10673,7 +10865,7 @@ ${lines.join(`
10673
10865
  return { content: [{ type: "text", text: "No tasks available \u2014 all pending tasks are blocked, locked, or none exist." }] };
10674
10866
  }
10675
10867
  return { content: [{ type: "text", text: `next: ${formatTask(task)}
10676
- ${formatTaskDetail(task)}` }] };
10868
+ ${formatTaskDetail(task, 300)}` }] };
10677
10869
  } catch (e) {
10678
10870
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
10679
10871
  }
@@ -10759,7 +10951,7 @@ ${text}` }] };
10759
10951
  return { content: [{ type: "text", text: "No tasks available to claim." }] };
10760
10952
  }
10761
10953
  return { content: [{ type: "text", text: `claimed: ${formatTask(task)}
10762
- ${formatTaskDetail(task)}` }] };
10954
+ ${formatTaskDetail(task, 300)}` }] };
10763
10955
  } catch (e) {
10764
10956
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
10765
10957
  }
@@ -10844,6 +11036,8 @@ No pending tasks available.`);
10844
11036
  }
10845
11037
  }
10846
11038
  }
11039
+ lines.push(`
11040
+ as_of: ${new Date().toISOString()} (pass to get_tasks_changed_since for incremental polling)`);
10847
11041
  return { content: [{ type: "text", text: lines.join(`
10848
11042
  `) }] };
10849
11043
  } catch (e) {
@@ -10955,6 +11149,58 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
10955
11149
  }
10956
11150
  if (next)
10957
11151
  lines.push(`Next up: ${next.short_id || next.id.slice(0, 8)} [${next.priority}] ${next.title}`);
11152
+ lines.push(`as_of: ${new Date().toISOString()}`);
11153
+ return { content: [{ type: "text", text: lines.join(`
11154
+ `) }] };
11155
+ } catch (e) {
11156
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11157
+ }
11158
+ });
11159
+ }
11160
+ if (shouldRegisterTool("bootstrap")) {
11161
+ 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.", {
11162
+ agent_id: exports_external.string().optional().describe("Your agent ID \u2014 used to find your active tasks and preferred next task"),
11163
+ project_id: exports_external.string().optional()
11164
+ }, async ({ agent_id, project_id }) => {
11165
+ try {
11166
+ const filters = {};
11167
+ if (project_id)
11168
+ filters.project_id = resolveId(project_id, "projects");
11169
+ const f = Object.keys(filters).length > 0 ? filters : undefined;
11170
+ const status = getStatus(f, agent_id);
11171
+ const next = getNextTask(agent_id, f);
11172
+ const lines = [];
11173
+ const myActive = agent_id ? status.active_work.filter((w) => w.assigned_to === agent_id || w.locked_by === agent_id) : [];
11174
+ if (myActive.length > 0) {
11175
+ lines.push(`## Resuming`);
11176
+ for (const w of myActive) {
11177
+ lines.push(`[${w.short_id || w.id.slice(0, 8)}] ${w.priority} \u2014 ${w.title}`);
11178
+ }
11179
+ lines.push("");
11180
+ }
11181
+ if (next) {
11182
+ lines.push(`## Next task to claim`);
11183
+ lines.push(`[${next.short_id || next.id.slice(0, 8)}] ${next.priority} \u2014 ${next.title}`);
11184
+ if (next.description)
11185
+ lines.push(next.description.slice(0, 300) + (next.description.length > 300 ? "\u2026" : ""));
11186
+ lines.push(` call: claim_next_task(agent_id: "${agent_id || "<your-id>"}")`);
11187
+ lines.push("");
11188
+ } else {
11189
+ lines.push(`## No tasks available to claim`);
11190
+ lines.push("");
11191
+ }
11192
+ lines.push(`## Health`);
11193
+ lines.push(`${status.pending} pending | ${status.in_progress} active | ${status.completed} done`);
11194
+ if (status.stale_count > 0)
11195
+ lines.push(`\u26A0 ${status.stale_count} stale task(s)`);
11196
+ if (status.overdue_recurring > 0)
11197
+ lines.push(`\uD83D\uDD01 ${status.overdue_recurring} overdue recurring`);
11198
+ if (status.active_work.length > 0) {
11199
+ const others = agent_id ? status.active_work.filter((w) => w.assigned_to !== agent_id && w.locked_by !== agent_id) : status.active_work;
11200
+ if (others.length > 0) {
11201
+ lines.push(`Other agents active: ${others.slice(0, 3).map((w) => `${w.short_id || w.id.slice(0, 8)} (${w.assigned_to || "?"})`).join(", ")}`);
11202
+ }
11203
+ }
10958
11204
  return { content: [{ type: "text", text: lines.join(`
10959
11205
  `) }] };
10960
11206
  } catch (e) {
@@ -11025,6 +11271,7 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
11025
11271
  "get_status",
11026
11272
  "get_context",
11027
11273
  "get_health",
11274
+ "bootstrap",
11028
11275
  "decompose_task",
11029
11276
  "set_task_status",
11030
11277
  "set_task_priority",
@@ -11042,9 +11289,9 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
11042
11289
  create_task: `Create a new task.
11043
11290
  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)
11044
11291
  Example: {title: 'Daily standup', recurrence_rule: 'every weekday', priority: 'medium'}`,
11045
- list_tasks: `List tasks with optional filters. Supports pagination.
11046
- 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)
11047
- Example: {status: ['pending', 'in_progress'], has_recurrence: true, limit: 20}`,
11292
+ list_tasks: `List tasks with optional filters. Default limit is 50 to avoid context overflow \u2014 always paginate with offset for large lists.
11293
+ 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)
11294
+ Example: {status: ['pending', 'in_progress'], limit: 50, offset: 0}`,
11048
11295
  get_task: `Get full task details with subtasks, deps, and comments.
11049
11296
  Params: id(string, req \u2014 task ID, short_id like 'APP-00001', or partial ID)
11050
11297
  Example: {id: 'a1b2c3d4'}`,
@@ -11103,9 +11350,9 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
11103
11350
  delete_plan: `Delete a plan. Tasks in the plan are orphaned, not deleted.
11104
11351
  Params: id(string, req)
11105
11352
  Example: {id: 'a1b2c3d4'}`,
11106
- register_agent: `Register an agent (idempotent by name). Returns existing agent if name matches.
11107
- Params: name(string, req \u2014 e.g. 'maximus'), description(string)
11108
- Example: {name: 'maximus', description: 'Backend developer'}`,
11353
+ 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.
11354
+ Params: name(string, req), description(string), session_id(string \u2014 unique per session, e.g. PID+timestamp), working_dir(string)
11355
+ Example: {name: 'maximus', session_id: 'abc123-1741952000', working_dir: '/workspace/platform'}`,
11109
11356
  list_agents: "List all registered agents with IDs, names, and last seen timestamps. No params.",
11110
11357
  get_agent: `Get agent details by ID or name. Provide one of id or name.
11111
11358
  Params: id(string), name(string)
@@ -11194,8 +11441,8 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
11194
11441
  get_active_work: `See all in-progress tasks and who is working on them.
11195
11442
  Params: project_id(string, optional), task_list_id(string, optional)
11196
11443
  Example: {project_id: 'a1b2c3d4'}`,
11197
- get_tasks_changed_since: `Get tasks modified after a timestamp \u2014 incremental delta sync.
11198
- Params: since(string, req \u2014 ISO date), project_id(string, optional), task_list_id(string, optional)
11444
+ 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.
11445
+ Params: since(string, req \u2014 ISO date from prior as_of), project_id(string, optional), task_list_id(string, optional)
11199
11446
  Example: {since: '2026-03-14T10:00:00Z'}`,
11200
11447
  get_stale_tasks: `Find stale in_progress tasks with no recent activity.
11201
11448
  Params: stale_minutes(number, default:30), project_id(string, optional), task_list_id(string, optional)
@@ -11203,6 +11450,9 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
11203
11450
  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.
11204
11451
  Params: agent_id(string, optional \u2014 prefers tasks assigned to this agent for next_task), project_id(string, optional), task_list_id(string, optional)
11205
11452
  Example: {agent_id: 'a1b2c3d4', project_id: 'e5f6g7h8'}`,
11453
+ 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.
11454
+ Params: agent_id(string, optional but recommended), project_id(string, optional)
11455
+ Example: {agent_id: 'a1b2c3d4'}`,
11206
11456
  decompose_task: `Break a task into subtasks in one call. Subtasks inherit project/plan/list from parent.
11207
11457
  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)
11208
11458
  Example: {parent_id: 'a1b2c3d4', subtasks: [{title: 'Research'}, {title: 'Implement'}, {title: 'Test'}], depends_on_prev: true}`,