@hasna/todos 0.9.34 → 0.9.37

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
@@ -192,6 +192,8 @@ function ensureSchema(db) {
192
192
  ensureColumn("tasks", "requires_approval", "INTEGER NOT NULL DEFAULT 0");
193
193
  ensureColumn("tasks", "approved_by", "TEXT");
194
194
  ensureColumn("tasks", "approved_at", "TEXT");
195
+ ensureColumn("tasks", "recurrence_rule", "TEXT");
196
+ ensureColumn("tasks", "recurrence_parent_id", "TEXT REFERENCES tasks(id) ON DELETE SET NULL");
195
197
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
196
198
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
197
199
  ensureColumn("agents", "reports_to", "TEXT");
@@ -216,6 +218,8 @@ function ensureSchema(db) {
216
218
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
217
219
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
218
220
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(agent_id)");
221
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id)");
222
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL");
219
223
  }
220
224
  function backfillTaskTags(db) {
221
225
  try {
@@ -515,6 +519,13 @@ var init_database = __esm(() => {
515
519
  ALTER TABLE agents ADD COLUMN org_id TEXT REFERENCES orgs(id) ON DELETE SET NULL;
516
520
  ALTER TABLE projects ADD COLUMN org_id TEXT REFERENCES orgs(id) ON DELETE SET NULL;
517
521
  INSERT OR IGNORE INTO _migrations (id) VALUES (12);
522
+ `,
523
+ `
524
+ ALTER TABLE tasks ADD COLUMN recurrence_rule TEXT;
525
+ ALTER TABLE tasks ADD COLUMN recurrence_parent_id TEXT REFERENCES tasks(id) ON DELETE SET NULL;
526
+ CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id);
527
+ CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL;
528
+ INSERT OR IGNORE INTO _migrations (id) VALUES (13);
518
529
  `
519
530
  ];
520
531
  });
@@ -546,6 +557,57 @@ var init_audit = __esm(() => {
546
557
  init_database();
547
558
  });
548
559
 
560
+ // src/db/webhooks.ts
561
+ var exports_webhooks = {};
562
+ __export(exports_webhooks, {
563
+ listWebhooks: () => listWebhooks,
564
+ getWebhook: () => getWebhook,
565
+ dispatchWebhook: () => dispatchWebhook,
566
+ deleteWebhook: () => deleteWebhook,
567
+ createWebhook: () => createWebhook
568
+ });
569
+ function rowToWebhook(row) {
570
+ return { ...row, events: JSON.parse(row.events || "[]"), active: !!row.active };
571
+ }
572
+ function createWebhook(input, db) {
573
+ const d = db || getDatabase();
574
+ const id = uuid();
575
+ d.run(`INSERT INTO webhooks (id, url, events, secret, created_at) VALUES (?, ?, ?, ?, ?)`, [id, input.url, JSON.stringify(input.events || []), input.secret || null, now()]);
576
+ return getWebhook(id, d);
577
+ }
578
+ function getWebhook(id, db) {
579
+ const d = db || getDatabase();
580
+ const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
581
+ return row ? rowToWebhook(row) : null;
582
+ }
583
+ function listWebhooks(db) {
584
+ const d = db || getDatabase();
585
+ return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
586
+ }
587
+ function deleteWebhook(id, db) {
588
+ const d = db || getDatabase();
589
+ return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
590
+ }
591
+ async function dispatchWebhook(event, payload, db) {
592
+ const webhooks = listWebhooks(db).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
593
+ for (const wh of webhooks) {
594
+ try {
595
+ const body = JSON.stringify({ event, payload, timestamp: now() });
596
+ const headers = { "Content-Type": "application/json" };
597
+ if (wh.secret) {
598
+ const encoder = new TextEncoder;
599
+ const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
600
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
601
+ headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
602
+ }
603
+ fetch(wh.url, { method: "POST", headers, body }).catch(() => {});
604
+ } catch {}
605
+ }
606
+ }
607
+ var init_webhooks = __esm(() => {
608
+ init_database();
609
+ });
610
+
549
611
  // src/db/agents.ts
550
612
  var exports_agents = {};
551
613
  __export(exports_agents, {
@@ -689,57 +751,6 @@ var init_agents = __esm(() => {
689
751
  init_database();
690
752
  });
691
753
 
692
- // src/db/webhooks.ts
693
- var exports_webhooks = {};
694
- __export(exports_webhooks, {
695
- listWebhooks: () => listWebhooks,
696
- getWebhook: () => getWebhook,
697
- dispatchWebhook: () => dispatchWebhook,
698
- deleteWebhook: () => deleteWebhook,
699
- createWebhook: () => createWebhook
700
- });
701
- function rowToWebhook(row) {
702
- return { ...row, events: JSON.parse(row.events || "[]"), active: !!row.active };
703
- }
704
- function createWebhook(input, db) {
705
- const d = db || getDatabase();
706
- const id = uuid();
707
- d.run(`INSERT INTO webhooks (id, url, events, secret, created_at) VALUES (?, ?, ?, ?, ?)`, [id, input.url, JSON.stringify(input.events || []), input.secret || null, now()]);
708
- return getWebhook(id, d);
709
- }
710
- function getWebhook(id, db) {
711
- const d = db || getDatabase();
712
- const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
713
- return row ? rowToWebhook(row) : null;
714
- }
715
- function listWebhooks(db) {
716
- const d = db || getDatabase();
717
- return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
718
- }
719
- function deleteWebhook(id, db) {
720
- const d = db || getDatabase();
721
- return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
722
- }
723
- async function dispatchWebhook(event, payload, db) {
724
- const webhooks = listWebhooks(db).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
725
- for (const wh of webhooks) {
726
- try {
727
- const body = JSON.stringify({ event, payload, timestamp: now() });
728
- const headers = { "Content-Type": "application/json" };
729
- if (wh.secret) {
730
- const encoder = new TextEncoder;
731
- const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
732
- const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
733
- headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
734
- }
735
- fetch(wh.url, { method: "POST", headers, body }).catch(() => {});
736
- } catch {}
737
- }
738
- }
739
- var init_webhooks = __esm(() => {
740
- init_database();
741
- });
742
-
743
754
  // src/db/templates.ts
744
755
  var exports_templates = {};
745
756
  __export(exports_templates, {
@@ -4789,6 +4800,8 @@ class VersionConflictError extends Error {
4789
4800
  taskId;
4790
4801
  expectedVersion;
4791
4802
  actualVersion;
4803
+ static code = "VERSION_CONFLICT";
4804
+ static suggestion = "Fetch the task with get_task to get the current version before updating.";
4792
4805
  constructor(taskId, expectedVersion, actualVersion) {
4793
4806
  super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
4794
4807
  this.taskId = taskId;
@@ -4800,14 +4813,30 @@ class VersionConflictError extends Error {
4800
4813
 
4801
4814
  class TaskNotFoundError extends Error {
4802
4815
  taskId;
4816
+ static code = "TASK_NOT_FOUND";
4817
+ static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
4803
4818
  constructor(taskId) {
4804
4819
  super(`Task not found: ${taskId}`);
4805
4820
  this.taskId = taskId;
4806
4821
  this.name = "TaskNotFoundError";
4807
4822
  }
4808
4823
  }
4824
+
4825
+ class ProjectNotFoundError extends Error {
4826
+ projectId;
4827
+ static code = "PROJECT_NOT_FOUND";
4828
+ static suggestion = "Use list_projects to see available projects.";
4829
+ constructor(projectId) {
4830
+ super(`Project not found: ${projectId}`);
4831
+ this.projectId = projectId;
4832
+ this.name = "ProjectNotFoundError";
4833
+ }
4834
+ }
4835
+
4809
4836
  class PlanNotFoundError extends Error {
4810
4837
  planId;
4838
+ static code = "PLAN_NOT_FOUND";
4839
+ static suggestion = "Use list_plans to see available plans.";
4811
4840
  constructor(planId) {
4812
4841
  super(`Plan not found: ${planId}`);
4813
4842
  this.planId = planId;
@@ -4818,6 +4847,8 @@ class PlanNotFoundError extends Error {
4818
4847
  class LockError extends Error {
4819
4848
  taskId;
4820
4849
  lockedBy;
4850
+ static code = "LOCK_ERROR";
4851
+ static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
4821
4852
  constructor(taskId, lockedBy) {
4822
4853
  super(`Task ${taskId} is locked by ${lockedBy}`);
4823
4854
  this.taskId = taskId;
@@ -4825,8 +4856,22 @@ class LockError extends Error {
4825
4856
  this.name = "LockError";
4826
4857
  }
4827
4858
  }
4859
+
4860
+ class AgentNotFoundError extends Error {
4861
+ agentId;
4862
+ static code = "AGENT_NOT_FOUND";
4863
+ static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
4864
+ constructor(agentId) {
4865
+ super(`Agent not found: ${agentId}`);
4866
+ this.agentId = agentId;
4867
+ this.name = "AgentNotFoundError";
4868
+ }
4869
+ }
4870
+
4828
4871
  class TaskListNotFoundError extends Error {
4829
4872
  taskListId;
4873
+ static code = "TASK_LIST_NOT_FOUND";
4874
+ static suggestion = "Use list_task_lists to see available lists.";
4830
4875
  constructor(taskListId) {
4831
4876
  super(`Task list not found: ${taskListId}`);
4832
4877
  this.taskListId = taskListId;
@@ -4837,6 +4882,8 @@ class TaskListNotFoundError extends Error {
4837
4882
  class DependencyCycleError extends Error {
4838
4883
  taskId;
4839
4884
  dependsOn;
4885
+ static code = "DEPENDENCY_CYCLE";
4886
+ static suggestion = "Check the dependency chain with get_task to avoid circular references.";
4840
4887
  constructor(taskId, dependsOn) {
4841
4888
  super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
4842
4889
  this.taskId = taskId;
@@ -4848,6 +4895,8 @@ class DependencyCycleError extends Error {
4848
4895
  class CompletionGuardError extends Error {
4849
4896
  reason;
4850
4897
  retryAfterSeconds;
4898
+ static code = "COMPLETION_BLOCKED";
4899
+ static suggestion = "Wait for the cooldown period, then retry.";
4851
4900
  constructor(reason, retryAfterSeconds) {
4852
4901
  super(reason);
4853
4902
  this.reason = reason;
@@ -5074,6 +5123,96 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
5074
5123
 
5075
5124
  // src/db/tasks.ts
5076
5125
  init_audit();
5126
+
5127
+ // src/lib/recurrence.ts
5128
+ var DAY_NAMES = {
5129
+ sunday: 0,
5130
+ sun: 0,
5131
+ monday: 1,
5132
+ mon: 1,
5133
+ tuesday: 2,
5134
+ tue: 2,
5135
+ wednesday: 3,
5136
+ wed: 3,
5137
+ thursday: 4,
5138
+ thu: 4,
5139
+ friday: 5,
5140
+ fri: 5,
5141
+ saturday: 6,
5142
+ sat: 6
5143
+ };
5144
+ function parseRecurrenceRule(rule) {
5145
+ const normalized = rule.trim().toLowerCase();
5146
+ if (normalized === "every weekday" || normalized === "every weekdays") {
5147
+ return { type: "specific_days", days: [1, 2, 3, 4, 5] };
5148
+ }
5149
+ if (normalized === "every day" || normalized === "daily") {
5150
+ return { type: "interval", interval: 1, unit: "day" };
5151
+ }
5152
+ if (normalized === "every week" || normalized === "weekly") {
5153
+ return { type: "interval", interval: 1, unit: "week" };
5154
+ }
5155
+ if (normalized === "every month" || normalized === "monthly") {
5156
+ return { type: "interval", interval: 1, unit: "month" };
5157
+ }
5158
+ const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
5159
+ if (intervalMatch) {
5160
+ return {
5161
+ type: "interval",
5162
+ interval: parseInt(intervalMatch[1], 10),
5163
+ unit: intervalMatch[2]
5164
+ };
5165
+ }
5166
+ const daysMatch = normalized.match(/^every\s+(.+)$/);
5167
+ if (daysMatch) {
5168
+ const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
5169
+ const days = [];
5170
+ for (const part of dayParts) {
5171
+ const dayNum = DAY_NAMES[part];
5172
+ if (dayNum !== undefined) {
5173
+ days.push(dayNum);
5174
+ }
5175
+ }
5176
+ if (days.length > 0) {
5177
+ return { type: "specific_days", days: days.sort((a, b) => a - b) };
5178
+ }
5179
+ }
5180
+ throw new Error(`Invalid recurrence rule: "${rule}". Supported formats: "every day", "every weekday", "every week", "every 2 weeks", "every month", "every N days/weeks/months", "every monday", "every mon,wed,fri"`);
5181
+ }
5182
+ function nextOccurrence(rule, from) {
5183
+ const parsed = parseRecurrenceRule(rule);
5184
+ const base = from || new Date;
5185
+ if (parsed.type === "interval") {
5186
+ const next = new Date(base);
5187
+ if (parsed.unit === "day") {
5188
+ next.setDate(next.getDate() + parsed.interval);
5189
+ } else if (parsed.unit === "week") {
5190
+ next.setDate(next.getDate() + parsed.interval * 7);
5191
+ } else if (parsed.unit === "month") {
5192
+ next.setMonth(next.getMonth() + parsed.interval);
5193
+ }
5194
+ return next.toISOString();
5195
+ }
5196
+ if (parsed.type === "specific_days") {
5197
+ const currentDay = base.getDay();
5198
+ const days = parsed.days;
5199
+ let daysToAdd = Infinity;
5200
+ for (const day of days) {
5201
+ let diff = day - currentDay;
5202
+ if (diff <= 0)
5203
+ diff += 7;
5204
+ if (diff < daysToAdd)
5205
+ daysToAdd = diff;
5206
+ }
5207
+ const next = new Date(base);
5208
+ next.setDate(next.getDate() + daysToAdd);
5209
+ return next.toISOString();
5210
+ }
5211
+ throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
5212
+ }
5213
+
5214
+ // src/db/tasks.ts
5215
+ init_webhooks();
5077
5216
  function rowToTask(row) {
5078
5217
  return {
5079
5218
  ...row,
@@ -5104,8 +5243,8 @@ function createTask(input, db) {
5104
5243
  const tags = input.tags || [];
5105
5244
  const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
5106
5245
  const title = shortId ? `${shortId}: ${input.title}` : input.title;
5107
- 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)
5108
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)`, [
5246
+ 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)
5247
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5109
5248
  id,
5110
5249
  shortId,
5111
5250
  input.project_id || null,
@@ -5128,12 +5267,16 @@ function createTask(input, db) {
5128
5267
  input.estimated_minutes || null,
5129
5268
  input.requires_approval ? 1 : 0,
5130
5269
  null,
5131
- null
5270
+ null,
5271
+ input.recurrence_rule || null,
5272
+ input.recurrence_parent_id || null
5132
5273
  ]);
5133
5274
  if (tags.length > 0) {
5134
5275
  insertTaskTags(id, tags, d);
5135
5276
  }
5136
- return getTask(id, d);
5277
+ const task = getTask(id, d);
5278
+ dispatchWebhook("task.created", { id: task.id, short_id: task.short_id, title: task.title, status: task.status, priority: task.priority, project_id: task.project_id, assigned_to: task.assigned_to }, d).catch(() => {});
5279
+ return task;
5137
5280
  }
5138
5281
  function getTask(id, db) {
5139
5282
  const d = db || getDatabase();
@@ -5228,6 +5371,11 @@ function listTasks(filter = {}, db) {
5228
5371
  conditions.push("task_list_id = ?");
5229
5372
  params.push(filter.task_list_id);
5230
5373
  }
5374
+ if (filter.has_recurrence === true) {
5375
+ conditions.push("recurrence_rule IS NOT NULL");
5376
+ } else if (filter.has_recurrence === false) {
5377
+ conditions.push("recurrence_rule IS NULL");
5378
+ }
5231
5379
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5232
5380
  let limitClause = "";
5233
5381
  if (filter.limit) {
@@ -5243,6 +5391,69 @@ function listTasks(filter = {}, db) {
5243
5391
  created_at DESC${limitClause}`).all(...params);
5244
5392
  return rows.map(rowToTask);
5245
5393
  }
5394
+ function countTasks(filter = {}, db) {
5395
+ const d = db || getDatabase();
5396
+ const conditions = [];
5397
+ const params = [];
5398
+ if (filter.project_id) {
5399
+ conditions.push("project_id = ?");
5400
+ params.push(filter.project_id);
5401
+ }
5402
+ if (filter.parent_id !== undefined) {
5403
+ if (filter.parent_id === null) {
5404
+ conditions.push("parent_id IS NULL");
5405
+ } else {
5406
+ conditions.push("parent_id = ?");
5407
+ params.push(filter.parent_id);
5408
+ }
5409
+ }
5410
+ if (filter.status) {
5411
+ if (Array.isArray(filter.status)) {
5412
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
5413
+ params.push(...filter.status);
5414
+ } else {
5415
+ conditions.push("status = ?");
5416
+ params.push(filter.status);
5417
+ }
5418
+ }
5419
+ if (filter.priority) {
5420
+ if (Array.isArray(filter.priority)) {
5421
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
5422
+ params.push(...filter.priority);
5423
+ } else {
5424
+ conditions.push("priority = ?");
5425
+ params.push(filter.priority);
5426
+ }
5427
+ }
5428
+ if (filter.assigned_to) {
5429
+ conditions.push("assigned_to = ?");
5430
+ params.push(filter.assigned_to);
5431
+ }
5432
+ if (filter.agent_id) {
5433
+ conditions.push("agent_id = ?");
5434
+ params.push(filter.agent_id);
5435
+ }
5436
+ if (filter.session_id) {
5437
+ conditions.push("session_id = ?");
5438
+ params.push(filter.session_id);
5439
+ }
5440
+ if (filter.tags && filter.tags.length > 0) {
5441
+ const placeholders = filter.tags.map(() => "?").join(",");
5442
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
5443
+ params.push(...filter.tags);
5444
+ }
5445
+ if (filter.plan_id) {
5446
+ conditions.push("plan_id = ?");
5447
+ params.push(filter.plan_id);
5448
+ }
5449
+ if (filter.task_list_id) {
5450
+ conditions.push("task_list_id = ?");
5451
+ params.push(filter.task_list_id);
5452
+ }
5453
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5454
+ const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
5455
+ return row.count;
5456
+ }
5246
5457
  function updateTask(id, input, db) {
5247
5458
  const d = db || getDatabase();
5248
5459
  const task = getTask(id, d);
@@ -5314,6 +5525,10 @@ function updateTask(id, input, db) {
5314
5525
  sets.push("approved_at = ?");
5315
5526
  params.push(now());
5316
5527
  }
5528
+ if (input.recurrence_rule !== undefined) {
5529
+ sets.push("recurrence_rule = ?");
5530
+ params.push(input.recurrence_rule);
5531
+ }
5317
5532
  params.push(id, input.version);
5318
5533
  const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
5319
5534
  if (result.changes === 0) {
@@ -5334,6 +5549,12 @@ function updateTask(id, input, db) {
5334
5549
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
5335
5550
  if (input.approved_by !== undefined)
5336
5551
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
5552
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
5553
+ dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
5554
+ }
5555
+ if (input.status !== undefined && input.status !== task.status) {
5556
+ dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
5557
+ }
5337
5558
  return {
5338
5559
  ...task,
5339
5560
  ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
@@ -5385,9 +5606,10 @@ function startTask(id, agentId, db) {
5385
5606
  }
5386
5607
  }
5387
5608
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
5609
+ dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
5388
5610
  return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
5389
5611
  }
5390
- function completeTask(id, agentId, db, evidence) {
5612
+ function completeTask(id, agentId, db, options) {
5391
5613
  const d = db || getDatabase();
5392
5614
  const task = getTask(id, d);
5393
5615
  if (!task)
@@ -5396,7 +5618,9 @@ function completeTask(id, agentId, db, evidence) {
5396
5618
  throw new LockError(id, task.locked_by);
5397
5619
  }
5398
5620
  checkCompletionGuard(task, agentId || null, d);
5399
- if (evidence) {
5621
+ const evidence = options ? { files_changed: options.files_changed, test_results: options.test_results, commit_hash: options.commit_hash, notes: options.notes } : undefined;
5622
+ const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes);
5623
+ if (hasEvidence) {
5400
5624
  const meta2 = { ...task.metadata, _evidence: evidence };
5401
5625
  d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
5402
5626
  }
@@ -5404,7 +5628,15 @@ function completeTask(id, agentId, db, evidence) {
5404
5628
  d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
5405
5629
  WHERE id = ?`, [timestamp, timestamp, id]);
5406
5630
  logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
5407
- const meta = evidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
5631
+ dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
5632
+ let spawnedTask = null;
5633
+ if (task.recurrence_rule && !options?.skip_recurrence) {
5634
+ spawnedTask = spawnNextRecurrence(task, d);
5635
+ }
5636
+ const meta = hasEvidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
5637
+ if (spawnedTask) {
5638
+ meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
5639
+ }
5408
5640
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
5409
5641
  }
5410
5642
  function lockTask(id, agentId, db) {
@@ -5467,51 +5699,428 @@ function getTaskDependencies(taskId, db) {
5467
5699
  const d = db || getDatabase();
5468
5700
  return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
5469
5701
  }
5470
- function wouldCreateCycle(taskId, dependsOn, db) {
5471
- const visited = new Set;
5472
- const queue = [dependsOn];
5473
- while (queue.length > 0) {
5474
- const current = queue.shift();
5475
- if (current === taskId)
5476
- return true;
5477
- if (visited.has(current))
5478
- continue;
5479
- visited.add(current);
5480
- const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
5481
- for (const dep of deps) {
5482
- queue.push(dep.depends_on);
5483
- }
5484
- }
5485
- return false;
5702
+ function cloneTask(taskId, overrides, db) {
5703
+ const d = db || getDatabase();
5704
+ const source = getTask(taskId, d);
5705
+ if (!source)
5706
+ throw new TaskNotFoundError(taskId);
5707
+ const input = {
5708
+ title: overrides?.title ?? source.title,
5709
+ description: overrides?.description ?? source.description ?? undefined,
5710
+ priority: overrides?.priority ?? source.priority,
5711
+ project_id: overrides?.project_id ?? source.project_id ?? undefined,
5712
+ parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
5713
+ plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
5714
+ task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
5715
+ status: overrides?.status ?? "pending",
5716
+ agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
5717
+ assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
5718
+ tags: overrides?.tags ?? source.tags,
5719
+ metadata: overrides?.metadata ?? source.metadata,
5720
+ estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
5721
+ recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
5722
+ };
5723
+ return createTask(input, d);
5486
5724
  }
5487
-
5488
- // src/db/comments.ts
5489
- init_database();
5490
- function addComment(input, db) {
5725
+ function getTaskGraph(taskId, direction = "both", db) {
5491
5726
  const d = db || getDatabase();
5492
- if (!getTask(input.task_id, d)) {
5493
- throw new TaskNotFoundError(input.task_id);
5727
+ const task = getTask(taskId, d);
5728
+ if (!task)
5729
+ throw new TaskNotFoundError(taskId);
5730
+ function toNode(t) {
5731
+ const deps = getTaskDependencies(t.id, d);
5732
+ const hasUnfinishedDeps = deps.some((dep) => {
5733
+ const depTask = getTask(dep.depends_on, d);
5734
+ return depTask && depTask.status !== "completed";
5735
+ });
5736
+ return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
5737
+ }
5738
+ function buildUp(id, visited) {
5739
+ if (visited.has(id))
5740
+ return [];
5741
+ visited.add(id);
5742
+ const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
5743
+ return deps.map((dep) => {
5744
+ const depTask = getTask(dep.depends_on, d);
5745
+ if (!depTask)
5746
+ return null;
5747
+ return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
5748
+ }).filter(Boolean);
5749
+ }
5750
+ function buildDown(id, visited) {
5751
+ if (visited.has(id))
5752
+ return [];
5753
+ visited.add(id);
5754
+ const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
5755
+ return dependents.map((dep) => {
5756
+ const depTask = getTask(dep.task_id, d);
5757
+ if (!depTask)
5758
+ return null;
5759
+ return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
5760
+ }).filter(Boolean);
5494
5761
  }
5495
- const id = uuid();
5496
- const timestamp = now();
5497
- d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, created_at)
5498
- VALUES (?, ?, ?, ?, ?, ?)`, [
5499
- id,
5500
- input.task_id,
5501
- input.agent_id || null,
5502
- input.session_id || null,
5503
- input.content,
5504
- timestamp
5505
- ]);
5506
- return getComment(id, d);
5762
+ const rootNode = toNode(task);
5763
+ const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
5764
+ const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
5765
+ return { task: rootNode, depends_on, blocks };
5507
5766
  }
5508
- function getComment(id, db) {
5767
+ function moveTask(taskId, target, db) {
5509
5768
  const d = db || getDatabase();
5510
- return d.query("SELECT * FROM task_comments WHERE id = ?").get(id);
5769
+ const task = getTask(taskId, d);
5770
+ if (!task)
5771
+ throw new TaskNotFoundError(taskId);
5772
+ const sets = ["updated_at = ?", "version = version + 1"];
5773
+ const params = [now()];
5774
+ if (target.task_list_id !== undefined) {
5775
+ sets.push("task_list_id = ?");
5776
+ params.push(target.task_list_id);
5777
+ }
5778
+ if (target.project_id !== undefined) {
5779
+ sets.push("project_id = ?");
5780
+ params.push(target.project_id);
5781
+ }
5782
+ if (target.plan_id !== undefined) {
5783
+ sets.push("plan_id = ?");
5784
+ params.push(target.plan_id);
5785
+ }
5786
+ params.push(taskId);
5787
+ d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
5788
+ return getTask(taskId, d);
5511
5789
  }
5512
-
5513
- // src/db/plans.ts
5514
- init_database();
5790
+ function spawnNextRecurrence(completedTask, db) {
5791
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
5792
+ let title = completedTask.title;
5793
+ if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
5794
+ title = title.slice(completedTask.short_id.length + 2);
5795
+ }
5796
+ const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
5797
+ return createTask({
5798
+ title,
5799
+ description: completedTask.description ?? undefined,
5800
+ priority: completedTask.priority,
5801
+ project_id: completedTask.project_id ?? undefined,
5802
+ task_list_id: completedTask.task_list_id ?? undefined,
5803
+ plan_id: completedTask.plan_id ?? undefined,
5804
+ assigned_to: completedTask.assigned_to ?? undefined,
5805
+ tags: completedTask.tags,
5806
+ metadata: completedTask.metadata,
5807
+ estimated_minutes: completedTask.estimated_minutes ?? undefined,
5808
+ recurrence_rule: completedTask.recurrence_rule,
5809
+ recurrence_parent_id: recurrenceParentId,
5810
+ due_at: dueAt
5811
+ }, db);
5812
+ }
5813
+ function claimNextTask(agentId, filters, db) {
5814
+ const d = db || getDatabase();
5815
+ const tx = d.transaction(() => {
5816
+ const task = getNextTask(agentId, filters, d);
5817
+ if (!task)
5818
+ return null;
5819
+ return startTask(task.id, agentId, d);
5820
+ });
5821
+ return tx();
5822
+ }
5823
+ function getNextTask(agentId, filters, db) {
5824
+ const d = db || getDatabase();
5825
+ clearExpiredLocks(d);
5826
+ const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
5827
+ const params = [lockExpiryCutoff()];
5828
+ if (filters?.project_id) {
5829
+ conditions.push("project_id = ?");
5830
+ params.push(filters.project_id);
5831
+ }
5832
+ if (filters?.task_list_id) {
5833
+ conditions.push("task_list_id = ?");
5834
+ params.push(filters.task_list_id);
5835
+ }
5836
+ if (filters?.plan_id) {
5837
+ conditions.push("plan_id = ?");
5838
+ params.push(filters.plan_id);
5839
+ }
5840
+ if (filters?.tags && filters.tags.length > 0) {
5841
+ const placeholders = filters.tags.map(() => "?").join(",");
5842
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
5843
+ params.push(...filters.tags);
5844
+ }
5845
+ 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')");
5846
+ const where = conditions.join(" AND ");
5847
+ let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
5848
+ if (agentId) {
5849
+ sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
5850
+ params.push(agentId);
5851
+ }
5852
+ 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`;
5853
+ const row = d.query(sql).get(...params);
5854
+ return row ? rowToTask(row) : null;
5855
+ }
5856
+ function getActiveWork(filters, db) {
5857
+ const d = db || getDatabase();
5858
+ clearExpiredLocks(d);
5859
+ const conditions = ["status = 'in_progress'"];
5860
+ const params = [];
5861
+ if (filters?.project_id) {
5862
+ conditions.push("project_id = ?");
5863
+ params.push(filters.project_id);
5864
+ }
5865
+ if (filters?.task_list_id) {
5866
+ conditions.push("task_list_id = ?");
5867
+ params.push(filters.task_list_id);
5868
+ }
5869
+ const where = conditions.join(" AND ");
5870
+ const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
5871
+ CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
5872
+ updated_at DESC`).all(...params);
5873
+ return rows;
5874
+ }
5875
+ function getTasksChangedSince(since, filters, db) {
5876
+ const d = db || getDatabase();
5877
+ const conditions = ["updated_at > ?"];
5878
+ const params = [since];
5879
+ if (filters?.project_id) {
5880
+ conditions.push("project_id = ?");
5881
+ params.push(filters.project_id);
5882
+ }
5883
+ if (filters?.task_list_id) {
5884
+ conditions.push("task_list_id = ?");
5885
+ params.push(filters.task_list_id);
5886
+ }
5887
+ const where = conditions.join(" AND ");
5888
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
5889
+ return rows.map(rowToTask);
5890
+ }
5891
+ function failTask(id, agentId, reason, options, db) {
5892
+ const d = db || getDatabase();
5893
+ const task = getTask(id, d);
5894
+ if (!task)
5895
+ throw new TaskNotFoundError(id);
5896
+ const meta = {
5897
+ ...task.metadata,
5898
+ _failure: {
5899
+ reason: reason || "Unknown failure",
5900
+ error_code: options?.error_code || null,
5901
+ failed_by: agentId || null,
5902
+ failed_at: now(),
5903
+ retry_requested: options?.retry || false
5904
+ }
5905
+ };
5906
+ const timestamp = now();
5907
+ d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
5908
+ WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
5909
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
5910
+ dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
5911
+ const failedTask = {
5912
+ ...task,
5913
+ status: "failed",
5914
+ locked_by: null,
5915
+ locked_at: null,
5916
+ metadata: meta,
5917
+ version: task.version + 1,
5918
+ updated_at: timestamp
5919
+ };
5920
+ let retryTask;
5921
+ if (options?.retry) {
5922
+ let title = task.title;
5923
+ if (task.short_id && title.startsWith(task.short_id + ": ")) {
5924
+ title = title.slice(task.short_id.length + 2);
5925
+ }
5926
+ retryTask = createTask({
5927
+ title,
5928
+ description: task.description ?? undefined,
5929
+ priority: task.priority,
5930
+ project_id: task.project_id ?? undefined,
5931
+ task_list_id: task.task_list_id ?? undefined,
5932
+ plan_id: task.plan_id ?? undefined,
5933
+ assigned_to: task.assigned_to ?? undefined,
5934
+ tags: task.tags,
5935
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_after: options.retry_after || null, failure_reason: reason } },
5936
+ estimated_minutes: task.estimated_minutes ?? undefined,
5937
+ recurrence_rule: task.recurrence_rule ?? undefined,
5938
+ due_at: options.retry_after || task.due_at || undefined
5939
+ }, d);
5940
+ }
5941
+ return { task: failedTask, retryTask };
5942
+ }
5943
+ function getStaleTasks(staleMinutes = 30, filters, db) {
5944
+ const d = db || getDatabase();
5945
+ const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
5946
+ const conditions = [
5947
+ "status = 'in_progress'",
5948
+ "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
5949
+ ];
5950
+ const params = [cutoff, cutoff];
5951
+ if (filters?.project_id) {
5952
+ conditions.push("project_id = ?");
5953
+ params.push(filters.project_id);
5954
+ }
5955
+ if (filters?.task_list_id) {
5956
+ conditions.push("task_list_id = ?");
5957
+ params.push(filters.task_list_id);
5958
+ }
5959
+ const where = conditions.join(" AND ");
5960
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
5961
+ return rows.map(rowToTask);
5962
+ }
5963
+ function getStatus(filters, agentId, db) {
5964
+ const d = db || getDatabase();
5965
+ const pending = countTasks({ ...filters, status: "pending" }, d);
5966
+ const in_progress = countTasks({ ...filters, status: "in_progress" }, d);
5967
+ const completed = countTasks({ ...filters, status: "completed" }, d);
5968
+ const total = countTasks(filters || {}, d);
5969
+ const active_work = getActiveWork(filters, d);
5970
+ const next_task = getNextTask(agentId, filters, d);
5971
+ const stale = getStaleTasks(30, filters, d);
5972
+ const conditions = ["recurrence_rule IS NOT NULL", "status = 'pending'", "due_at < ?"];
5973
+ const params = [now()];
5974
+ if (filters?.project_id) {
5975
+ conditions.push("project_id = ?");
5976
+ params.push(filters.project_id);
5977
+ }
5978
+ if (filters?.task_list_id) {
5979
+ conditions.push("task_list_id = ?");
5980
+ params.push(filters.task_list_id);
5981
+ }
5982
+ const overdueRow = d.query(`SELECT COUNT(*) as count FROM tasks WHERE ${conditions.join(" AND ")}`).get(...params);
5983
+ return {
5984
+ pending,
5985
+ in_progress,
5986
+ completed,
5987
+ total,
5988
+ active_work,
5989
+ next_task,
5990
+ stale_count: stale.length,
5991
+ overdue_recurring: overdueRow.count
5992
+ };
5993
+ }
5994
+ function wouldCreateCycle(taskId, dependsOn, db) {
5995
+ const visited = new Set;
5996
+ const queue = [dependsOn];
5997
+ while (queue.length > 0) {
5998
+ const current = queue.shift();
5999
+ if (current === taskId)
6000
+ return true;
6001
+ if (visited.has(current))
6002
+ continue;
6003
+ visited.add(current);
6004
+ const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
6005
+ for (const dep of deps) {
6006
+ queue.push(dep.depends_on);
6007
+ }
6008
+ }
6009
+ return false;
6010
+ }
6011
+ function getTaskStats(filters, db) {
6012
+ const d = db || getDatabase();
6013
+ const conditions = [];
6014
+ const params = [];
6015
+ if (filters?.project_id) {
6016
+ conditions.push("project_id = ?");
6017
+ params.push(filters.project_id);
6018
+ }
6019
+ if (filters?.task_list_id) {
6020
+ conditions.push("task_list_id = ?");
6021
+ params.push(filters.task_list_id);
6022
+ }
6023
+ if (filters?.agent_id) {
6024
+ conditions.push("(agent_id = ? OR assigned_to = ?)");
6025
+ params.push(filters.agent_id, filters.agent_id);
6026
+ }
6027
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
6028
+ const totalRow = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
6029
+ const statusRows = d.query(`SELECT status, COUNT(*) as count FROM tasks ${where} GROUP BY status`).all(...params);
6030
+ const by_status = {};
6031
+ for (const r of statusRows)
6032
+ by_status[r.status] = r.count;
6033
+ const priorityRows = d.query(`SELECT priority, COUNT(*) as count FROM tasks ${where} GROUP BY priority`).all(...params);
6034
+ const by_priority = {};
6035
+ for (const r of priorityRows)
6036
+ by_priority[r.priority] = r.count;
6037
+ const agentRows = d.query(`SELECT COALESCE(assigned_to, agent_id, 'unassigned') as agent, COUNT(*) as count FROM tasks ${where} GROUP BY agent`).all(...params);
6038
+ const by_agent = {};
6039
+ for (const r of agentRows)
6040
+ by_agent[r.agent] = r.count;
6041
+ const completed = by_status["completed"] || 0;
6042
+ const completion_rate = totalRow.count > 0 ? Math.round(completed / totalRow.count * 100) : 0;
6043
+ return { total: totalRow.count, by_status, by_priority, completion_rate, by_agent };
6044
+ }
6045
+ function bulkCreateTasks(inputs, db) {
6046
+ const d = db || getDatabase();
6047
+ const tempIdToRealId = new Map;
6048
+ const created = [];
6049
+ const tx = d.transaction(() => {
6050
+ for (const input of inputs) {
6051
+ const { temp_id, depends_on_temp_ids: _deps, ...createInput } = input;
6052
+ const task = createTask(createInput, d);
6053
+ if (temp_id)
6054
+ tempIdToRealId.set(temp_id, task.id);
6055
+ created.push({ temp_id: temp_id || null, id: task.id, short_id: task.short_id, title: task.title });
6056
+ }
6057
+ for (const input of inputs) {
6058
+ if (input.depends_on_temp_ids && input.depends_on_temp_ids.length > 0) {
6059
+ const taskId = input.temp_id ? tempIdToRealId.get(input.temp_id) : null;
6060
+ if (!taskId)
6061
+ continue;
6062
+ for (const depTempId of input.depends_on_temp_ids) {
6063
+ const depRealId = tempIdToRealId.get(depTempId);
6064
+ if (depRealId) {
6065
+ addDependency(taskId, depRealId, d);
6066
+ }
6067
+ }
6068
+ }
6069
+ }
6070
+ });
6071
+ tx();
6072
+ return { created };
6073
+ }
6074
+ function bulkUpdateTasks(taskIds, updates, db) {
6075
+ const d = db || getDatabase();
6076
+ let updated = 0;
6077
+ const failed = [];
6078
+ const tx = d.transaction(() => {
6079
+ for (const id of taskIds) {
6080
+ try {
6081
+ const task = getTask(id, d);
6082
+ if (!task) {
6083
+ failed.push({ id, error: "Task not found" });
6084
+ continue;
6085
+ }
6086
+ updateTask(id, { ...updates, version: task.version }, d);
6087
+ updated++;
6088
+ } catch (e) {
6089
+ failed.push({ id, error: e instanceof Error ? e.message : String(e) });
6090
+ }
6091
+ }
6092
+ });
6093
+ tx();
6094
+ return { updated, failed };
6095
+ }
6096
+
6097
+ // src/db/comments.ts
6098
+ init_database();
6099
+ function addComment(input, db) {
6100
+ const d = db || getDatabase();
6101
+ if (!getTask(input.task_id, d)) {
6102
+ throw new TaskNotFoundError(input.task_id);
6103
+ }
6104
+ const id = uuid();
6105
+ const timestamp = now();
6106
+ d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, created_at)
6107
+ VALUES (?, ?, ?, ?, ?, ?)`, [
6108
+ id,
6109
+ input.task_id,
6110
+ input.agent_id || null,
6111
+ input.session_id || null,
6112
+ input.content,
6113
+ timestamp
6114
+ ]);
6115
+ return getComment(id, d);
6116
+ }
6117
+ function getComment(id, db) {
6118
+ const d = db || getDatabase();
6119
+ return d.query("SELECT * FROM task_comments WHERE id = ?").get(id);
6120
+ }
6121
+
6122
+ // src/db/plans.ts
6123
+ init_database();
5515
6124
  function createPlan(input, db) {
5516
6125
  const d = db || getDatabase();
5517
6126
  const id = uuid();
@@ -5657,19 +6266,64 @@ function rowToTask2(row) {
5657
6266
  requires_approval: Boolean(row.requires_approval)
5658
6267
  };
5659
6268
  }
5660
- function searchTasks(query, projectId, taskListId, db) {
6269
+ function searchTasks(options, projectId, taskListId, db) {
6270
+ const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
5661
6271
  const d = db || getDatabase();
5662
6272
  clearExpiredLocks(d);
5663
- const pattern = `%${query}%`;
6273
+ const pattern = `%${opts.query}%`;
5664
6274
  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 ?))`;
5665
6275
  const params = [pattern, pattern, pattern];
5666
- if (projectId) {
6276
+ if (opts.project_id) {
5667
6277
  sql += " AND project_id = ?";
5668
- params.push(projectId);
6278
+ params.push(opts.project_id);
5669
6279
  }
5670
- if (taskListId) {
6280
+ if (opts.task_list_id) {
5671
6281
  sql += " AND task_list_id = ?";
5672
- params.push(taskListId);
6282
+ params.push(opts.task_list_id);
6283
+ }
6284
+ if (opts.status) {
6285
+ if (Array.isArray(opts.status)) {
6286
+ sql += ` AND status IN (${opts.status.map(() => "?").join(",")})`;
6287
+ params.push(...opts.status);
6288
+ } else {
6289
+ sql += " AND status = ?";
6290
+ params.push(opts.status);
6291
+ }
6292
+ }
6293
+ if (opts.priority) {
6294
+ if (Array.isArray(opts.priority)) {
6295
+ sql += ` AND priority IN (${opts.priority.map(() => "?").join(",")})`;
6296
+ params.push(...opts.priority);
6297
+ } else {
6298
+ sql += " AND priority = ?";
6299
+ params.push(opts.priority);
6300
+ }
6301
+ }
6302
+ if (opts.assigned_to) {
6303
+ sql += " AND assigned_to = ?";
6304
+ params.push(opts.assigned_to);
6305
+ }
6306
+ if (opts.agent_id) {
6307
+ sql += " AND agent_id = ?";
6308
+ params.push(opts.agent_id);
6309
+ }
6310
+ if (opts.created_after) {
6311
+ sql += " AND created_at > ?";
6312
+ params.push(opts.created_after);
6313
+ }
6314
+ if (opts.updated_after) {
6315
+ sql += " AND updated_at > ?";
6316
+ params.push(opts.updated_after);
6317
+ }
6318
+ if (opts.has_dependencies === true) {
6319
+ sql += " AND id IN (SELECT task_id FROM task_dependencies)";
6320
+ } else if (opts.has_dependencies === false) {
6321
+ sql += " AND id NOT IN (SELECT task_id FROM task_dependencies)";
6322
+ }
6323
+ if (opts.is_blocked === true) {
6324
+ 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')";
6325
+ } else if (opts.is_blocked === false) {
6326
+ 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')";
5673
6327
  }
5674
6328
  sql += ` ORDER BY
5675
6329
  CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
@@ -6199,28 +6853,41 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
6199
6853
  init_database();
6200
6854
  var server = new McpServer({
6201
6855
  name: "todos",
6202
- version: "0.9.33"
6856
+ version: "0.9.35"
6203
6857
  });
6204
6858
  function formatError(error) {
6205
- if (error instanceof VersionConflictError)
6206
- return `Version conflict: ${error.message}`;
6207
- if (error instanceof TaskNotFoundError)
6208
- return `Not found: ${error.message}`;
6209
- if (error instanceof PlanNotFoundError)
6210
- return `Not found: ${error.message}`;
6211
- if (error instanceof TaskListNotFoundError)
6212
- return `Not found: ${error.message}`;
6213
- if (error instanceof LockError)
6214
- return `Lock error: ${error.message}`;
6215
- if (error instanceof DependencyCycleError)
6216
- return `Dependency cycle: ${error.message}`;
6859
+ if (error instanceof VersionConflictError) {
6860
+ return JSON.stringify({ code: VersionConflictError.code, message: error.message, suggestion: VersionConflictError.suggestion });
6861
+ }
6862
+ if (error instanceof TaskNotFoundError) {
6863
+ return JSON.stringify({ code: TaskNotFoundError.code, message: error.message, suggestion: TaskNotFoundError.suggestion });
6864
+ }
6865
+ if (error instanceof ProjectNotFoundError) {
6866
+ return JSON.stringify({ code: ProjectNotFoundError.code, message: error.message, suggestion: ProjectNotFoundError.suggestion });
6867
+ }
6868
+ if (error instanceof PlanNotFoundError) {
6869
+ return JSON.stringify({ code: PlanNotFoundError.code, message: error.message, suggestion: PlanNotFoundError.suggestion });
6870
+ }
6871
+ if (error instanceof TaskListNotFoundError) {
6872
+ return JSON.stringify({ code: TaskListNotFoundError.code, message: error.message, suggestion: TaskListNotFoundError.suggestion });
6873
+ }
6874
+ if (error instanceof LockError) {
6875
+ return JSON.stringify({ code: LockError.code, message: error.message, suggestion: LockError.suggestion });
6876
+ }
6877
+ if (error instanceof AgentNotFoundError) {
6878
+ return JSON.stringify({ code: AgentNotFoundError.code, message: error.message, suggestion: AgentNotFoundError.suggestion });
6879
+ }
6880
+ if (error instanceof DependencyCycleError) {
6881
+ return JSON.stringify({ code: DependencyCycleError.code, message: error.message, suggestion: DependencyCycleError.suggestion });
6882
+ }
6217
6883
  if (error instanceof CompletionGuardError) {
6218
- const retry = error.retryAfterSeconds ? ` (retry after ${error.retryAfterSeconds}s)` : "";
6219
- return `Completion blocked: ${error.reason}${retry}`;
6884
+ const retry = error.retryAfterSeconds ? { retryAfterSeconds: error.retryAfterSeconds } : {};
6885
+ return JSON.stringify({ code: CompletionGuardError.code, message: error.reason, suggestion: CompletionGuardError.suggestion, ...retry });
6220
6886
  }
6221
- if (error instanceof Error)
6222
- return error.message;
6223
- return String(error);
6887
+ if (error instanceof Error) {
6888
+ return JSON.stringify({ code: "UNKNOWN_ERROR", message: error.message });
6889
+ }
6890
+ return JSON.stringify({ code: "UNKNOWN_ERROR", message: String(error) });
6224
6891
  }
6225
6892
  function resolveId(partialId, table = "tasks") {
6226
6893
  const db = getDatabase();
@@ -6243,7 +6910,8 @@ function formatTask(task) {
6243
6910
  const id = task.short_id || task.id.slice(0, 8);
6244
6911
  const assigned = task.assigned_to ? ` -> ${task.assigned_to}` : "";
6245
6912
  const lock = task.locked_by ? ` [locked:${task.locked_by}]` : "";
6246
- return `${id} ${task.status.padEnd(11)} ${task.priority.padEnd(8)} ${task.title}${assigned}${lock}`;
6913
+ const recur = task.recurrence_rule ? ` [\u21BB]` : "";
6914
+ return `${id} ${task.status.padEnd(11)} ${task.priority.padEnd(8)} ${task.title}${assigned}${lock}${recur}`;
6247
6915
  }
6248
6916
  function formatTaskDetail(task) {
6249
6917
  const parts = [
@@ -6268,6 +6936,10 @@ function formatTaskDetail(task) {
6268
6936
  parts.push(`Plan: ${task.plan_id}`);
6269
6937
  if (task.tags.length > 0)
6270
6938
  parts.push(`Tags: ${task.tags.join(", ")}`);
6939
+ if (task.recurrence_rule)
6940
+ parts.push(`Recurrence: ${task.recurrence_rule}`);
6941
+ if (task.recurrence_parent_id)
6942
+ parts.push(`Recurrence parent: ${task.recurrence_parent_id}`);
6271
6943
  parts.push(`Version: ${task.version}`);
6272
6944
  parts.push(`Created: ${task.created_at}`);
6273
6945
  if (task.completed_at)
@@ -6291,7 +6963,8 @@ server.tool("create_task", "Create a new task", {
6291
6963
  tags: exports_external.array(exports_external.string()).optional(),
6292
6964
  metadata: exports_external.record(exports_external.unknown()).optional(),
6293
6965
  estimated_minutes: exports_external.number().optional(),
6294
- requires_approval: exports_external.boolean().optional()
6966
+ requires_approval: exports_external.boolean().optional(),
6967
+ recurrence_rule: exports_external.string().optional()
6295
6968
  }, async (params) => {
6296
6969
  try {
6297
6970
  const resolved = { ...params };
@@ -6309,7 +6982,7 @@ server.tool("create_task", "Create a new task", {
6309
6982
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6310
6983
  }
6311
6984
  });
6312
- server.tool("list_tasks", "List tasks with optional filters", {
6985
+ server.tool("list_tasks", "List tasks with optional filters and pagination.", {
6313
6986
  project_id: exports_external.string().optional(),
6314
6987
  status: exports_external.union([
6315
6988
  exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]),
@@ -6322,7 +6995,10 @@ server.tool("list_tasks", "List tasks with optional filters", {
6322
6995
  assigned_to: exports_external.string().optional(),
6323
6996
  tags: exports_external.array(exports_external.string()).optional(),
6324
6997
  plan_id: exports_external.string().optional(),
6325
- task_list_id: exports_external.string().optional()
6998
+ task_list_id: exports_external.string().optional(),
6999
+ has_recurrence: exports_external.boolean().optional(),
7000
+ limit: exports_external.number().optional(),
7001
+ offset: exports_external.number().optional()
6326
7002
  }, async (params) => {
6327
7003
  try {
6328
7004
  const resolved = { ...params };
@@ -6333,8 +7009,10 @@ server.tool("list_tasks", "List tasks with optional filters", {
6333
7009
  if (resolved.task_list_id)
6334
7010
  resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
6335
7011
  const tasks = listTasks(resolved);
7012
+ const { limit: _limit, offset: _offset, ...countFilter } = resolved;
7013
+ const total = countTasks(countFilter);
6336
7014
  if (tasks.length === 0) {
6337
- return { content: [{ type: "text", text: "No tasks found." }] };
7015
+ return { content: [{ type: "text", text: total > 0 ? `No tasks in this page (total: ${total}).` : "No tasks found." }] };
6338
7016
  }
6339
7017
  const text = tasks.map((t) => {
6340
7018
  const lock = t.locked_by ? ` [locked by ${t.locked_by}]` : "";
@@ -6342,13 +7020,15 @@ server.tool("list_tasks", "List tasks with optional filters", {
6342
7020
  return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned}${lock}`;
6343
7021
  }).join(`
6344
7022
  `);
7023
+ const pagination = resolved.limit ? `
7024
+ (showing ${tasks.length} of ${total}, offset: ${resolved.offset || 0})` : "";
6345
7025
  return { content: [{ type: "text", text: `${tasks.length} task(s):
6346
- ${text}` }] };
7026
+ ${text}${pagination}` }] };
6347
7027
  } catch (e) {
6348
7028
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6349
7029
  }
6350
7030
  });
6351
- server.tool("get_task", "Get full task details with relations", {
7031
+ server.tool("get_task", "Get full task details with subtasks, deps, and comments.", {
6352
7032
  id: exports_external.string()
6353
7033
  }, async ({ id }) => {
6354
7034
  try {
@@ -6417,7 +7097,7 @@ server.tool("update_task", "Update task fields. Version required for optimistic
6417
7097
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6418
7098
  }
6419
7099
  });
6420
- server.tool("delete_task", "Delete a task permanently", {
7100
+ server.tool("delete_task", "Delete a task permanently. Subtasks cascade-deleted.", {
6421
7101
  id: exports_external.string()
6422
7102
  }, async ({ id }) => {
6423
7103
  try {
@@ -6433,7 +7113,7 @@ server.tool("delete_task", "Delete a task permanently", {
6433
7113
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6434
7114
  }
6435
7115
  });
6436
- server.tool("start_task", "Claim, lock, and set task status to in_progress.", {
7116
+ server.tool("start_task", "Claim, lock, and set task to in_progress.", {
6437
7117
  id: exports_external.string(),
6438
7118
  agent_id: exports_external.string()
6439
7119
  }, async ({ id, agent_id }) => {
@@ -6445,19 +7125,26 @@ server.tool("start_task", "Claim, lock, and set task status to in_progress.", {
6445
7125
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6446
7126
  }
6447
7127
  });
6448
- server.tool("complete_task", "Mark task completed and release lock.", {
7128
+ server.tool("complete_task", "Complete a task. For recurring tasks, auto-spawns next instance.", {
6449
7129
  id: exports_external.string(),
6450
- agent_id: exports_external.string().optional()
6451
- }, async ({ id, agent_id }) => {
7130
+ agent_id: exports_external.string().optional(),
7131
+ skip_recurrence: exports_external.boolean().optional()
7132
+ }, async ({ id, agent_id, skip_recurrence }) => {
6452
7133
  try {
6453
7134
  const resolvedId = resolveId(id);
6454
- const task = completeTask(resolvedId, agent_id);
6455
- return { content: [{ type: "text", text: `completed: ${formatTask(task)}` }] };
7135
+ const task = completeTask(resolvedId, agent_id, undefined, { skip_recurrence });
7136
+ let text = `completed: ${formatTask(task)}`;
7137
+ if (task.metadata._next_recurrence) {
7138
+ const next = task.metadata._next_recurrence;
7139
+ text += `
7140
+ next: ${next.short_id || next.id.slice(0, 8)} due ${next.due_at}`;
7141
+ }
7142
+ return { content: [{ type: "text", text }] };
6456
7143
  } catch (e) {
6457
7144
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6458
7145
  }
6459
7146
  });
6460
- server.tool("lock_task", "Acquire exclusive lock on a task", {
7147
+ server.tool("lock_task", "Acquire exclusive lock. Expires after 30 min. Idempotent per agent.", {
6461
7148
  id: exports_external.string(),
6462
7149
  agent_id: exports_external.string()
6463
7150
  }, async ({ id, agent_id }) => {
@@ -6472,7 +7159,7 @@ server.tool("lock_task", "Acquire exclusive lock on a task", {
6472
7159
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6473
7160
  }
6474
7161
  });
6475
- server.tool("unlock_task", "Release exclusive lock on a task", {
7162
+ server.tool("unlock_task", "Release exclusive lock on a task.", {
6476
7163
  id: exports_external.string(),
6477
7164
  agent_id: exports_external.string().optional()
6478
7165
  }, async ({ id, agent_id }) => {
@@ -6484,7 +7171,7 @@ server.tool("unlock_task", "Release exclusive lock on a task", {
6484
7171
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6485
7172
  }
6486
7173
  });
6487
- server.tool("add_dependency", "Add a dependency: task_id depends on depends_on.", {
7174
+ server.tool("add_dependency", "Add a dependency. Prevents cycles via BFS detection.", {
6488
7175
  task_id: exports_external.string(),
6489
7176
  depends_on: exports_external.string()
6490
7177
  }, async ({ task_id, depends_on }) => {
@@ -6497,7 +7184,7 @@ server.tool("add_dependency", "Add a dependency: task_id depends on depends_on."
6497
7184
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6498
7185
  }
6499
7186
  });
6500
- server.tool("remove_dependency", "Remove a dependency between tasks", {
7187
+ server.tool("remove_dependency", "Remove a dependency link between two tasks.", {
6501
7188
  task_id: exports_external.string(),
6502
7189
  depends_on: exports_external.string()
6503
7190
  }, async ({ task_id, depends_on }) => {
@@ -6515,7 +7202,7 @@ server.tool("remove_dependency", "Remove a dependency between tasks", {
6515
7202
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6516
7203
  }
6517
7204
  });
6518
- server.tool("add_comment", "Add a comment/note to a task", {
7205
+ server.tool("add_comment", "Add a comment or note to a task. Comments are append-only.", {
6519
7206
  task_id: exports_external.string(),
6520
7207
  content: exports_external.string(),
6521
7208
  agent_id: exports_external.string().optional(),
@@ -6546,7 +7233,7 @@ ${text}` }] };
6546
7233
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6547
7234
  }
6548
7235
  });
6549
- server.tool("create_project", "Register a new project", {
7236
+ server.tool("create_project", "Register a new project with auto-generated task prefix.", {
6550
7237
  name: exports_external.string(),
6551
7238
  path: exports_external.string(),
6552
7239
  description: exports_external.string().optional(),
@@ -6565,7 +7252,7 @@ server.tool("create_project", "Register a new project", {
6565
7252
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6566
7253
  }
6567
7254
  });
6568
- server.tool("create_plan", "Create a new plan", {
7255
+ server.tool("create_plan", "Create a plan to group related tasks.", {
6569
7256
  name: exports_external.string(),
6570
7257
  project_id: exports_external.string().optional(),
6571
7258
  description: exports_external.string().optional(),
@@ -6590,7 +7277,7 @@ server.tool("create_plan", "Create a new plan", {
6590
7277
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6591
7278
  }
6592
7279
  });
6593
- server.tool("list_plans", "List plans with optional project filter", {
7280
+ server.tool("list_plans", "List all plans, optionally filtered by project.", {
6594
7281
  project_id: exports_external.string().optional()
6595
7282
  }, async ({ project_id }) => {
6596
7283
  try {
@@ -6610,7 +7297,7 @@ ${text}` }] };
6610
7297
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6611
7298
  }
6612
7299
  });
6613
- server.tool("get_plan", "Get plan details", {
7300
+ server.tool("get_plan", "Get plan details including status and timestamps.", {
6614
7301
  id: exports_external.string()
6615
7302
  }, async ({ id }) => {
6616
7303
  try {
@@ -6635,7 +7322,7 @@ server.tool("get_plan", "Get plan details", {
6635
7322
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6636
7323
  }
6637
7324
  });
6638
- server.tool("update_plan", "Update a plan", {
7325
+ server.tool("update_plan", "Update plan fields (name, description, status).", {
6639
7326
  id: exports_external.string(),
6640
7327
  name: exports_external.string().optional(),
6641
7328
  description: exports_external.string().optional(),
@@ -6659,7 +7346,7 @@ server.tool("update_plan", "Update a plan", {
6659
7346
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6660
7347
  }
6661
7348
  });
6662
- server.tool("delete_plan", "Delete a plan", {
7349
+ server.tool("delete_plan", "Delete a plan. Tasks in the plan are orphaned (not deleted).", {
6663
7350
  id: exports_external.string()
6664
7351
  }, async ({ id }) => {
6665
7352
  try {
@@ -6675,15 +7362,34 @@ server.tool("delete_plan", "Delete a plan", {
6675
7362
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6676
7363
  }
6677
7364
  });
6678
- server.tool("search_tasks", "Full-text search across task titles, descriptions, tags.", {
7365
+ server.tool("search_tasks", "Full-text search across tasks with filters.", {
6679
7366
  query: exports_external.string(),
6680
7367
  project_id: exports_external.string().optional(),
6681
- task_list_id: exports_external.string().optional()
6682
- }, async ({ query, project_id, task_list_id }) => {
7368
+ task_list_id: exports_external.string().optional(),
7369
+ status: exports_external.union([
7370
+ exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]),
7371
+ exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]))
7372
+ ]).optional(),
7373
+ priority: exports_external.union([
7374
+ exports_external.enum(["low", "medium", "high", "critical"]),
7375
+ exports_external.array(exports_external.enum(["low", "medium", "high", "critical"]))
7376
+ ]).optional(),
7377
+ assigned_to: exports_external.string().optional(),
7378
+ agent_id: exports_external.string().optional(),
7379
+ created_after: exports_external.string().optional(),
7380
+ updated_after: exports_external.string().optional(),
7381
+ has_dependencies: exports_external.boolean().optional(),
7382
+ is_blocked: exports_external.boolean().optional()
7383
+ }, async ({ query, project_id, task_list_id, ...filters }) => {
6683
7384
  try {
6684
7385
  const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
6685
7386
  const resolvedTaskListId = task_list_id ? resolveId(task_list_id, "task_lists") : undefined;
6686
- const tasks = searchTasks(query, resolvedProjectId, resolvedTaskListId);
7387
+ const tasks = searchTasks({
7388
+ query,
7389
+ project_id: resolvedProjectId,
7390
+ task_list_id: resolvedTaskListId,
7391
+ ...filters
7392
+ });
6687
7393
  if (tasks.length === 0) {
6688
7394
  return { content: [{ type: "text", text: `No tasks matching "${query}".` }] };
6689
7395
  }
@@ -6695,7 +7401,7 @@ ${text}` }] };
6695
7401
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6696
7402
  }
6697
7403
  });
6698
- server.tool("sync", "Sync tasks with an agent task list.", {
7404
+ server.tool("sync", "Sync tasks between local DB and agent task list.", {
6699
7405
  task_list_id: exports_external.string().optional(),
6700
7406
  agent: exports_external.string().optional(),
6701
7407
  all_agents: exports_external.boolean().optional(),
@@ -6743,7 +7449,7 @@ server.tool("sync", "Sync tasks with an agent task list.", {
6743
7449
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6744
7450
  }
6745
7451
  });
6746
- server.tool("register_agent", "Register an agent (idempotent by name).", {
7452
+ server.tool("register_agent", "Register an agent (idempotent by name). Updates last_seen_at.", {
6747
7453
  name: exports_external.string(),
6748
7454
  description: exports_external.string().optional()
6749
7455
  }, async ({ name, description }) => {
@@ -6780,7 +7486,7 @@ ${text}` }] };
6780
7486
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6781
7487
  }
6782
7488
  });
6783
- server.tool("get_agent", "Get agent details by ID or name", {
7489
+ server.tool("get_agent", "Get agent details by ID or name. Provide one of id or name.", {
6784
7490
  id: exports_external.string().optional(),
6785
7491
  name: exports_external.string().optional()
6786
7492
  }, async ({ id, name }) => {
@@ -6809,9 +7515,9 @@ server.tool("get_agent", "Get agent details by ID or name", {
6809
7515
  }
6810
7516
  });
6811
7517
  server.tool("rename_agent", "Rename an agent. Resolve by id or current name.", {
6812
- id: exports_external.string().optional().describe("Agent ID"),
6813
- name: exports_external.string().optional().describe("Current agent name"),
6814
- new_name: exports_external.string().describe("New name for the agent")
7518
+ id: exports_external.string().optional(),
7519
+ name: exports_external.string().optional(),
7520
+ new_name: exports_external.string()
6815
7521
  }, async ({ id, name, new_name }) => {
6816
7522
  try {
6817
7523
  if (!id && !name) {
@@ -6834,8 +7540,8 @@ ID: ${updated.id}`
6834
7540
  }
6835
7541
  });
6836
7542
  server.tool("delete_agent", "Delete an agent permanently. Resolve by id or name.", {
6837
- id: exports_external.string().optional().describe("Agent ID"),
6838
- name: exports_external.string().optional().describe("Agent name")
7543
+ id: exports_external.string().optional(),
7544
+ name: exports_external.string().optional()
6839
7545
  }, async ({ id, name }) => {
6840
7546
  try {
6841
7547
  if (!id && !name) {
@@ -6857,7 +7563,7 @@ server.tool("delete_agent", "Delete an agent permanently. Resolve by id or name.
6857
7563
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6858
7564
  }
6859
7565
  });
6860
- server.tool("create_task_list", "Create a new task list", {
7566
+ server.tool("create_task_list", "Create a task list container for organizing tasks.", {
6861
7567
  name: exports_external.string(),
6862
7568
  slug: exports_external.string().optional(),
6863
7569
  project_id: exports_external.string().optional(),
@@ -6883,7 +7589,7 @@ Description: ${list.description}` : ""}`
6883
7589
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6884
7590
  }
6885
7591
  });
6886
- server.tool("list_task_lists", "List task lists, optionally filtered by project", {
7592
+ server.tool("list_task_lists", "List all task lists, optionally filtered by project.", {
6887
7593
  project_id: exports_external.string().optional()
6888
7594
  }, async ({ project_id }) => {
6889
7595
  try {
@@ -6903,7 +7609,7 @@ ${text}` }] };
6903
7609
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6904
7610
  }
6905
7611
  });
6906
- server.tool("get_task_list", "Get task list details", {
7612
+ server.tool("get_task_list", "Get task list details including slug and metadata.", {
6907
7613
  id: exports_external.string()
6908
7614
  }, async ({ id }) => {
6909
7615
  try {
@@ -6931,7 +7637,7 @@ server.tool("get_task_list", "Get task list details", {
6931
7637
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6932
7638
  }
6933
7639
  });
6934
- server.tool("update_task_list", "Update a task list", {
7640
+ server.tool("update_task_list", "Update a task list's name or description.", {
6935
7641
  id: exports_external.string(),
6936
7642
  name: exports_external.string().optional(),
6937
7643
  description: exports_external.string().optional()
@@ -6952,7 +7658,7 @@ Slug: ${list.slug}`
6952
7658
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6953
7659
  }
6954
7660
  });
6955
- server.tool("delete_task_list", "Delete a task list. Tasks lose association but keep data.", {
7661
+ server.tool("delete_task_list", "Delete a task list. Tasks are orphaned, not deleted.", {
6956
7662
  id: exports_external.string()
6957
7663
  }, async ({ id }) => {
6958
7664
  try {
@@ -6968,7 +7674,7 @@ server.tool("delete_task_list", "Delete a task list. Tasks lose association but
6968
7674
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6969
7675
  }
6970
7676
  });
6971
- server.tool("get_task_history", "Get audit log for a task.", {
7677
+ server.tool("get_task_history", "Get audit log \u2014 field changes with timestamps and actors.", {
6972
7678
  task_id: exports_external.string()
6973
7679
  }, async ({ task_id }) => {
6974
7680
  try {
@@ -6985,7 +7691,7 @@ ${text}` }] };
6985
7691
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6986
7692
  }
6987
7693
  });
6988
- server.tool("get_recent_activity", "Get recent task changes across all tasks.", {
7694
+ server.tool("get_recent_activity", "Get recent task changes \u2014 global activity feed.", {
6989
7695
  limit: exports_external.number().optional()
6990
7696
  }, async ({ limit }) => {
6991
7697
  try {
@@ -7001,7 +7707,7 @@ ${text}` }] };
7001
7707
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7002
7708
  }
7003
7709
  });
7004
- server.tool("create_webhook", "Register a webhook to receive task change events.", {
7710
+ server.tool("create_webhook", "Register a webhook for task change events.", {
7005
7711
  url: exports_external.string(),
7006
7712
  events: exports_external.array(exports_external.string()).optional(),
7007
7713
  secret: exports_external.string().optional()
@@ -7028,7 +7734,7 @@ ${text}` }] };
7028
7734
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7029
7735
  }
7030
7736
  });
7031
- server.tool("delete_webhook", "Delete a webhook", {
7737
+ server.tool("delete_webhook", "Delete a webhook by ID.", {
7032
7738
  id: exports_external.string()
7033
7739
  }, async ({ id }) => {
7034
7740
  try {
@@ -7094,7 +7800,7 @@ ${task.id.slice(0, 8)} | ${task.priority} | ${task.title}` }] };
7094
7800
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7095
7801
  }
7096
7802
  });
7097
- server.tool("delete_template", "Delete a task template", { id: exports_external.string() }, async ({ id }) => {
7803
+ server.tool("delete_template", "Delete a task template by ID.", { id: exports_external.string() }, async ({ id }) => {
7098
7804
  try {
7099
7805
  const { deleteTemplate: deleteTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
7100
7806
  const deleted = deleteTemplate2(id);
@@ -7103,7 +7809,7 @@ server.tool("delete_template", "Delete a task template", { id: exports_external.
7103
7809
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7104
7810
  }
7105
7811
  });
7106
- server.tool("approve_task", "Approve a task that requires approval.", {
7812
+ server.tool("approve_task", "Approve a task with requires_approval=true.", {
7107
7813
  id: exports_external.string(),
7108
7814
  agent_id: exports_external.string().optional()
7109
7815
  }, async ({ id, agent_id }) => {
@@ -7122,7 +7828,31 @@ server.tool("approve_task", "Approve a task that requires approval.", {
7122
7828
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7123
7829
  }
7124
7830
  });
7125
- server.tool("get_my_tasks", "Get assigned tasks and stats for an agent.", {
7831
+ server.tool("fail_task", "Mark a task as failed with structured reason and optional auto-retry.", {
7832
+ id: exports_external.string(),
7833
+ agent_id: exports_external.string().optional(),
7834
+ reason: exports_external.string().optional(),
7835
+ error_code: exports_external.string().optional(),
7836
+ retry: exports_external.boolean().optional(),
7837
+ retry_after: exports_external.string().optional()
7838
+ }, async ({ id, agent_id, reason, error_code, retry, retry_after }) => {
7839
+ try {
7840
+ const resolvedId = resolveId(id);
7841
+ const result = failTask(resolvedId, agent_id, reason, { retry, retry_after, error_code });
7842
+ let text = `failed: ${formatTask(result.task)}`;
7843
+ if (reason)
7844
+ text += `
7845
+ Reason: ${reason}`;
7846
+ if (result.retryTask) {
7847
+ text += `
7848
+ Retry task created: ${formatTask(result.retryTask)}`;
7849
+ }
7850
+ return { content: [{ type: "text", text }] };
7851
+ } catch (e) {
7852
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
7853
+ }
7854
+ });
7855
+ server.tool("get_my_tasks", "Get tasks assigned to/created by an agent with stats.", {
7126
7856
  agent_name: exports_external.string()
7127
7857
  }, async ({ agent_name }) => {
7128
7858
  try {
@@ -7155,7 +7885,7 @@ In Progress:`);
7155
7885
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7156
7886
  }
7157
7887
  });
7158
- server.tool("get_org_chart", "Get agent org chart \u2014 who reports to who.", {}, async () => {
7888
+ server.tool("get_org_chart", "Get agent org chart showing reporting hierarchy.", {}, async () => {
7159
7889
  try {
7160
7890
  let render = function(nodes, indent = 0) {
7161
7891
  return nodes.map((n) => {
@@ -7177,7 +7907,7 @@ server.tool("get_org_chart", "Get agent org chart \u2014 who reports to who.", {
7177
7907
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7178
7908
  }
7179
7909
  });
7180
- server.tool("set_reports_to", "Set who an agent reports to in the org chart.", {
7910
+ server.tool("set_reports_to", "Set agent reporting relationship in org chart.", {
7181
7911
  agent_name: exports_external.string(),
7182
7912
  manager_name: exports_external.string().optional()
7183
7913
  }, async ({ agent_name, manager_name }) => {
@@ -7200,7 +7930,367 @@ server.tool("set_reports_to", "Set who an agent reports to in the org chart.", {
7200
7930
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7201
7931
  }
7202
7932
  });
7203
- server.tool("search_tools", "List tool names matching a query.", { query: exports_external.string().optional() }, async ({ query }) => {
7933
+ server.tool("bulk_update_tasks", "Update multiple tasks at once with the same changes.", {
7934
+ task_ids: exports_external.array(exports_external.string()),
7935
+ status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
7936
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
7937
+ assigned_to: exports_external.string().optional(),
7938
+ tags: exports_external.array(exports_external.string()).optional()
7939
+ }, async ({ task_ids, ...updates }) => {
7940
+ try {
7941
+ const resolvedIds = task_ids.map((id) => resolveId(id));
7942
+ const result = bulkUpdateTasks(resolvedIds, updates);
7943
+ const parts = [`Updated ${result.updated} task(s).`];
7944
+ if (result.failed.length > 0) {
7945
+ parts.push(`Failed ${result.failed.length}:`);
7946
+ for (const f of result.failed)
7947
+ parts.push(` ${f.id.slice(0, 8)}: ${f.error}`);
7948
+ }
7949
+ return { content: [{ type: "text", text: parts.join(`
7950
+ `) }] };
7951
+ } catch (e) {
7952
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
7953
+ }
7954
+ });
7955
+ server.tool("clone_task", "Duplicate a task with optional field overrides.", {
7956
+ task_id: exports_external.string(),
7957
+ title: exports_external.string().optional(),
7958
+ description: exports_external.string().optional(),
7959
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
7960
+ status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
7961
+ project_id: exports_external.string().optional(),
7962
+ plan_id: exports_external.string().optional(),
7963
+ task_list_id: exports_external.string().optional(),
7964
+ assigned_to: exports_external.string().optional(),
7965
+ tags: exports_external.array(exports_external.string()).optional(),
7966
+ estimated_minutes: exports_external.number().optional()
7967
+ }, async ({ task_id, ...overrides }) => {
7968
+ try {
7969
+ const resolvedId = resolveId(task_id);
7970
+ const resolved = { ...overrides };
7971
+ if (resolved.project_id)
7972
+ resolved.project_id = resolveId(resolved.project_id, "projects");
7973
+ if (resolved.plan_id)
7974
+ resolved.plan_id = resolveId(resolved.plan_id, "plans");
7975
+ if (resolved.task_list_id)
7976
+ resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
7977
+ const task = cloneTask(resolvedId, resolved);
7978
+ return { content: [{ type: "text", text: `cloned: ${formatTask(task)}` }] };
7979
+ } catch (e) {
7980
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
7981
+ }
7982
+ });
7983
+ server.tool("get_task_stats", "Get task analytics: counts by status, priority, agent.", {
7984
+ project_id: exports_external.string().optional(),
7985
+ task_list_id: exports_external.string().optional(),
7986
+ agent_id: exports_external.string().optional()
7987
+ }, async ({ project_id, task_list_id, agent_id }) => {
7988
+ try {
7989
+ const filters = {};
7990
+ if (project_id)
7991
+ filters.project_id = resolveId(project_id, "projects");
7992
+ if (task_list_id)
7993
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
7994
+ if (agent_id)
7995
+ filters.agent_id = agent_id;
7996
+ const stats = getTaskStats(Object.keys(filters).length > 0 ? filters : undefined);
7997
+ return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
7998
+ } catch (e) {
7999
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8000
+ }
8001
+ });
8002
+ server.tool("get_task_graph", "Get full dependency tree for a task.", {
8003
+ id: exports_external.string(),
8004
+ direction: exports_external.enum(["up", "down", "both"]).optional()
8005
+ }, async ({ id, direction }) => {
8006
+ try {
8007
+ let formatNode = function(node, indent) {
8008
+ const prefix = " ".repeat(indent);
8009
+ const idLabel = node.task.short_id || node.task.id.slice(0, 8);
8010
+ const blocked = node.task.is_blocked ? " (blocked: yes)" : "";
8011
+ let out = `${prefix}[${node.task.status}] ${idLabel} | ${node.task.title}${blocked}
8012
+ `;
8013
+ if (node.depends_on.length > 0) {
8014
+ out += `${prefix} Depends on:
8015
+ `;
8016
+ for (const dep of node.depends_on) {
8017
+ out += formatNode(dep, indent + 2);
8018
+ }
8019
+ }
8020
+ if (node.blocks.length > 0) {
8021
+ out += `${prefix} Blocks:
8022
+ `;
8023
+ for (const dep of node.blocks) {
8024
+ out += formatNode(dep, indent + 2);
8025
+ }
8026
+ }
8027
+ return out;
8028
+ };
8029
+ const taskId = resolveId(id, "tasks");
8030
+ const graph = getTaskGraph(taskId, direction || "both");
8031
+ let text = `Task: ${formatNode(graph, 0)}`;
8032
+ if (graph.depends_on.length > 0) {
8033
+ text += `
8034
+ Depends on:
8035
+ `;
8036
+ for (const dep of graph.depends_on) {
8037
+ text += formatNode(dep, 1);
8038
+ }
8039
+ }
8040
+ if (graph.blocks.length > 0) {
8041
+ text += `
8042
+ Blocks:
8043
+ `;
8044
+ for (const dep of graph.blocks) {
8045
+ text += formatNode(dep, 1);
8046
+ }
8047
+ }
8048
+ return { content: [{ type: "text", text }] };
8049
+ } catch (e) {
8050
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8051
+ }
8052
+ });
8053
+ server.tool("bulk_create_tasks", "Create multiple tasks atomically with dependency support.", {
8054
+ tasks: exports_external.array(exports_external.object({
8055
+ temp_id: exports_external.string().optional(),
8056
+ title: exports_external.string(),
8057
+ description: exports_external.string().optional(),
8058
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
8059
+ status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
8060
+ project_id: exports_external.string().optional(),
8061
+ plan_id: exports_external.string().optional(),
8062
+ task_list_id: exports_external.string().optional(),
8063
+ agent_id: exports_external.string().optional(),
8064
+ assigned_to: exports_external.string().optional(),
8065
+ tags: exports_external.array(exports_external.string()).optional(),
8066
+ estimated_minutes: exports_external.number().optional(),
8067
+ depends_on_temp_ids: exports_external.array(exports_external.string()).optional()
8068
+ })),
8069
+ project_id: exports_external.string().optional(),
8070
+ plan_id: exports_external.string().optional(),
8071
+ task_list_id: exports_external.string().optional()
8072
+ }, async ({ tasks, project_id, plan_id, task_list_id }) => {
8073
+ try {
8074
+ const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
8075
+ const resolvedPlanId = plan_id ? resolveId(plan_id, "plans") : undefined;
8076
+ const resolvedTaskListId = task_list_id ? resolveId(task_list_id, "task_lists") : undefined;
8077
+ const enrichedTasks = tasks.map((t) => ({
8078
+ ...t,
8079
+ project_id: t.project_id || resolvedProjectId,
8080
+ plan_id: t.plan_id || resolvedPlanId,
8081
+ task_list_id: t.task_list_id || resolvedTaskListId
8082
+ }));
8083
+ const result = bulkCreateTasks(enrichedTasks);
8084
+ const lines = result.created.map((t) => {
8085
+ const tid = t.temp_id ? `[${t.temp_id}] ` : "";
8086
+ const sid = t.short_id || t.id.slice(0, 8);
8087
+ return ` ${tid}${sid} | ${t.title}`;
8088
+ });
8089
+ return { content: [{ type: "text", text: `Created ${result.created.length} task(s):
8090
+ ${lines.join(`
8091
+ `)}` }] };
8092
+ } catch (e) {
8093
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8094
+ }
8095
+ });
8096
+ server.tool("move_task", "Move a task to a different list, project, or plan.", {
8097
+ task_id: exports_external.string(),
8098
+ task_list_id: exports_external.string().nullable().optional(),
8099
+ project_id: exports_external.string().nullable().optional(),
8100
+ plan_id: exports_external.string().nullable().optional()
8101
+ }, async ({ task_id, ...target }) => {
8102
+ try {
8103
+ const resolvedId = resolveId(task_id);
8104
+ const resolvedTarget = {};
8105
+ if (target.task_list_id !== undefined)
8106
+ resolvedTarget.task_list_id = target.task_list_id ? resolveId(target.task_list_id, "task_lists") : null;
8107
+ if (target.project_id !== undefined)
8108
+ resolvedTarget.project_id = target.project_id ? resolveId(target.project_id, "projects") : null;
8109
+ if (target.plan_id !== undefined)
8110
+ resolvedTarget.plan_id = target.plan_id ? resolveId(target.plan_id, "plans") : null;
8111
+ const task = moveTask(resolvedId, resolvedTarget);
8112
+ return { content: [{ type: "text", text: `moved: ${formatTask(task)}` }] };
8113
+ } catch (e) {
8114
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8115
+ }
8116
+ });
8117
+ server.tool("get_next_task", "Get the best pending task to work on next.", {
8118
+ agent_id: exports_external.string().optional(),
8119
+ project_id: exports_external.string().optional(),
8120
+ task_list_id: exports_external.string().optional(),
8121
+ plan_id: exports_external.string().optional(),
8122
+ tags: exports_external.array(exports_external.string()).optional()
8123
+ }, async ({ agent_id, project_id, task_list_id, plan_id, tags }) => {
8124
+ try {
8125
+ const filters = {};
8126
+ if (project_id)
8127
+ filters.project_id = resolveId(project_id, "projects");
8128
+ if (task_list_id)
8129
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
8130
+ if (plan_id)
8131
+ filters.plan_id = resolveId(plan_id, "plans");
8132
+ if (tags)
8133
+ filters.tags = tags;
8134
+ const task = getNextTask(agent_id, Object.keys(filters).length > 0 ? filters : undefined);
8135
+ if (!task) {
8136
+ return { content: [{ type: "text", text: "No tasks available \u2014 all pending tasks are blocked, locked, or none exist." }] };
8137
+ }
8138
+ return { content: [{ type: "text", text: `next: ${formatTask(task)}
8139
+ ${formatTaskDetail(task)}` }] };
8140
+ } catch (e) {
8141
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8142
+ }
8143
+ });
8144
+ server.tool("get_active_work", "See all in-progress tasks and who is working on them.", {
8145
+ project_id: exports_external.string().optional(),
8146
+ task_list_id: exports_external.string().optional()
8147
+ }, async ({ project_id, task_list_id }) => {
8148
+ try {
8149
+ const filters = {};
8150
+ if (project_id)
8151
+ filters.project_id = resolveId(project_id, "projects");
8152
+ if (task_list_id)
8153
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
8154
+ const work = getActiveWork(Object.keys(filters).length > 0 ? filters : undefined);
8155
+ if (work.length === 0) {
8156
+ return { content: [{ type: "text", text: "No active work \u2014 no tasks are currently in progress." }] };
8157
+ }
8158
+ const text = work.map((w) => {
8159
+ const id = w.short_id || w.id.slice(0, 8);
8160
+ const agent = w.assigned_to || w.locked_by || "unassigned";
8161
+ const since = w.updated_at;
8162
+ return `${agent.padEnd(12)} | ${w.priority.padEnd(8)} | ${id} | ${w.title} (since ${since})`;
8163
+ }).join(`
8164
+ `);
8165
+ return { content: [{ type: "text", text: `${work.length} active task(s):
8166
+ ${text}` }] };
8167
+ } catch (e) {
8168
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8169
+ }
8170
+ });
8171
+ server.tool("get_tasks_changed_since", "Get tasks modified after a timestamp for incremental sync.", {
8172
+ since: exports_external.string(),
8173
+ project_id: exports_external.string().optional(),
8174
+ task_list_id: exports_external.string().optional()
8175
+ }, async ({ since, project_id, task_list_id }) => {
8176
+ try {
8177
+ const filters = {};
8178
+ if (project_id)
8179
+ filters.project_id = resolveId(project_id, "projects");
8180
+ if (task_list_id)
8181
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
8182
+ const tasks = getTasksChangedSince(since, Object.keys(filters).length > 0 ? filters : undefined);
8183
+ if (tasks.length === 0) {
8184
+ return { content: [{ type: "text", text: `No tasks changed since ${since}.` }] };
8185
+ }
8186
+ const text = tasks.map((t) => {
8187
+ const assigned = t.assigned_to ? ` -> ${t.assigned_to}` : "";
8188
+ return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned} (updated: ${t.updated_at})`;
8189
+ }).join(`
8190
+ `);
8191
+ return { content: [{ type: "text", text: `${tasks.length} task(s) changed since ${since}:
8192
+ ${text}` }] };
8193
+ } catch (e) {
8194
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8195
+ }
8196
+ });
8197
+ server.tool("claim_next_task", "Atomically claim, lock, and start the best pending task.", {
8198
+ agent_id: exports_external.string(),
8199
+ project_id: exports_external.string().optional(),
8200
+ task_list_id: exports_external.string().optional(),
8201
+ plan_id: exports_external.string().optional(),
8202
+ tags: exports_external.array(exports_external.string()).optional()
8203
+ }, async ({ agent_id, project_id, task_list_id, plan_id, tags }) => {
8204
+ try {
8205
+ const filters = {};
8206
+ if (project_id)
8207
+ filters.project_id = resolveId(project_id, "projects");
8208
+ if (task_list_id)
8209
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
8210
+ if (plan_id)
8211
+ filters.plan_id = resolveId(plan_id, "plans");
8212
+ if (tags)
8213
+ filters.tags = tags;
8214
+ const task = claimNextTask(agent_id, Object.keys(filters).length > 0 ? filters : undefined);
8215
+ if (!task) {
8216
+ return { content: [{ type: "text", text: "No tasks available to claim." }] };
8217
+ }
8218
+ return { content: [{ type: "text", text: `claimed: ${formatTask(task)}
8219
+ ${formatTaskDetail(task)}` }] };
8220
+ } catch (e) {
8221
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8222
+ }
8223
+ });
8224
+ server.tool("get_stale_tasks", "Find stale in_progress tasks with no recent activity.", {
8225
+ stale_minutes: exports_external.number().optional(),
8226
+ project_id: exports_external.string().optional(),
8227
+ task_list_id: exports_external.string().optional()
8228
+ }, async ({ stale_minutes, project_id, task_list_id }) => {
8229
+ try {
8230
+ const filters = {};
8231
+ if (project_id)
8232
+ filters.project_id = resolveId(project_id, "projects");
8233
+ if (task_list_id)
8234
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
8235
+ const tasks = getStaleTasks(stale_minutes || 30, Object.keys(filters).length > 0 ? filters : undefined);
8236
+ if (tasks.length === 0) {
8237
+ return { content: [{ type: "text", text: "No stale tasks found." }] };
8238
+ }
8239
+ const text = tasks.map((t) => {
8240
+ const id = t.short_id || t.id.slice(0, 8);
8241
+ const agent = t.locked_by || t.assigned_to || "unknown";
8242
+ const staleFor = Math.round((Date.now() - new Date(t.updated_at).getTime()) / 60000);
8243
+ return `${id} | ${agent} | ${t.title} (stale ${staleFor}min)`;
8244
+ }).join(`
8245
+ `);
8246
+ return { content: [{ type: "text", text: `${tasks.length} stale task(s):
8247
+ ${text}` }] };
8248
+ } catch (e) {
8249
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8250
+ }
8251
+ });
8252
+ server.tool("get_status", "Get a full project health snapshot \u2014 counts, active work, next task, stale/overdue summary.", {
8253
+ agent_id: exports_external.string().optional(),
8254
+ project_id: exports_external.string().optional(),
8255
+ task_list_id: exports_external.string().optional()
8256
+ }, async ({ agent_id, project_id, task_list_id }) => {
8257
+ try {
8258
+ const filters = {};
8259
+ if (project_id)
8260
+ filters.project_id = resolveId(project_id, "projects");
8261
+ if (task_list_id)
8262
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
8263
+ const status = getStatus(Object.keys(filters).length > 0 ? filters : undefined, agent_id);
8264
+ const lines = [
8265
+ `Tasks: ${status.pending} pending | ${status.in_progress} active | ${status.completed} done | ${status.total} total`
8266
+ ];
8267
+ if (status.stale_count > 0)
8268
+ lines.push(`\u26A0\uFE0F ${status.stale_count} stale (stuck in_progress)`);
8269
+ if (status.overdue_recurring > 0)
8270
+ lines.push(`\uD83D\uDD01 ${status.overdue_recurring} overdue recurring`);
8271
+ if (status.active_work.length > 0) {
8272
+ lines.push(`
8273
+ Active (${status.active_work.length}):`);
8274
+ for (const w of status.active_work.slice(0, 5)) {
8275
+ const id = w.short_id || w.id.slice(0, 8);
8276
+ lines.push(` ${id} | ${w.assigned_to || w.locked_by || "?"} | ${w.title}`);
8277
+ }
8278
+ }
8279
+ if (status.next_task) {
8280
+ lines.push(`
8281
+ Next up:`);
8282
+ lines.push(` ${formatTask(status.next_task)}`);
8283
+ } else {
8284
+ lines.push(`
8285
+ No pending tasks available.`);
8286
+ }
8287
+ return { content: [{ type: "text", text: lines.join(`
8288
+ `) }] };
8289
+ } catch (e) {
8290
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8291
+ }
8292
+ });
8293
+ server.tool("search_tools", "List all tool names, optionally filtered by substring.", { query: exports_external.string().optional() }, async ({ query }) => {
7204
8294
  const all = [
7205
8295
  "create_task",
7206
8296
  "list_tasks",
@@ -7209,6 +8299,7 @@ server.tool("search_tools", "List tool names matching a query.", { query: export
7209
8299
  "delete_task",
7210
8300
  "start_task",
7211
8301
  "complete_task",
8302
+ "fail_task",
7212
8303
  "lock_task",
7213
8304
  "unlock_task",
7214
8305
  "approve_task",
@@ -7225,7 +8316,11 @@ server.tool("search_tools", "List tool names matching a query.", { query: export
7225
8316
  "register_agent",
7226
8317
  "list_agents",
7227
8318
  "get_agent",
8319
+ "rename_agent",
8320
+ "delete_agent",
7228
8321
  "get_my_tasks",
8322
+ "get_org_chart",
8323
+ "set_reports_to",
7229
8324
  "create_task_list",
7230
8325
  "list_task_lists",
7231
8326
  "get_task_list",
@@ -7233,6 +8328,10 @@ server.tool("search_tools", "List tool names matching a query.", { query: export
7233
8328
  "delete_task_list",
7234
8329
  "search_tasks",
7235
8330
  "sync",
8331
+ "clone_task",
8332
+ "move_task",
8333
+ "get_next_task",
8334
+ "claim_next_task",
7236
8335
  "get_task_history",
7237
8336
  "get_recent_activity",
7238
8337
  "create_webhook",
@@ -7242,6 +8341,14 @@ server.tool("search_tools", "List tool names matching a query.", { query: export
7242
8341
  "list_templates",
7243
8342
  "create_task_from_template",
7244
8343
  "delete_template",
8344
+ "bulk_update_tasks",
8345
+ "bulk_create_tasks",
8346
+ "get_task_stats",
8347
+ "get_task_graph",
8348
+ "get_active_work",
8349
+ "get_tasks_changed_since",
8350
+ "get_stale_tasks",
8351
+ "get_status",
7245
8352
  "search_tools",
7246
8353
  "describe_tools"
7247
8354
  ];
@@ -7249,27 +8356,178 @@ server.tool("search_tools", "List tool names matching a query.", { query: export
7249
8356
  const matches = q ? all.filter((n) => n.includes(q)) : all;
7250
8357
  return { content: [{ type: "text", text: matches.join(", ") }] };
7251
8358
  });
7252
- server.tool("describe_tools", "Get descriptions for specific tools by name.", { names: exports_external.array(exports_external.string()) }, async ({ names }) => {
8359
+ server.tool("describe_tools", "Get detailed parameter info for specific tools by name.", { names: exports_external.array(exports_external.string()) }, async ({ names }) => {
7253
8360
  const descriptions = {
7254
- create_task: "Create a task. Params: title(req), description, priority, project_id, plan_id, tags, assigned_to, estimated_minutes, requires_approval",
7255
- list_tasks: "List tasks. Params: status, priority, project_id, plan_id, assigned_to, tags, limit",
7256
- get_task: "Get full task details. Params: id",
7257
- update_task: "Update task fields. Params: id, version(req), title, description, status, priority, tags, assigned_to, due_at",
7258
- delete_task: "Delete a task. Params: id",
7259
- start_task: "Claim, lock, and start a task. Params: id",
7260
- complete_task: "Mark task completed. Params: id, agent_id",
7261
- approve_task: "Approve task requiring approval. Params: id, agent_id",
7262
- create_plan: "Create a plan. Params: name, description, project_id, task_list_id, agent_id, status",
7263
- list_plans: "List plans. Params: project_id",
7264
- get_plan: "Get plan with tasks. Params: id",
7265
- search_tasks: "Full-text search tasks. Params: query, project_id, task_list_id",
7266
- get_my_tasks: "Get your tasks and stats. Params: agent_name",
7267
- get_task_history: "Get task audit log. Params: task_id",
7268
- get_recent_activity: "Recent changes across all tasks. Params: limit",
7269
- create_template: "Create task template. Params: name, title_pattern, description, priority, tags",
7270
- create_task_from_template: "Create task from template. Params: template_id, title, priority, assigned_to"
8361
+ create_task: `Create a new task.
8362
+ 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)
8363
+ Example: {title: 'Daily standup', recurrence_rule: 'every weekday', priority: 'medium'}`,
8364
+ list_tasks: `List tasks with optional filters. Supports pagination.
8365
+ 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)
8366
+ Example: {status: ['pending', 'in_progress'], has_recurrence: true, limit: 20}`,
8367
+ get_task: `Get full task details with subtasks, deps, and comments.
8368
+ Params: id(string, req \u2014 task ID, short_id like 'APP-00001', or partial ID)
8369
+ Example: {id: 'a1b2c3d4'}`,
8370
+ update_task: `Update task fields. Requires version for optimistic locking (get it from get_task first).
8371
+ Params: id(string, req), version(number, req), title(string), description(string), status(pending|in_progress|completed|failed|cancelled), priority(low|medium|high|critical), assigned_to(string), tags(string[]), metadata(object), plan_id(string), task_list_id(string)
8372
+ Example: {id: 'a1b2c3d4', version: 3, status: 'completed'}`,
8373
+ delete_task: `Delete a task permanently. Subtasks cascade-delete. Dependencies removed.
8374
+ Params: id(string, req)
8375
+ Example: {id: 'a1b2c3d4'}`,
8376
+ start_task: `Claim, lock, and set task status to in_progress in one call.
8377
+ Params: id(string, req), agent_id(string, req \u2014 your 8-char agent ID)
8378
+ Example: {id: 'a1b2c3d4', agent_id: 'e5f6g7h8'}`,
8379
+ complete_task: `Mark task completed, release lock, set completed_at timestamp. For recurring tasks, auto-spawns next instance unless skip_recurrence is true.
8380
+ Params: id(string, req), agent_id(string, optional \u2014 required if locked by different agent), skip_recurrence(boolean \u2014 set true to prevent auto-creating next recurring instance)
8381
+ Example: {id: 'a1b2c3d4', skip_recurrence: false}`,
8382
+ lock_task: `Acquire exclusive lock on a task. Locks auto-expire after 30 min. Re-locking by same agent is idempotent.
8383
+ Params: id(string, req), agent_id(string, req)
8384
+ Example: {id: 'a1b2c3d4', agent_id: 'e5f6g7h8'}`,
8385
+ unlock_task: `Release exclusive lock on a task.
8386
+ Params: id(string, req), agent_id(string, optional \u2014 omit to force-unlock)
8387
+ Example: {id: 'a1b2c3d4', agent_id: 'e5f6g7h8'}`,
8388
+ approve_task: `Approve a task with requires_approval=true. Must be approved before completion.
8389
+ Params: id(string, req), agent_id(string, optional \u2014 defaults to 'system')
8390
+ Example: {id: 'a1b2c3d4', agent_id: 'e5f6g7h8'}`,
8391
+ fail_task: `Mark a task as failed with structured reason and optional auto-retry. Stores failure info in metadata._failure, releases lock.
8392
+ Params: id(string, req), agent_id(string, optional), reason(string), error_code(string \u2014 e.g. 'TIMEOUT'), retry(boolean \u2014 create new pending copy), retry_after(ISO date)
8393
+ Example: {id: 'a1b2c3d4', reason: 'Build timeout', error_code: 'TIMEOUT', retry: true}`,
8394
+ add_dependency: `Add a dependency: task_id depends on depends_on. Prevents cycles via BFS.
8395
+ Params: task_id(string, req), depends_on(string, req)
8396
+ Example: {task_id: 'abc12345', depends_on: 'def67890'}`,
8397
+ remove_dependency: `Remove a dependency link between two tasks.
8398
+ Params: task_id(string, req), depends_on(string, req)
8399
+ Example: {task_id: 'abc12345', depends_on: 'def67890'}`,
8400
+ add_comment: `Add a comment/note to a task. Comments are append-only.
8401
+ Params: task_id(string, req), content(string, req), agent_id(string), session_id(string)
8402
+ Example: {task_id: 'a1b2c3d4', content: 'Blocked by API rate limit'}`,
8403
+ create_project: `Register a new project. Auto-generates task prefix for short IDs (e.g. APP-00001).
8404
+ Params: name(string, req), path(string, req \u2014 unique absolute path), description(string), task_list_id(string)
8405
+ Example: {name: 'my-app', path: '/Users/dev/my-app'}`,
8406
+ list_projects: "List all registered projects. No params.",
8407
+ create_plan: `Create a plan to group related tasks.
8408
+ Params: name(string, req), project_id(string), description(string), status(active|completed|archived, default:active), task_list_id(string), agent_id(string)
8409
+ Example: {name: 'Sprint 1', project_id: 'a1b2c3d4'}`,
8410
+ list_plans: `List all plans, optionally filtered by project.
8411
+ Params: project_id(string)
8412
+ Example: {project_id: 'a1b2c3d4'}`,
8413
+ get_plan: `Get plan details (name, status, description, timestamps).
8414
+ Params: id(string, req)
8415
+ Example: {id: 'a1b2c3d4'}`,
8416
+ update_plan: `Update plan fields.
8417
+ Params: id(string, req), name(string), description(string), status(active|completed|archived), task_list_id(string), agent_id(string)
8418
+ Example: {id: 'a1b2c3d4', status: 'completed'}`,
8419
+ delete_plan: `Delete a plan. Tasks in the plan are orphaned, not deleted.
8420
+ Params: id(string, req)
8421
+ Example: {id: 'a1b2c3d4'}`,
8422
+ register_agent: `Register an agent (idempotent by name). Returns existing agent if name matches.
8423
+ Params: name(string, req \u2014 e.g. 'maximus'), description(string)
8424
+ Example: {name: 'maximus', description: 'Backend developer'}`,
8425
+ list_agents: "List all registered agents with IDs, names, and last seen timestamps. No params.",
8426
+ get_agent: `Get agent details by ID or name. Provide one of id or name.
8427
+ Params: id(string), name(string)
8428
+ Example: {name: 'maximus'}`,
8429
+ rename_agent: `Rename an agent. Resolve by id or current name.
8430
+ Params: id(string), name(string \u2014 current name), new_name(string, req)
8431
+ Example: {name: 'old-name', new_name: 'new-name'}`,
8432
+ delete_agent: `Delete an agent permanently. Resolve by id or name.
8433
+ Params: id(string), name(string)
8434
+ Example: {name: 'maximus'}`,
8435
+ get_my_tasks: `Get all tasks assigned to/created by an agent, with stats (pending/active/done/rate).
8436
+ Params: agent_name(string, req)
8437
+ Example: {agent_name: 'maximus'}`,
8438
+ get_org_chart: "Get agent org chart showing reporting hierarchy. No params.",
8439
+ set_reports_to: `Set who an agent reports to in the org chart. Omit manager_name for top-level.
8440
+ Params: agent_name(string, req), manager_name(string, optional)
8441
+ Example: {agent_name: 'brutus', manager_name: 'maximus'}`,
8442
+ create_task_list: `Create a task list \u2014 a container/folder for organizing tasks.
8443
+ Params: name(string, req), slug(string \u2014 auto-generated if omitted), project_id(string), description(string)
8444
+ Example: {name: 'Sprint 1', project_id: 'a1b2c3d4'}`,
8445
+ list_task_lists: `List all task lists, optionally filtered by project.
8446
+ Params: project_id(string)
8447
+ Example: {project_id: 'a1b2c3d4'}`,
8448
+ get_task_list: `Get task list details (name, slug, project, metadata).
8449
+ Params: id(string, req)
8450
+ Example: {id: 'a1b2c3d4'}`,
8451
+ update_task_list: `Update a task list's name or description.
8452
+ Params: id(string, req), name(string), description(string)
8453
+ Example: {id: 'a1b2c3d4', name: 'Sprint 2'}`,
8454
+ delete_task_list: `Delete a task list. Tasks are orphaned (not deleted).
8455
+ Params: id(string, req)
8456
+ Example: {id: 'a1b2c3d4'}`,
8457
+ search_tasks: `Full-text search across task titles, descriptions, and tags. Supports filters.
8458
+ Params: query(string, req), project_id(string), task_list_id(string), status(string|string[]), priority(string|string[]), assigned_to(string), agent_id(string), created_after(ISO date), updated_after(ISO date), has_dependencies(boolean), is_blocked(boolean)
8459
+ Example: {query: 'auth bug', status: 'pending'}`,
8460
+ get_next_task: `Get the optimal next task to work on \u2014 finds highest-priority pending task that is not blocked or locked. Prefers tasks assigned to the given agent.
8461
+ Params: agent_id(string \u2014 prefers your tasks), project_id(string), task_list_id(string), plan_id(string), tags(string[])
8462
+ Example: {agent_id: 'a1b2c3d4', project_id: 'e5f6g7h8'}`,
8463
+ claim_next_task: `Atomically find the best pending task, lock it, and start it \u2014 one call instead of get_next_task + start_task. Eliminates race conditions between agents.
8464
+ Params: agent_id(string, req \u2014 used for lock and assignment), project_id(string), task_list_id(string), plan_id(string), tags(string[])
8465
+ Example: {agent_id: 'a1b2c3d4', project_id: 'e5f6g7h8'}`,
8466
+ sync: `Sync tasks between local DB and agent task list (e.g. Claude Code).
8467
+ Params: agent(string, default:'claude'), task_list_id(string), all_agents(boolean), project_id(string), direction(push|pull|both, default:both), prefer(local|remote, default:remote)
8468
+ Example: {agent: 'claude', direction: 'push'}`,
8469
+ clone_task: `Duplicate a task with optional field overrides. Creates new independent copy.
8470
+ Params: task_id(string, req), title(string), description(string), priority(low|medium|high|critical), status(pending|in_progress|completed|failed|cancelled), project_id(string), plan_id(string), task_list_id(string), assigned_to(string), tags(string[]), estimated_minutes(number)
8471
+ Example: {task_id: 'a1b2c3d4', title: 'Cloned task', assigned_to: 'brutus'}`,
8472
+ move_task: `Move a task to a different list, project, or plan.
8473
+ Params: task_id(string, req), task_list_id(string|null), project_id(string|null), plan_id(string|null)
8474
+ Example: {task_id: 'a1b2c3d4', task_list_id: 'e5f6g7h8'}`,
8475
+ bulk_update_tasks: `Update multiple tasks at once with the same changes.
8476
+ Params: task_ids(string[], req), status(pending|in_progress|completed|failed|cancelled), priority(low|medium|high|critical), assigned_to(string), tags(string[])
8477
+ Example: {task_ids: ['abc12345', 'def67890'], status: 'completed'}`,
8478
+ bulk_create_tasks: `Create multiple tasks atomically. Supports inter-task dependencies via temp_id references.
8479
+ Params: tasks(array, req \u2014 [{temp_id, title, description, priority, status, project_id, plan_id, task_list_id, agent_id, assigned_to, tags, estimated_minutes, depends_on_temp_ids}]), project_id(string \u2014 default for all), plan_id(string \u2014 default for all), task_list_id(string \u2014 default for all)
8480
+ Example: {tasks: [{temp_id: 'a', title: 'First'}, {temp_id: 'b', title: 'Second', depends_on_temp_ids: ['a']}]}`,
8481
+ get_task_stats: `Get task analytics: counts by status, priority, agent, and completion rate. All via SQL.
8482
+ Params: project_id(string), task_list_id(string), agent_id(string)
8483
+ Example: {project_id: 'a1b2c3d4'}`,
8484
+ get_task_graph: `Get full dependency tree for a task \u2014 upstream blockers and downstream dependents.
8485
+ Params: id(string, req), direction(up|down|both, default:both)
8486
+ Example: {id: 'a1b2c3d4', direction: 'up'}`,
8487
+ get_task_history: `Get audit log for a task \u2014 all field changes with timestamps and actors.
8488
+ Params: task_id(string, req)
8489
+ Example: {task_id: 'a1b2c3d4'}`,
8490
+ get_recent_activity: `Get recent task changes across all tasks \u2014 global activity feed.
8491
+ Params: limit(number, default:50)
8492
+ Example: {limit: 20}`,
8493
+ create_webhook: `Register a webhook for task change events.
8494
+ Params: url(string, req), events(string[] \u2014 empty=all), secret(string \u2014 HMAC signing)
8495
+ Example: {url: 'https://example.com/hook', events: ['task.created', 'task.completed']}`,
8496
+ list_webhooks: "List all registered webhooks. No params.",
8497
+ delete_webhook: `Delete a webhook by ID.
8498
+ Params: id(string, req)
8499
+ Example: {id: 'a1b2c3d4'}`,
8500
+ create_template: `Create a reusable task template.
8501
+ Params: name(string, req), title_pattern(string, req \u2014 e.g. 'Fix: {description}'), description(string), priority(low|medium|high|critical), tags(string[]), project_id(string), plan_id(string)
8502
+ Example: {name: 'Bug Report', title_pattern: 'Bug: {description}', priority: 'high', tags: ['bug']}`,
8503
+ list_templates: "List all task templates. No params.",
8504
+ create_task_from_template: `Create a task from a template with optional overrides.
8505
+ Params: template_id(string, req), title(string), description(string), priority(low|medium|high|critical), assigned_to(string), project_id(string)
8506
+ Example: {template_id: 'a1b2c3d4', assigned_to: 'maximus'}`,
8507
+ delete_template: `Delete a task template.
8508
+ Params: id(string, req)
8509
+ Example: {id: 'a1b2c3d4'}`,
8510
+ get_active_work: `See all in-progress tasks and who is working on them.
8511
+ Params: project_id(string, optional), task_list_id(string, optional)
8512
+ Example: {project_id: 'a1b2c3d4'}`,
8513
+ get_tasks_changed_since: `Get tasks modified after a timestamp \u2014 incremental delta sync.
8514
+ Params: since(string, req \u2014 ISO date), project_id(string, optional), task_list_id(string, optional)
8515
+ Example: {since: '2026-03-14T10:00:00Z'}`,
8516
+ get_stale_tasks: `Find stale in_progress tasks with no recent activity.
8517
+ Params: stale_minutes(number, default:30), project_id(string, optional), task_list_id(string, optional)
8518
+ Example: {stale_minutes: 60, project_id: 'a1b2c3d4'}`,
8519
+ 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.
8520
+ Params: agent_id(string, optional \u2014 prefers tasks assigned to this agent for next_task), project_id(string, optional), task_list_id(string, optional)
8521
+ Example: {agent_id: 'a1b2c3d4', project_id: 'e5f6g7h8'}`,
8522
+ search_tools: `List all tool names or filter by substring.
8523
+ Params: query(string, optional)
8524
+ Example: {query: 'task'}`,
8525
+ describe_tools: `Get detailed descriptions and parameter info for tools by name.
8526
+ Params: names(string[], req)
8527
+ Example: {names: ['create_task', 'update_task']}`
7271
8528
  };
7272
- const result = names.map((n) => `${n}: ${descriptions[n] || "See tool schema"}`).join(`
8529
+ const result = names.map((n) => `${n}: ${descriptions[n] || "Unknown tool. Use search_tools to list available tools."}`).join(`
8530
+
7273
8531
  `);
7274
8532
  return { content: [{ type: "text", text: result }] };
7275
8533
  });