@hasna/todos 0.9.34 → 0.9.35

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
  });
@@ -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,95 @@ 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
5077
5215
  function rowToTask(row) {
5078
5216
  return {
5079
5217
  ...row,
@@ -5104,8 +5242,8 @@ function createTask(input, db) {
5104
5242
  const tags = input.tags || [];
5105
5243
  const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
5106
5244
  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, ?, ?, ?, ?, ?, ?, ?)`, [
5245
+ 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)
5246
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5109
5247
  id,
5110
5248
  shortId,
5111
5249
  input.project_id || null,
@@ -5128,7 +5266,9 @@ function createTask(input, db) {
5128
5266
  input.estimated_minutes || null,
5129
5267
  input.requires_approval ? 1 : 0,
5130
5268
  null,
5131
- null
5269
+ null,
5270
+ input.recurrence_rule || null,
5271
+ input.recurrence_parent_id || null
5132
5272
  ]);
5133
5273
  if (tags.length > 0) {
5134
5274
  insertTaskTags(id, tags, d);
@@ -5228,6 +5368,11 @@ function listTasks(filter = {}, db) {
5228
5368
  conditions.push("task_list_id = ?");
5229
5369
  params.push(filter.task_list_id);
5230
5370
  }
5371
+ if (filter.has_recurrence === true) {
5372
+ conditions.push("recurrence_rule IS NOT NULL");
5373
+ } else if (filter.has_recurrence === false) {
5374
+ conditions.push("recurrence_rule IS NULL");
5375
+ }
5231
5376
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5232
5377
  let limitClause = "";
5233
5378
  if (filter.limit) {
@@ -5243,6 +5388,69 @@ function listTasks(filter = {}, db) {
5243
5388
  created_at DESC${limitClause}`).all(...params);
5244
5389
  return rows.map(rowToTask);
5245
5390
  }
5391
+ function countTasks(filter = {}, db) {
5392
+ const d = db || getDatabase();
5393
+ const conditions = [];
5394
+ const params = [];
5395
+ if (filter.project_id) {
5396
+ conditions.push("project_id = ?");
5397
+ params.push(filter.project_id);
5398
+ }
5399
+ if (filter.parent_id !== undefined) {
5400
+ if (filter.parent_id === null) {
5401
+ conditions.push("parent_id IS NULL");
5402
+ } else {
5403
+ conditions.push("parent_id = ?");
5404
+ params.push(filter.parent_id);
5405
+ }
5406
+ }
5407
+ if (filter.status) {
5408
+ if (Array.isArray(filter.status)) {
5409
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
5410
+ params.push(...filter.status);
5411
+ } else {
5412
+ conditions.push("status = ?");
5413
+ params.push(filter.status);
5414
+ }
5415
+ }
5416
+ if (filter.priority) {
5417
+ if (Array.isArray(filter.priority)) {
5418
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
5419
+ params.push(...filter.priority);
5420
+ } else {
5421
+ conditions.push("priority = ?");
5422
+ params.push(filter.priority);
5423
+ }
5424
+ }
5425
+ if (filter.assigned_to) {
5426
+ conditions.push("assigned_to = ?");
5427
+ params.push(filter.assigned_to);
5428
+ }
5429
+ if (filter.agent_id) {
5430
+ conditions.push("agent_id = ?");
5431
+ params.push(filter.agent_id);
5432
+ }
5433
+ if (filter.session_id) {
5434
+ conditions.push("session_id = ?");
5435
+ params.push(filter.session_id);
5436
+ }
5437
+ if (filter.tags && filter.tags.length > 0) {
5438
+ const placeholders = filter.tags.map(() => "?").join(",");
5439
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
5440
+ params.push(...filter.tags);
5441
+ }
5442
+ if (filter.plan_id) {
5443
+ conditions.push("plan_id = ?");
5444
+ params.push(filter.plan_id);
5445
+ }
5446
+ if (filter.task_list_id) {
5447
+ conditions.push("task_list_id = ?");
5448
+ params.push(filter.task_list_id);
5449
+ }
5450
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5451
+ const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
5452
+ return row.count;
5453
+ }
5246
5454
  function updateTask(id, input, db) {
5247
5455
  const d = db || getDatabase();
5248
5456
  const task = getTask(id, d);
@@ -5314,6 +5522,10 @@ function updateTask(id, input, db) {
5314
5522
  sets.push("approved_at = ?");
5315
5523
  params.push(now());
5316
5524
  }
5525
+ if (input.recurrence_rule !== undefined) {
5526
+ sets.push("recurrence_rule = ?");
5527
+ params.push(input.recurrence_rule);
5528
+ }
5317
5529
  params.push(id, input.version);
5318
5530
  const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
5319
5531
  if (result.changes === 0) {
@@ -5387,7 +5599,7 @@ function startTask(id, agentId, db) {
5387
5599
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
5388
5600
  return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
5389
5601
  }
5390
- function completeTask(id, agentId, db, evidence) {
5602
+ function completeTask(id, agentId, db, options) {
5391
5603
  const d = db || getDatabase();
5392
5604
  const task = getTask(id, d);
5393
5605
  if (!task)
@@ -5396,7 +5608,9 @@ function completeTask(id, agentId, db, evidence) {
5396
5608
  throw new LockError(id, task.locked_by);
5397
5609
  }
5398
5610
  checkCompletionGuard(task, agentId || null, d);
5399
- if (evidence) {
5611
+ const evidence = options ? { files_changed: options.files_changed, test_results: options.test_results, commit_hash: options.commit_hash, notes: options.notes } : undefined;
5612
+ const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes);
5613
+ if (hasEvidence) {
5400
5614
  const meta2 = { ...task.metadata, _evidence: evidence };
5401
5615
  d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
5402
5616
  }
@@ -5404,7 +5618,14 @@ function completeTask(id, agentId, db, evidence) {
5404
5618
  d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
5405
5619
  WHERE id = ?`, [timestamp, timestamp, id]);
5406
5620
  logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
5407
- const meta = evidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
5621
+ let spawnedTask = null;
5622
+ if (task.recurrence_rule && !options?.skip_recurrence) {
5623
+ spawnedTask = spawnNextRecurrence(task, d);
5624
+ }
5625
+ const meta = hasEvidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
5626
+ if (spawnedTask) {
5627
+ meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
5628
+ }
5408
5629
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
5409
5630
  }
5410
5631
  function lockTask(id, agentId, db) {
@@ -5467,6 +5688,266 @@ function getTaskDependencies(taskId, db) {
5467
5688
  const d = db || getDatabase();
5468
5689
  return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
5469
5690
  }
5691
+ function cloneTask(taskId, overrides, db) {
5692
+ const d = db || getDatabase();
5693
+ const source = getTask(taskId, d);
5694
+ if (!source)
5695
+ throw new TaskNotFoundError(taskId);
5696
+ const input = {
5697
+ title: overrides?.title ?? source.title,
5698
+ description: overrides?.description ?? source.description ?? undefined,
5699
+ priority: overrides?.priority ?? source.priority,
5700
+ project_id: overrides?.project_id ?? source.project_id ?? undefined,
5701
+ parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
5702
+ plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
5703
+ task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
5704
+ status: overrides?.status ?? "pending",
5705
+ agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
5706
+ assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
5707
+ tags: overrides?.tags ?? source.tags,
5708
+ metadata: overrides?.metadata ?? source.metadata,
5709
+ estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
5710
+ recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
5711
+ };
5712
+ return createTask(input, d);
5713
+ }
5714
+ function getTaskGraph(taskId, direction = "both", db) {
5715
+ const d = db || getDatabase();
5716
+ const task = getTask(taskId, d);
5717
+ if (!task)
5718
+ throw new TaskNotFoundError(taskId);
5719
+ function toNode(t) {
5720
+ const deps = getTaskDependencies(t.id, d);
5721
+ const hasUnfinishedDeps = deps.some((dep) => {
5722
+ const depTask = getTask(dep.depends_on, d);
5723
+ return depTask && depTask.status !== "completed";
5724
+ });
5725
+ return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
5726
+ }
5727
+ function buildUp(id, visited) {
5728
+ if (visited.has(id))
5729
+ return [];
5730
+ visited.add(id);
5731
+ const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
5732
+ return deps.map((dep) => {
5733
+ const depTask = getTask(dep.depends_on, d);
5734
+ if (!depTask)
5735
+ return null;
5736
+ return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
5737
+ }).filter(Boolean);
5738
+ }
5739
+ function buildDown(id, visited) {
5740
+ if (visited.has(id))
5741
+ return [];
5742
+ visited.add(id);
5743
+ const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
5744
+ return dependents.map((dep) => {
5745
+ const depTask = getTask(dep.task_id, d);
5746
+ if (!depTask)
5747
+ return null;
5748
+ return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
5749
+ }).filter(Boolean);
5750
+ }
5751
+ const rootNode = toNode(task);
5752
+ const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
5753
+ const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
5754
+ return { task: rootNode, depends_on, blocks };
5755
+ }
5756
+ function moveTask(taskId, target, db) {
5757
+ const d = db || getDatabase();
5758
+ const task = getTask(taskId, d);
5759
+ if (!task)
5760
+ throw new TaskNotFoundError(taskId);
5761
+ const sets = ["updated_at = ?", "version = version + 1"];
5762
+ const params = [now()];
5763
+ if (target.task_list_id !== undefined) {
5764
+ sets.push("task_list_id = ?");
5765
+ params.push(target.task_list_id);
5766
+ }
5767
+ if (target.project_id !== undefined) {
5768
+ sets.push("project_id = ?");
5769
+ params.push(target.project_id);
5770
+ }
5771
+ if (target.plan_id !== undefined) {
5772
+ sets.push("plan_id = ?");
5773
+ params.push(target.plan_id);
5774
+ }
5775
+ params.push(taskId);
5776
+ d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
5777
+ return getTask(taskId, d);
5778
+ }
5779
+ function spawnNextRecurrence(completedTask, db) {
5780
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
5781
+ let title = completedTask.title;
5782
+ if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
5783
+ title = title.slice(completedTask.short_id.length + 2);
5784
+ }
5785
+ const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
5786
+ return createTask({
5787
+ title,
5788
+ description: completedTask.description ?? undefined,
5789
+ priority: completedTask.priority,
5790
+ project_id: completedTask.project_id ?? undefined,
5791
+ task_list_id: completedTask.task_list_id ?? undefined,
5792
+ plan_id: completedTask.plan_id ?? undefined,
5793
+ assigned_to: completedTask.assigned_to ?? undefined,
5794
+ tags: completedTask.tags,
5795
+ metadata: completedTask.metadata,
5796
+ estimated_minutes: completedTask.estimated_minutes ?? undefined,
5797
+ recurrence_rule: completedTask.recurrence_rule,
5798
+ recurrence_parent_id: recurrenceParentId,
5799
+ due_at: dueAt
5800
+ }, db);
5801
+ }
5802
+ function claimNextTask(agentId, filters, db) {
5803
+ const d = db || getDatabase();
5804
+ const tx = d.transaction(() => {
5805
+ const task = getNextTask(agentId, filters, d);
5806
+ if (!task)
5807
+ return null;
5808
+ return startTask(task.id, agentId, d);
5809
+ });
5810
+ return tx();
5811
+ }
5812
+ function getNextTask(agentId, filters, db) {
5813
+ const d = db || getDatabase();
5814
+ clearExpiredLocks(d);
5815
+ const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
5816
+ const params = [lockExpiryCutoff()];
5817
+ if (filters?.project_id) {
5818
+ conditions.push("project_id = ?");
5819
+ params.push(filters.project_id);
5820
+ }
5821
+ if (filters?.task_list_id) {
5822
+ conditions.push("task_list_id = ?");
5823
+ params.push(filters.task_list_id);
5824
+ }
5825
+ if (filters?.plan_id) {
5826
+ conditions.push("plan_id = ?");
5827
+ params.push(filters.plan_id);
5828
+ }
5829
+ if (filters?.tags && filters.tags.length > 0) {
5830
+ const placeholders = filters.tags.map(() => "?").join(",");
5831
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
5832
+ params.push(...filters.tags);
5833
+ }
5834
+ 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')");
5835
+ const where = conditions.join(" AND ");
5836
+ let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
5837
+ if (agentId) {
5838
+ sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
5839
+ params.push(agentId);
5840
+ }
5841
+ 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`;
5842
+ const row = d.query(sql).get(...params);
5843
+ return row ? rowToTask(row) : null;
5844
+ }
5845
+ function getActiveWork(filters, db) {
5846
+ const d = db || getDatabase();
5847
+ clearExpiredLocks(d);
5848
+ const conditions = ["status = 'in_progress'"];
5849
+ const params = [];
5850
+ if (filters?.project_id) {
5851
+ conditions.push("project_id = ?");
5852
+ params.push(filters.project_id);
5853
+ }
5854
+ if (filters?.task_list_id) {
5855
+ conditions.push("task_list_id = ?");
5856
+ params.push(filters.task_list_id);
5857
+ }
5858
+ const where = conditions.join(" AND ");
5859
+ const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
5860
+ CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
5861
+ updated_at DESC`).all(...params);
5862
+ return rows;
5863
+ }
5864
+ function getTasksChangedSince(since, filters, db) {
5865
+ const d = db || getDatabase();
5866
+ const conditions = ["updated_at > ?"];
5867
+ const params = [since];
5868
+ if (filters?.project_id) {
5869
+ conditions.push("project_id = ?");
5870
+ params.push(filters.project_id);
5871
+ }
5872
+ if (filters?.task_list_id) {
5873
+ conditions.push("task_list_id = ?");
5874
+ params.push(filters.task_list_id);
5875
+ }
5876
+ const where = conditions.join(" AND ");
5877
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
5878
+ return rows.map(rowToTask);
5879
+ }
5880
+ function failTask(id, agentId, reason, options, db) {
5881
+ const d = db || getDatabase();
5882
+ const task = getTask(id, d);
5883
+ if (!task)
5884
+ throw new TaskNotFoundError(id);
5885
+ const meta = {
5886
+ ...task.metadata,
5887
+ _failure: {
5888
+ reason: reason || "Unknown failure",
5889
+ error_code: options?.error_code || null,
5890
+ failed_by: agentId || null,
5891
+ failed_at: now(),
5892
+ retry_requested: options?.retry || false
5893
+ }
5894
+ };
5895
+ const timestamp = now();
5896
+ d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
5897
+ WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
5898
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
5899
+ const failedTask = {
5900
+ ...task,
5901
+ status: "failed",
5902
+ locked_by: null,
5903
+ locked_at: null,
5904
+ metadata: meta,
5905
+ version: task.version + 1,
5906
+ updated_at: timestamp
5907
+ };
5908
+ let retryTask;
5909
+ if (options?.retry) {
5910
+ let title = task.title;
5911
+ if (task.short_id && title.startsWith(task.short_id + ": ")) {
5912
+ title = title.slice(task.short_id.length + 2);
5913
+ }
5914
+ retryTask = createTask({
5915
+ title,
5916
+ description: task.description ?? undefined,
5917
+ priority: task.priority,
5918
+ project_id: task.project_id ?? undefined,
5919
+ task_list_id: task.task_list_id ?? undefined,
5920
+ plan_id: task.plan_id ?? undefined,
5921
+ assigned_to: task.assigned_to ?? undefined,
5922
+ tags: task.tags,
5923
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_after: options.retry_after || null, failure_reason: reason } },
5924
+ estimated_minutes: task.estimated_minutes ?? undefined,
5925
+ recurrence_rule: task.recurrence_rule ?? undefined,
5926
+ due_at: options.retry_after || task.due_at || undefined
5927
+ }, d);
5928
+ }
5929
+ return { task: failedTask, retryTask };
5930
+ }
5931
+ function getStaleTasks(staleMinutes = 30, filters, db) {
5932
+ const d = db || getDatabase();
5933
+ const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
5934
+ const conditions = [
5935
+ "status = 'in_progress'",
5936
+ "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
5937
+ ];
5938
+ const params = [cutoff, cutoff];
5939
+ if (filters?.project_id) {
5940
+ conditions.push("project_id = ?");
5941
+ params.push(filters.project_id);
5942
+ }
5943
+ if (filters?.task_list_id) {
5944
+ conditions.push("task_list_id = ?");
5945
+ params.push(filters.task_list_id);
5946
+ }
5947
+ const where = conditions.join(" AND ");
5948
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
5949
+ return rows.map(rowToTask);
5950
+ }
5470
5951
  function wouldCreateCycle(taskId, dependsOn, db) {
5471
5952
  const visited = new Set;
5472
5953
  const queue = [dependsOn];
@@ -5484,6 +5965,91 @@ function wouldCreateCycle(taskId, dependsOn, db) {
5484
5965
  }
5485
5966
  return false;
5486
5967
  }
5968
+ function getTaskStats(filters, db) {
5969
+ const d = db || getDatabase();
5970
+ const conditions = [];
5971
+ const params = [];
5972
+ if (filters?.project_id) {
5973
+ conditions.push("project_id = ?");
5974
+ params.push(filters.project_id);
5975
+ }
5976
+ if (filters?.task_list_id) {
5977
+ conditions.push("task_list_id = ?");
5978
+ params.push(filters.task_list_id);
5979
+ }
5980
+ if (filters?.agent_id) {
5981
+ conditions.push("(agent_id = ? OR assigned_to = ?)");
5982
+ params.push(filters.agent_id, filters.agent_id);
5983
+ }
5984
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5985
+ const totalRow = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
5986
+ const statusRows = d.query(`SELECT status, COUNT(*) as count FROM tasks ${where} GROUP BY status`).all(...params);
5987
+ const by_status = {};
5988
+ for (const r of statusRows)
5989
+ by_status[r.status] = r.count;
5990
+ const priorityRows = d.query(`SELECT priority, COUNT(*) as count FROM tasks ${where} GROUP BY priority`).all(...params);
5991
+ const by_priority = {};
5992
+ for (const r of priorityRows)
5993
+ by_priority[r.priority] = r.count;
5994
+ const agentRows = d.query(`SELECT COALESCE(assigned_to, agent_id, 'unassigned') as agent, COUNT(*) as count FROM tasks ${where} GROUP BY agent`).all(...params);
5995
+ const by_agent = {};
5996
+ for (const r of agentRows)
5997
+ by_agent[r.agent] = r.count;
5998
+ const completed = by_status["completed"] || 0;
5999
+ const completion_rate = totalRow.count > 0 ? Math.round(completed / totalRow.count * 100) : 0;
6000
+ return { total: totalRow.count, by_status, by_priority, completion_rate, by_agent };
6001
+ }
6002
+ function bulkCreateTasks(inputs, db) {
6003
+ const d = db || getDatabase();
6004
+ const tempIdToRealId = new Map;
6005
+ const created = [];
6006
+ const tx = d.transaction(() => {
6007
+ for (const input of inputs) {
6008
+ const { temp_id, depends_on_temp_ids: _deps, ...createInput } = input;
6009
+ const task = createTask(createInput, d);
6010
+ if (temp_id)
6011
+ tempIdToRealId.set(temp_id, task.id);
6012
+ created.push({ temp_id: temp_id || null, id: task.id, short_id: task.short_id, title: task.title });
6013
+ }
6014
+ for (const input of inputs) {
6015
+ if (input.depends_on_temp_ids && input.depends_on_temp_ids.length > 0) {
6016
+ const taskId = input.temp_id ? tempIdToRealId.get(input.temp_id) : null;
6017
+ if (!taskId)
6018
+ continue;
6019
+ for (const depTempId of input.depends_on_temp_ids) {
6020
+ const depRealId = tempIdToRealId.get(depTempId);
6021
+ if (depRealId) {
6022
+ addDependency(taskId, depRealId, d);
6023
+ }
6024
+ }
6025
+ }
6026
+ }
6027
+ });
6028
+ tx();
6029
+ return { created };
6030
+ }
6031
+ function bulkUpdateTasks(taskIds, updates, db) {
6032
+ const d = db || getDatabase();
6033
+ let updated = 0;
6034
+ const failed = [];
6035
+ const tx = d.transaction(() => {
6036
+ for (const id of taskIds) {
6037
+ try {
6038
+ const task = getTask(id, d);
6039
+ if (!task) {
6040
+ failed.push({ id, error: "Task not found" });
6041
+ continue;
6042
+ }
6043
+ updateTask(id, { ...updates, version: task.version }, d);
6044
+ updated++;
6045
+ } catch (e) {
6046
+ failed.push({ id, error: e instanceof Error ? e.message : String(e) });
6047
+ }
6048
+ }
6049
+ });
6050
+ tx();
6051
+ return { updated, failed };
6052
+ }
5487
6053
 
5488
6054
  // src/db/comments.ts
5489
6055
  init_database();
@@ -5657,19 +6223,64 @@ function rowToTask2(row) {
5657
6223
  requires_approval: Boolean(row.requires_approval)
5658
6224
  };
5659
6225
  }
5660
- function searchTasks(query, projectId, taskListId, db) {
6226
+ function searchTasks(options, projectId, taskListId, db) {
6227
+ const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
5661
6228
  const d = db || getDatabase();
5662
6229
  clearExpiredLocks(d);
5663
- const pattern = `%${query}%`;
6230
+ const pattern = `%${opts.query}%`;
5664
6231
  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
6232
  const params = [pattern, pattern, pattern];
5666
- if (projectId) {
6233
+ if (opts.project_id) {
5667
6234
  sql += " AND project_id = ?";
5668
- params.push(projectId);
6235
+ params.push(opts.project_id);
5669
6236
  }
5670
- if (taskListId) {
6237
+ if (opts.task_list_id) {
5671
6238
  sql += " AND task_list_id = ?";
5672
- params.push(taskListId);
6239
+ params.push(opts.task_list_id);
6240
+ }
6241
+ if (opts.status) {
6242
+ if (Array.isArray(opts.status)) {
6243
+ sql += ` AND status IN (${opts.status.map(() => "?").join(",")})`;
6244
+ params.push(...opts.status);
6245
+ } else {
6246
+ sql += " AND status = ?";
6247
+ params.push(opts.status);
6248
+ }
6249
+ }
6250
+ if (opts.priority) {
6251
+ if (Array.isArray(opts.priority)) {
6252
+ sql += ` AND priority IN (${opts.priority.map(() => "?").join(",")})`;
6253
+ params.push(...opts.priority);
6254
+ } else {
6255
+ sql += " AND priority = ?";
6256
+ params.push(opts.priority);
6257
+ }
6258
+ }
6259
+ if (opts.assigned_to) {
6260
+ sql += " AND assigned_to = ?";
6261
+ params.push(opts.assigned_to);
6262
+ }
6263
+ if (opts.agent_id) {
6264
+ sql += " AND agent_id = ?";
6265
+ params.push(opts.agent_id);
6266
+ }
6267
+ if (opts.created_after) {
6268
+ sql += " AND created_at > ?";
6269
+ params.push(opts.created_after);
6270
+ }
6271
+ if (opts.updated_after) {
6272
+ sql += " AND updated_at > ?";
6273
+ params.push(opts.updated_after);
6274
+ }
6275
+ if (opts.has_dependencies === true) {
6276
+ sql += " AND id IN (SELECT task_id FROM task_dependencies)";
6277
+ } else if (opts.has_dependencies === false) {
6278
+ sql += " AND id NOT IN (SELECT task_id FROM task_dependencies)";
6279
+ }
6280
+ if (opts.is_blocked === true) {
6281
+ 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')";
6282
+ } else if (opts.is_blocked === false) {
6283
+ 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
6284
  }
5674
6285
  sql += ` ORDER BY
5675
6286
  CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
@@ -6199,28 +6810,41 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
6199
6810
  init_database();
6200
6811
  var server = new McpServer({
6201
6812
  name: "todos",
6202
- version: "0.9.33"
6813
+ version: "0.9.35"
6203
6814
  });
6204
6815
  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}`;
6816
+ if (error instanceof VersionConflictError) {
6817
+ return JSON.stringify({ code: VersionConflictError.code, message: error.message, suggestion: VersionConflictError.suggestion });
6818
+ }
6819
+ if (error instanceof TaskNotFoundError) {
6820
+ return JSON.stringify({ code: TaskNotFoundError.code, message: error.message, suggestion: TaskNotFoundError.suggestion });
6821
+ }
6822
+ if (error instanceof ProjectNotFoundError) {
6823
+ return JSON.stringify({ code: ProjectNotFoundError.code, message: error.message, suggestion: ProjectNotFoundError.suggestion });
6824
+ }
6825
+ if (error instanceof PlanNotFoundError) {
6826
+ return JSON.stringify({ code: PlanNotFoundError.code, message: error.message, suggestion: PlanNotFoundError.suggestion });
6827
+ }
6828
+ if (error instanceof TaskListNotFoundError) {
6829
+ return JSON.stringify({ code: TaskListNotFoundError.code, message: error.message, suggestion: TaskListNotFoundError.suggestion });
6830
+ }
6831
+ if (error instanceof LockError) {
6832
+ return JSON.stringify({ code: LockError.code, message: error.message, suggestion: LockError.suggestion });
6833
+ }
6834
+ if (error instanceof AgentNotFoundError) {
6835
+ return JSON.stringify({ code: AgentNotFoundError.code, message: error.message, suggestion: AgentNotFoundError.suggestion });
6836
+ }
6837
+ if (error instanceof DependencyCycleError) {
6838
+ return JSON.stringify({ code: DependencyCycleError.code, message: error.message, suggestion: DependencyCycleError.suggestion });
6839
+ }
6217
6840
  if (error instanceof CompletionGuardError) {
6218
- const retry = error.retryAfterSeconds ? ` (retry after ${error.retryAfterSeconds}s)` : "";
6219
- return `Completion blocked: ${error.reason}${retry}`;
6841
+ const retry = error.retryAfterSeconds ? { retryAfterSeconds: error.retryAfterSeconds } : {};
6842
+ return JSON.stringify({ code: CompletionGuardError.code, message: error.reason, suggestion: CompletionGuardError.suggestion, ...retry });
6220
6843
  }
6221
- if (error instanceof Error)
6222
- return error.message;
6223
- return String(error);
6844
+ if (error instanceof Error) {
6845
+ return JSON.stringify({ code: "UNKNOWN_ERROR", message: error.message });
6846
+ }
6847
+ return JSON.stringify({ code: "UNKNOWN_ERROR", message: String(error) });
6224
6848
  }
6225
6849
  function resolveId(partialId, table = "tasks") {
6226
6850
  const db = getDatabase();
@@ -6243,7 +6867,8 @@ function formatTask(task) {
6243
6867
  const id = task.short_id || task.id.slice(0, 8);
6244
6868
  const assigned = task.assigned_to ? ` -> ${task.assigned_to}` : "";
6245
6869
  const lock = task.locked_by ? ` [locked:${task.locked_by}]` : "";
6246
- return `${id} ${task.status.padEnd(11)} ${task.priority.padEnd(8)} ${task.title}${assigned}${lock}`;
6870
+ const recur = task.recurrence_rule ? ` [\u21BB]` : "";
6871
+ return `${id} ${task.status.padEnd(11)} ${task.priority.padEnd(8)} ${task.title}${assigned}${lock}${recur}`;
6247
6872
  }
6248
6873
  function formatTaskDetail(task) {
6249
6874
  const parts = [
@@ -6268,6 +6893,10 @@ function formatTaskDetail(task) {
6268
6893
  parts.push(`Plan: ${task.plan_id}`);
6269
6894
  if (task.tags.length > 0)
6270
6895
  parts.push(`Tags: ${task.tags.join(", ")}`);
6896
+ if (task.recurrence_rule)
6897
+ parts.push(`Recurrence: ${task.recurrence_rule}`);
6898
+ if (task.recurrence_parent_id)
6899
+ parts.push(`Recurrence parent: ${task.recurrence_parent_id}`);
6271
6900
  parts.push(`Version: ${task.version}`);
6272
6901
  parts.push(`Created: ${task.created_at}`);
6273
6902
  if (task.completed_at)
@@ -6291,7 +6920,8 @@ server.tool("create_task", "Create a new task", {
6291
6920
  tags: exports_external.array(exports_external.string()).optional(),
6292
6921
  metadata: exports_external.record(exports_external.unknown()).optional(),
6293
6922
  estimated_minutes: exports_external.number().optional(),
6294
- requires_approval: exports_external.boolean().optional()
6923
+ requires_approval: exports_external.boolean().optional(),
6924
+ recurrence_rule: exports_external.string().optional()
6295
6925
  }, async (params) => {
6296
6926
  try {
6297
6927
  const resolved = { ...params };
@@ -6309,7 +6939,7 @@ server.tool("create_task", "Create a new task", {
6309
6939
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6310
6940
  }
6311
6941
  });
6312
- server.tool("list_tasks", "List tasks with optional filters", {
6942
+ server.tool("list_tasks", "List tasks with optional filters and pagination.", {
6313
6943
  project_id: exports_external.string().optional(),
6314
6944
  status: exports_external.union([
6315
6945
  exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]),
@@ -6322,7 +6952,10 @@ server.tool("list_tasks", "List tasks with optional filters", {
6322
6952
  assigned_to: exports_external.string().optional(),
6323
6953
  tags: exports_external.array(exports_external.string()).optional(),
6324
6954
  plan_id: exports_external.string().optional(),
6325
- task_list_id: exports_external.string().optional()
6955
+ task_list_id: exports_external.string().optional(),
6956
+ has_recurrence: exports_external.boolean().optional(),
6957
+ limit: exports_external.number().optional(),
6958
+ offset: exports_external.number().optional()
6326
6959
  }, async (params) => {
6327
6960
  try {
6328
6961
  const resolved = { ...params };
@@ -6333,8 +6966,10 @@ server.tool("list_tasks", "List tasks with optional filters", {
6333
6966
  if (resolved.task_list_id)
6334
6967
  resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
6335
6968
  const tasks = listTasks(resolved);
6969
+ const { limit: _limit, offset: _offset, ...countFilter } = resolved;
6970
+ const total = countTasks(countFilter);
6336
6971
  if (tasks.length === 0) {
6337
- return { content: [{ type: "text", text: "No tasks found." }] };
6972
+ return { content: [{ type: "text", text: total > 0 ? `No tasks in this page (total: ${total}).` : "No tasks found." }] };
6338
6973
  }
6339
6974
  const text = tasks.map((t) => {
6340
6975
  const lock = t.locked_by ? ` [locked by ${t.locked_by}]` : "";
@@ -6342,13 +6977,15 @@ server.tool("list_tasks", "List tasks with optional filters", {
6342
6977
  return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned}${lock}`;
6343
6978
  }).join(`
6344
6979
  `);
6980
+ const pagination = resolved.limit ? `
6981
+ (showing ${tasks.length} of ${total}, offset: ${resolved.offset || 0})` : "";
6345
6982
  return { content: [{ type: "text", text: `${tasks.length} task(s):
6346
- ${text}` }] };
6983
+ ${text}${pagination}` }] };
6347
6984
  } catch (e) {
6348
6985
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6349
6986
  }
6350
6987
  });
6351
- server.tool("get_task", "Get full task details with relations", {
6988
+ server.tool("get_task", "Get full task details with subtasks, deps, and comments.", {
6352
6989
  id: exports_external.string()
6353
6990
  }, async ({ id }) => {
6354
6991
  try {
@@ -6417,7 +7054,7 @@ server.tool("update_task", "Update task fields. Version required for optimistic
6417
7054
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6418
7055
  }
6419
7056
  });
6420
- server.tool("delete_task", "Delete a task permanently", {
7057
+ server.tool("delete_task", "Delete a task permanently. Subtasks cascade-deleted.", {
6421
7058
  id: exports_external.string()
6422
7059
  }, async ({ id }) => {
6423
7060
  try {
@@ -6433,7 +7070,7 @@ server.tool("delete_task", "Delete a task permanently", {
6433
7070
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6434
7071
  }
6435
7072
  });
6436
- server.tool("start_task", "Claim, lock, and set task status to in_progress.", {
7073
+ server.tool("start_task", "Claim, lock, and set task to in_progress.", {
6437
7074
  id: exports_external.string(),
6438
7075
  agent_id: exports_external.string()
6439
7076
  }, async ({ id, agent_id }) => {
@@ -6445,19 +7082,26 @@ server.tool("start_task", "Claim, lock, and set task status to in_progress.", {
6445
7082
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6446
7083
  }
6447
7084
  });
6448
- server.tool("complete_task", "Mark task completed and release lock.", {
7085
+ server.tool("complete_task", "Complete a task. For recurring tasks, auto-spawns next instance.", {
6449
7086
  id: exports_external.string(),
6450
- agent_id: exports_external.string().optional()
6451
- }, async ({ id, agent_id }) => {
7087
+ agent_id: exports_external.string().optional(),
7088
+ skip_recurrence: exports_external.boolean().optional()
7089
+ }, async ({ id, agent_id, skip_recurrence }) => {
6452
7090
  try {
6453
7091
  const resolvedId = resolveId(id);
6454
- const task = completeTask(resolvedId, agent_id);
6455
- return { content: [{ type: "text", text: `completed: ${formatTask(task)}` }] };
7092
+ const task = completeTask(resolvedId, agent_id, undefined, { skip_recurrence });
7093
+ let text = `completed: ${formatTask(task)}`;
7094
+ if (task.metadata._next_recurrence) {
7095
+ const next = task.metadata._next_recurrence;
7096
+ text += `
7097
+ next: ${next.short_id || next.id.slice(0, 8)} due ${next.due_at}`;
7098
+ }
7099
+ return { content: [{ type: "text", text }] };
6456
7100
  } catch (e) {
6457
7101
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6458
7102
  }
6459
7103
  });
6460
- server.tool("lock_task", "Acquire exclusive lock on a task", {
7104
+ server.tool("lock_task", "Acquire exclusive lock. Expires after 30 min. Idempotent per agent.", {
6461
7105
  id: exports_external.string(),
6462
7106
  agent_id: exports_external.string()
6463
7107
  }, async ({ id, agent_id }) => {
@@ -6472,7 +7116,7 @@ server.tool("lock_task", "Acquire exclusive lock on a task", {
6472
7116
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6473
7117
  }
6474
7118
  });
6475
- server.tool("unlock_task", "Release exclusive lock on a task", {
7119
+ server.tool("unlock_task", "Release exclusive lock on a task.", {
6476
7120
  id: exports_external.string(),
6477
7121
  agent_id: exports_external.string().optional()
6478
7122
  }, async ({ id, agent_id }) => {
@@ -6484,7 +7128,7 @@ server.tool("unlock_task", "Release exclusive lock on a task", {
6484
7128
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6485
7129
  }
6486
7130
  });
6487
- server.tool("add_dependency", "Add a dependency: task_id depends on depends_on.", {
7131
+ server.tool("add_dependency", "Add a dependency. Prevents cycles via BFS detection.", {
6488
7132
  task_id: exports_external.string(),
6489
7133
  depends_on: exports_external.string()
6490
7134
  }, async ({ task_id, depends_on }) => {
@@ -6497,7 +7141,7 @@ server.tool("add_dependency", "Add a dependency: task_id depends on depends_on."
6497
7141
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6498
7142
  }
6499
7143
  });
6500
- server.tool("remove_dependency", "Remove a dependency between tasks", {
7144
+ server.tool("remove_dependency", "Remove a dependency link between two tasks.", {
6501
7145
  task_id: exports_external.string(),
6502
7146
  depends_on: exports_external.string()
6503
7147
  }, async ({ task_id, depends_on }) => {
@@ -6515,7 +7159,7 @@ server.tool("remove_dependency", "Remove a dependency between tasks", {
6515
7159
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6516
7160
  }
6517
7161
  });
6518
- server.tool("add_comment", "Add a comment/note to a task", {
7162
+ server.tool("add_comment", "Add a comment or note to a task. Comments are append-only.", {
6519
7163
  task_id: exports_external.string(),
6520
7164
  content: exports_external.string(),
6521
7165
  agent_id: exports_external.string().optional(),
@@ -6546,7 +7190,7 @@ ${text}` }] };
6546
7190
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6547
7191
  }
6548
7192
  });
6549
- server.tool("create_project", "Register a new project", {
7193
+ server.tool("create_project", "Register a new project with auto-generated task prefix.", {
6550
7194
  name: exports_external.string(),
6551
7195
  path: exports_external.string(),
6552
7196
  description: exports_external.string().optional(),
@@ -6565,7 +7209,7 @@ server.tool("create_project", "Register a new project", {
6565
7209
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6566
7210
  }
6567
7211
  });
6568
- server.tool("create_plan", "Create a new plan", {
7212
+ server.tool("create_plan", "Create a plan to group related tasks.", {
6569
7213
  name: exports_external.string(),
6570
7214
  project_id: exports_external.string().optional(),
6571
7215
  description: exports_external.string().optional(),
@@ -6590,7 +7234,7 @@ server.tool("create_plan", "Create a new plan", {
6590
7234
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6591
7235
  }
6592
7236
  });
6593
- server.tool("list_plans", "List plans with optional project filter", {
7237
+ server.tool("list_plans", "List all plans, optionally filtered by project.", {
6594
7238
  project_id: exports_external.string().optional()
6595
7239
  }, async ({ project_id }) => {
6596
7240
  try {
@@ -6610,7 +7254,7 @@ ${text}` }] };
6610
7254
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6611
7255
  }
6612
7256
  });
6613
- server.tool("get_plan", "Get plan details", {
7257
+ server.tool("get_plan", "Get plan details including status and timestamps.", {
6614
7258
  id: exports_external.string()
6615
7259
  }, async ({ id }) => {
6616
7260
  try {
@@ -6635,7 +7279,7 @@ server.tool("get_plan", "Get plan details", {
6635
7279
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6636
7280
  }
6637
7281
  });
6638
- server.tool("update_plan", "Update a plan", {
7282
+ server.tool("update_plan", "Update plan fields (name, description, status).", {
6639
7283
  id: exports_external.string(),
6640
7284
  name: exports_external.string().optional(),
6641
7285
  description: exports_external.string().optional(),
@@ -6659,7 +7303,7 @@ server.tool("update_plan", "Update a plan", {
6659
7303
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6660
7304
  }
6661
7305
  });
6662
- server.tool("delete_plan", "Delete a plan", {
7306
+ server.tool("delete_plan", "Delete a plan. Tasks in the plan are orphaned (not deleted).", {
6663
7307
  id: exports_external.string()
6664
7308
  }, async ({ id }) => {
6665
7309
  try {
@@ -6675,15 +7319,34 @@ server.tool("delete_plan", "Delete a plan", {
6675
7319
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6676
7320
  }
6677
7321
  });
6678
- server.tool("search_tasks", "Full-text search across task titles, descriptions, tags.", {
7322
+ server.tool("search_tasks", "Full-text search across tasks with filters.", {
6679
7323
  query: exports_external.string(),
6680
7324
  project_id: exports_external.string().optional(),
6681
- task_list_id: exports_external.string().optional()
6682
- }, async ({ query, project_id, task_list_id }) => {
7325
+ task_list_id: exports_external.string().optional(),
7326
+ status: exports_external.union([
7327
+ exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]),
7328
+ exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]))
7329
+ ]).optional(),
7330
+ priority: exports_external.union([
7331
+ exports_external.enum(["low", "medium", "high", "critical"]),
7332
+ exports_external.array(exports_external.enum(["low", "medium", "high", "critical"]))
7333
+ ]).optional(),
7334
+ assigned_to: exports_external.string().optional(),
7335
+ agent_id: exports_external.string().optional(),
7336
+ created_after: exports_external.string().optional(),
7337
+ updated_after: exports_external.string().optional(),
7338
+ has_dependencies: exports_external.boolean().optional(),
7339
+ is_blocked: exports_external.boolean().optional()
7340
+ }, async ({ query, project_id, task_list_id, ...filters }) => {
6683
7341
  try {
6684
7342
  const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
6685
7343
  const resolvedTaskListId = task_list_id ? resolveId(task_list_id, "task_lists") : undefined;
6686
- const tasks = searchTasks(query, resolvedProjectId, resolvedTaskListId);
7344
+ const tasks = searchTasks({
7345
+ query,
7346
+ project_id: resolvedProjectId,
7347
+ task_list_id: resolvedTaskListId,
7348
+ ...filters
7349
+ });
6687
7350
  if (tasks.length === 0) {
6688
7351
  return { content: [{ type: "text", text: `No tasks matching "${query}".` }] };
6689
7352
  }
@@ -6695,7 +7358,7 @@ ${text}` }] };
6695
7358
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6696
7359
  }
6697
7360
  });
6698
- server.tool("sync", "Sync tasks with an agent task list.", {
7361
+ server.tool("sync", "Sync tasks between local DB and agent task list.", {
6699
7362
  task_list_id: exports_external.string().optional(),
6700
7363
  agent: exports_external.string().optional(),
6701
7364
  all_agents: exports_external.boolean().optional(),
@@ -6743,7 +7406,7 @@ server.tool("sync", "Sync tasks with an agent task list.", {
6743
7406
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6744
7407
  }
6745
7408
  });
6746
- server.tool("register_agent", "Register an agent (idempotent by name).", {
7409
+ server.tool("register_agent", "Register an agent (idempotent by name). Updates last_seen_at.", {
6747
7410
  name: exports_external.string(),
6748
7411
  description: exports_external.string().optional()
6749
7412
  }, async ({ name, description }) => {
@@ -6780,7 +7443,7 @@ ${text}` }] };
6780
7443
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6781
7444
  }
6782
7445
  });
6783
- server.tool("get_agent", "Get agent details by ID or name", {
7446
+ server.tool("get_agent", "Get agent details by ID or name. Provide one of id or name.", {
6784
7447
  id: exports_external.string().optional(),
6785
7448
  name: exports_external.string().optional()
6786
7449
  }, async ({ id, name }) => {
@@ -6809,9 +7472,9 @@ server.tool("get_agent", "Get agent details by ID or name", {
6809
7472
  }
6810
7473
  });
6811
7474
  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")
7475
+ id: exports_external.string().optional(),
7476
+ name: exports_external.string().optional(),
7477
+ new_name: exports_external.string()
6815
7478
  }, async ({ id, name, new_name }) => {
6816
7479
  try {
6817
7480
  if (!id && !name) {
@@ -6834,8 +7497,8 @@ ID: ${updated.id}`
6834
7497
  }
6835
7498
  });
6836
7499
  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")
7500
+ id: exports_external.string().optional(),
7501
+ name: exports_external.string().optional()
6839
7502
  }, async ({ id, name }) => {
6840
7503
  try {
6841
7504
  if (!id && !name) {
@@ -6857,7 +7520,7 @@ server.tool("delete_agent", "Delete an agent permanently. Resolve by id or name.
6857
7520
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6858
7521
  }
6859
7522
  });
6860
- server.tool("create_task_list", "Create a new task list", {
7523
+ server.tool("create_task_list", "Create a task list container for organizing tasks.", {
6861
7524
  name: exports_external.string(),
6862
7525
  slug: exports_external.string().optional(),
6863
7526
  project_id: exports_external.string().optional(),
@@ -6883,7 +7546,7 @@ Description: ${list.description}` : ""}`
6883
7546
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6884
7547
  }
6885
7548
  });
6886
- server.tool("list_task_lists", "List task lists, optionally filtered by project", {
7549
+ server.tool("list_task_lists", "List all task lists, optionally filtered by project.", {
6887
7550
  project_id: exports_external.string().optional()
6888
7551
  }, async ({ project_id }) => {
6889
7552
  try {
@@ -6903,7 +7566,7 @@ ${text}` }] };
6903
7566
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6904
7567
  }
6905
7568
  });
6906
- server.tool("get_task_list", "Get task list details", {
7569
+ server.tool("get_task_list", "Get task list details including slug and metadata.", {
6907
7570
  id: exports_external.string()
6908
7571
  }, async ({ id }) => {
6909
7572
  try {
@@ -6931,7 +7594,7 @@ server.tool("get_task_list", "Get task list details", {
6931
7594
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6932
7595
  }
6933
7596
  });
6934
- server.tool("update_task_list", "Update a task list", {
7597
+ server.tool("update_task_list", "Update a task list's name or description.", {
6935
7598
  id: exports_external.string(),
6936
7599
  name: exports_external.string().optional(),
6937
7600
  description: exports_external.string().optional()
@@ -6952,7 +7615,7 @@ Slug: ${list.slug}`
6952
7615
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6953
7616
  }
6954
7617
  });
6955
- server.tool("delete_task_list", "Delete a task list. Tasks lose association but keep data.", {
7618
+ server.tool("delete_task_list", "Delete a task list. Tasks are orphaned, not deleted.", {
6956
7619
  id: exports_external.string()
6957
7620
  }, async ({ id }) => {
6958
7621
  try {
@@ -6968,7 +7631,7 @@ server.tool("delete_task_list", "Delete a task list. Tasks lose association but
6968
7631
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6969
7632
  }
6970
7633
  });
6971
- server.tool("get_task_history", "Get audit log for a task.", {
7634
+ server.tool("get_task_history", "Get audit log \u2014 field changes with timestamps and actors.", {
6972
7635
  task_id: exports_external.string()
6973
7636
  }, async ({ task_id }) => {
6974
7637
  try {
@@ -6985,7 +7648,7 @@ ${text}` }] };
6985
7648
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
6986
7649
  }
6987
7650
  });
6988
- server.tool("get_recent_activity", "Get recent task changes across all tasks.", {
7651
+ server.tool("get_recent_activity", "Get recent task changes \u2014 global activity feed.", {
6989
7652
  limit: exports_external.number().optional()
6990
7653
  }, async ({ limit }) => {
6991
7654
  try {
@@ -7001,7 +7664,7 @@ ${text}` }] };
7001
7664
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7002
7665
  }
7003
7666
  });
7004
- server.tool("create_webhook", "Register a webhook to receive task change events.", {
7667
+ server.tool("create_webhook", "Register a webhook for task change events.", {
7005
7668
  url: exports_external.string(),
7006
7669
  events: exports_external.array(exports_external.string()).optional(),
7007
7670
  secret: exports_external.string().optional()
@@ -7028,7 +7691,7 @@ ${text}` }] };
7028
7691
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7029
7692
  }
7030
7693
  });
7031
- server.tool("delete_webhook", "Delete a webhook", {
7694
+ server.tool("delete_webhook", "Delete a webhook by ID.", {
7032
7695
  id: exports_external.string()
7033
7696
  }, async ({ id }) => {
7034
7697
  try {
@@ -7094,7 +7757,7 @@ ${task.id.slice(0, 8)} | ${task.priority} | ${task.title}` }] };
7094
7757
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7095
7758
  }
7096
7759
  });
7097
- server.tool("delete_template", "Delete a task template", { id: exports_external.string() }, async ({ id }) => {
7760
+ server.tool("delete_template", "Delete a task template by ID.", { id: exports_external.string() }, async ({ id }) => {
7098
7761
  try {
7099
7762
  const { deleteTemplate: deleteTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
7100
7763
  const deleted = deleteTemplate2(id);
@@ -7103,7 +7766,7 @@ server.tool("delete_template", "Delete a task template", { id: exports_external.
7103
7766
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7104
7767
  }
7105
7768
  });
7106
- server.tool("approve_task", "Approve a task that requires approval.", {
7769
+ server.tool("approve_task", "Approve a task with requires_approval=true.", {
7107
7770
  id: exports_external.string(),
7108
7771
  agent_id: exports_external.string().optional()
7109
7772
  }, async ({ id, agent_id }) => {
@@ -7122,7 +7785,31 @@ server.tool("approve_task", "Approve a task that requires approval.", {
7122
7785
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7123
7786
  }
7124
7787
  });
7125
- server.tool("get_my_tasks", "Get assigned tasks and stats for an agent.", {
7788
+ server.tool("fail_task", "Mark a task as failed with structured reason and optional auto-retry.", {
7789
+ id: exports_external.string(),
7790
+ agent_id: exports_external.string().optional(),
7791
+ reason: exports_external.string().optional(),
7792
+ error_code: exports_external.string().optional(),
7793
+ retry: exports_external.boolean().optional(),
7794
+ retry_after: exports_external.string().optional()
7795
+ }, async ({ id, agent_id, reason, error_code, retry, retry_after }) => {
7796
+ try {
7797
+ const resolvedId = resolveId(id);
7798
+ const result = failTask(resolvedId, agent_id, reason, { retry, retry_after, error_code });
7799
+ let text = `failed: ${formatTask(result.task)}`;
7800
+ if (reason)
7801
+ text += `
7802
+ Reason: ${reason}`;
7803
+ if (result.retryTask) {
7804
+ text += `
7805
+ Retry task created: ${formatTask(result.retryTask)}`;
7806
+ }
7807
+ return { content: [{ type: "text", text }] };
7808
+ } catch (e) {
7809
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
7810
+ }
7811
+ });
7812
+ server.tool("get_my_tasks", "Get tasks assigned to/created by an agent with stats.", {
7126
7813
  agent_name: exports_external.string()
7127
7814
  }, async ({ agent_name }) => {
7128
7815
  try {
@@ -7155,7 +7842,7 @@ In Progress:`);
7155
7842
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7156
7843
  }
7157
7844
  });
7158
- server.tool("get_org_chart", "Get agent org chart \u2014 who reports to who.", {}, async () => {
7845
+ server.tool("get_org_chart", "Get agent org chart showing reporting hierarchy.", {}, async () => {
7159
7846
  try {
7160
7847
  let render = function(nodes, indent = 0) {
7161
7848
  return nodes.map((n) => {
@@ -7177,7 +7864,7 @@ server.tool("get_org_chart", "Get agent org chart \u2014 who reports to who.", {
7177
7864
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7178
7865
  }
7179
7866
  });
7180
- server.tool("set_reports_to", "Set who an agent reports to in the org chart.", {
7867
+ server.tool("set_reports_to", "Set agent reporting relationship in org chart.", {
7181
7868
  agent_name: exports_external.string(),
7182
7869
  manager_name: exports_external.string().optional()
7183
7870
  }, async ({ agent_name, manager_name }) => {
@@ -7200,7 +7887,326 @@ server.tool("set_reports_to", "Set who an agent reports to in the org chart.", {
7200
7887
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
7201
7888
  }
7202
7889
  });
7203
- server.tool("search_tools", "List tool names matching a query.", { query: exports_external.string().optional() }, async ({ query }) => {
7890
+ server.tool("bulk_update_tasks", "Update multiple tasks at once with the same changes.", {
7891
+ task_ids: exports_external.array(exports_external.string()),
7892
+ status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
7893
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
7894
+ assigned_to: exports_external.string().optional(),
7895
+ tags: exports_external.array(exports_external.string()).optional()
7896
+ }, async ({ task_ids, ...updates }) => {
7897
+ try {
7898
+ const resolvedIds = task_ids.map((id) => resolveId(id));
7899
+ const result = bulkUpdateTasks(resolvedIds, updates);
7900
+ const parts = [`Updated ${result.updated} task(s).`];
7901
+ if (result.failed.length > 0) {
7902
+ parts.push(`Failed ${result.failed.length}:`);
7903
+ for (const f of result.failed)
7904
+ parts.push(` ${f.id.slice(0, 8)}: ${f.error}`);
7905
+ }
7906
+ return { content: [{ type: "text", text: parts.join(`
7907
+ `) }] };
7908
+ } catch (e) {
7909
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
7910
+ }
7911
+ });
7912
+ server.tool("clone_task", "Duplicate a task with optional field overrides.", {
7913
+ task_id: exports_external.string(),
7914
+ title: exports_external.string().optional(),
7915
+ description: exports_external.string().optional(),
7916
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
7917
+ status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
7918
+ project_id: exports_external.string().optional(),
7919
+ plan_id: exports_external.string().optional(),
7920
+ task_list_id: exports_external.string().optional(),
7921
+ assigned_to: exports_external.string().optional(),
7922
+ tags: exports_external.array(exports_external.string()).optional(),
7923
+ estimated_minutes: exports_external.number().optional()
7924
+ }, async ({ task_id, ...overrides }) => {
7925
+ try {
7926
+ const resolvedId = resolveId(task_id);
7927
+ const resolved = { ...overrides };
7928
+ if (resolved.project_id)
7929
+ resolved.project_id = resolveId(resolved.project_id, "projects");
7930
+ if (resolved.plan_id)
7931
+ resolved.plan_id = resolveId(resolved.plan_id, "plans");
7932
+ if (resolved.task_list_id)
7933
+ resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
7934
+ const task = cloneTask(resolvedId, resolved);
7935
+ return { content: [{ type: "text", text: `cloned: ${formatTask(task)}` }] };
7936
+ } catch (e) {
7937
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
7938
+ }
7939
+ });
7940
+ server.tool("get_task_stats", "Get task analytics: counts by status, priority, agent.", {
7941
+ project_id: exports_external.string().optional(),
7942
+ task_list_id: exports_external.string().optional(),
7943
+ agent_id: exports_external.string().optional()
7944
+ }, async ({ project_id, task_list_id, agent_id }) => {
7945
+ try {
7946
+ const filters = {};
7947
+ if (project_id)
7948
+ filters.project_id = resolveId(project_id, "projects");
7949
+ if (task_list_id)
7950
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
7951
+ if (agent_id)
7952
+ filters.agent_id = agent_id;
7953
+ const stats = getTaskStats(Object.keys(filters).length > 0 ? filters : undefined);
7954
+ return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
7955
+ } catch (e) {
7956
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
7957
+ }
7958
+ });
7959
+ server.tool("get_task_graph", "Get full dependency tree for a task.", {
7960
+ id: exports_external.string(),
7961
+ direction: exports_external.enum(["up", "down", "both"]).optional()
7962
+ }, async ({ id, direction }) => {
7963
+ try {
7964
+ let formatNode = function(node, indent) {
7965
+ const prefix = " ".repeat(indent);
7966
+ const idLabel = node.task.short_id || node.task.id.slice(0, 8);
7967
+ const blocked = node.task.is_blocked ? " (blocked: yes)" : "";
7968
+ let out = `${prefix}[${node.task.status}] ${idLabel} | ${node.task.title}${blocked}
7969
+ `;
7970
+ if (node.depends_on.length > 0) {
7971
+ out += `${prefix} Depends on:
7972
+ `;
7973
+ for (const dep of node.depends_on) {
7974
+ out += formatNode(dep, indent + 2);
7975
+ }
7976
+ }
7977
+ if (node.blocks.length > 0) {
7978
+ out += `${prefix} Blocks:
7979
+ `;
7980
+ for (const dep of node.blocks) {
7981
+ out += formatNode(dep, indent + 2);
7982
+ }
7983
+ }
7984
+ return out;
7985
+ };
7986
+ const taskId = resolveId(id, "tasks");
7987
+ const graph = getTaskGraph(taskId, direction || "both");
7988
+ let text = `Task: ${formatNode(graph, 0)}`;
7989
+ if (graph.depends_on.length > 0) {
7990
+ text += `
7991
+ Depends on:
7992
+ `;
7993
+ for (const dep of graph.depends_on) {
7994
+ text += formatNode(dep, 1);
7995
+ }
7996
+ }
7997
+ if (graph.blocks.length > 0) {
7998
+ text += `
7999
+ Blocks:
8000
+ `;
8001
+ for (const dep of graph.blocks) {
8002
+ text += formatNode(dep, 1);
8003
+ }
8004
+ }
8005
+ return { content: [{ type: "text", text }] };
8006
+ } catch (e) {
8007
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8008
+ }
8009
+ });
8010
+ server.tool("bulk_create_tasks", "Create multiple tasks atomically with dependency support.", {
8011
+ tasks: exports_external.array(exports_external.object({
8012
+ temp_id: exports_external.string().optional(),
8013
+ title: exports_external.string(),
8014
+ description: exports_external.string().optional(),
8015
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
8016
+ status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
8017
+ project_id: exports_external.string().optional(),
8018
+ plan_id: exports_external.string().optional(),
8019
+ task_list_id: exports_external.string().optional(),
8020
+ agent_id: exports_external.string().optional(),
8021
+ assigned_to: exports_external.string().optional(),
8022
+ tags: exports_external.array(exports_external.string()).optional(),
8023
+ estimated_minutes: exports_external.number().optional(),
8024
+ depends_on_temp_ids: exports_external.array(exports_external.string()).optional()
8025
+ })),
8026
+ project_id: exports_external.string().optional(),
8027
+ plan_id: exports_external.string().optional(),
8028
+ task_list_id: exports_external.string().optional()
8029
+ }, async ({ tasks, project_id, plan_id, task_list_id }) => {
8030
+ try {
8031
+ const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
8032
+ const resolvedPlanId = plan_id ? resolveId(plan_id, "plans") : undefined;
8033
+ const resolvedTaskListId = task_list_id ? resolveId(task_list_id, "task_lists") : undefined;
8034
+ const enrichedTasks = tasks.map((t) => ({
8035
+ ...t,
8036
+ project_id: t.project_id || resolvedProjectId,
8037
+ plan_id: t.plan_id || resolvedPlanId,
8038
+ task_list_id: t.task_list_id || resolvedTaskListId
8039
+ }));
8040
+ const result = bulkCreateTasks(enrichedTasks);
8041
+ const lines = result.created.map((t) => {
8042
+ const tid = t.temp_id ? `[${t.temp_id}] ` : "";
8043
+ const sid = t.short_id || t.id.slice(0, 8);
8044
+ return ` ${tid}${sid} | ${t.title}`;
8045
+ });
8046
+ return { content: [{ type: "text", text: `Created ${result.created.length} task(s):
8047
+ ${lines.join(`
8048
+ `)}` }] };
8049
+ } catch (e) {
8050
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8051
+ }
8052
+ });
8053
+ server.tool("move_task", "Move a task to a different list, project, or plan.", {
8054
+ task_id: exports_external.string(),
8055
+ task_list_id: exports_external.string().nullable().optional(),
8056
+ project_id: exports_external.string().nullable().optional(),
8057
+ plan_id: exports_external.string().nullable().optional()
8058
+ }, async ({ task_id, ...target }) => {
8059
+ try {
8060
+ const resolvedId = resolveId(task_id);
8061
+ const resolvedTarget = {};
8062
+ if (target.task_list_id !== undefined)
8063
+ resolvedTarget.task_list_id = target.task_list_id ? resolveId(target.task_list_id, "task_lists") : null;
8064
+ if (target.project_id !== undefined)
8065
+ resolvedTarget.project_id = target.project_id ? resolveId(target.project_id, "projects") : null;
8066
+ if (target.plan_id !== undefined)
8067
+ resolvedTarget.plan_id = target.plan_id ? resolveId(target.plan_id, "plans") : null;
8068
+ const task = moveTask(resolvedId, resolvedTarget);
8069
+ return { content: [{ type: "text", text: `moved: ${formatTask(task)}` }] };
8070
+ } catch (e) {
8071
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8072
+ }
8073
+ });
8074
+ server.tool("get_next_task", "Get the best pending task to work on next.", {
8075
+ agent_id: exports_external.string().optional(),
8076
+ project_id: exports_external.string().optional(),
8077
+ task_list_id: exports_external.string().optional(),
8078
+ plan_id: exports_external.string().optional(),
8079
+ tags: exports_external.array(exports_external.string()).optional()
8080
+ }, async ({ agent_id, project_id, task_list_id, plan_id, tags }) => {
8081
+ try {
8082
+ const filters = {};
8083
+ if (project_id)
8084
+ filters.project_id = resolveId(project_id, "projects");
8085
+ if (task_list_id)
8086
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
8087
+ if (plan_id)
8088
+ filters.plan_id = resolveId(plan_id, "plans");
8089
+ if (tags)
8090
+ filters.tags = tags;
8091
+ const task = getNextTask(agent_id, Object.keys(filters).length > 0 ? filters : undefined);
8092
+ if (!task) {
8093
+ return { content: [{ type: "text", text: "No tasks available \u2014 all pending tasks are blocked, locked, or none exist." }] };
8094
+ }
8095
+ return { content: [{ type: "text", text: `next: ${formatTask(task)}
8096
+ ${formatTaskDetail(task)}` }] };
8097
+ } catch (e) {
8098
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8099
+ }
8100
+ });
8101
+ server.tool("get_active_work", "See all in-progress tasks and who is working on them.", {
8102
+ project_id: exports_external.string().optional(),
8103
+ task_list_id: exports_external.string().optional()
8104
+ }, async ({ project_id, task_list_id }) => {
8105
+ try {
8106
+ const filters = {};
8107
+ if (project_id)
8108
+ filters.project_id = resolveId(project_id, "projects");
8109
+ if (task_list_id)
8110
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
8111
+ const work = getActiveWork(Object.keys(filters).length > 0 ? filters : undefined);
8112
+ if (work.length === 0) {
8113
+ return { content: [{ type: "text", text: "No active work \u2014 no tasks are currently in progress." }] };
8114
+ }
8115
+ const text = work.map((w) => {
8116
+ const id = w.short_id || w.id.slice(0, 8);
8117
+ const agent = w.assigned_to || w.locked_by || "unassigned";
8118
+ const since = w.updated_at;
8119
+ return `${agent.padEnd(12)} | ${w.priority.padEnd(8)} | ${id} | ${w.title} (since ${since})`;
8120
+ }).join(`
8121
+ `);
8122
+ return { content: [{ type: "text", text: `${work.length} active task(s):
8123
+ ${text}` }] };
8124
+ } catch (e) {
8125
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8126
+ }
8127
+ });
8128
+ server.tool("get_tasks_changed_since", "Get tasks modified after a timestamp for incremental sync.", {
8129
+ since: exports_external.string(),
8130
+ project_id: exports_external.string().optional(),
8131
+ task_list_id: exports_external.string().optional()
8132
+ }, async ({ since, project_id, task_list_id }) => {
8133
+ try {
8134
+ const filters = {};
8135
+ if (project_id)
8136
+ filters.project_id = resolveId(project_id, "projects");
8137
+ if (task_list_id)
8138
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
8139
+ const tasks = getTasksChangedSince(since, Object.keys(filters).length > 0 ? filters : undefined);
8140
+ if (tasks.length === 0) {
8141
+ return { content: [{ type: "text", text: `No tasks changed since ${since}.` }] };
8142
+ }
8143
+ const text = tasks.map((t) => {
8144
+ const assigned = t.assigned_to ? ` -> ${t.assigned_to}` : "";
8145
+ return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned} (updated: ${t.updated_at})`;
8146
+ }).join(`
8147
+ `);
8148
+ return { content: [{ type: "text", text: `${tasks.length} task(s) changed since ${since}:
8149
+ ${text}` }] };
8150
+ } catch (e) {
8151
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8152
+ }
8153
+ });
8154
+ server.tool("claim_next_task", "Atomically claim, lock, and start the best pending task.", {
8155
+ agent_id: exports_external.string(),
8156
+ project_id: exports_external.string().optional(),
8157
+ task_list_id: exports_external.string().optional(),
8158
+ plan_id: exports_external.string().optional(),
8159
+ tags: exports_external.array(exports_external.string()).optional()
8160
+ }, async ({ agent_id, project_id, task_list_id, plan_id, tags }) => {
8161
+ try {
8162
+ const filters = {};
8163
+ if (project_id)
8164
+ filters.project_id = resolveId(project_id, "projects");
8165
+ if (task_list_id)
8166
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
8167
+ if (plan_id)
8168
+ filters.plan_id = resolveId(plan_id, "plans");
8169
+ if (tags)
8170
+ filters.tags = tags;
8171
+ const task = claimNextTask(agent_id, Object.keys(filters).length > 0 ? filters : undefined);
8172
+ if (!task) {
8173
+ return { content: [{ type: "text", text: "No tasks available to claim." }] };
8174
+ }
8175
+ return { content: [{ type: "text", text: `claimed: ${formatTask(task)}
8176
+ ${formatTaskDetail(task)}` }] };
8177
+ } catch (e) {
8178
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8179
+ }
8180
+ });
8181
+ server.tool("get_stale_tasks", "Find stale in_progress tasks with no recent activity.", {
8182
+ stale_minutes: exports_external.number().optional(),
8183
+ project_id: exports_external.string().optional(),
8184
+ task_list_id: exports_external.string().optional()
8185
+ }, async ({ stale_minutes, project_id, task_list_id }) => {
8186
+ try {
8187
+ const filters = {};
8188
+ if (project_id)
8189
+ filters.project_id = resolveId(project_id, "projects");
8190
+ if (task_list_id)
8191
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
8192
+ const tasks = getStaleTasks(stale_minutes || 30, Object.keys(filters).length > 0 ? filters : undefined);
8193
+ if (tasks.length === 0) {
8194
+ return { content: [{ type: "text", text: "No stale tasks found." }] };
8195
+ }
8196
+ const text = tasks.map((t) => {
8197
+ const id = t.short_id || t.id.slice(0, 8);
8198
+ const agent = t.locked_by || t.assigned_to || "unknown";
8199
+ const staleFor = Math.round((Date.now() - new Date(t.updated_at).getTime()) / 60000);
8200
+ return `${id} | ${agent} | ${t.title} (stale ${staleFor}min)`;
8201
+ }).join(`
8202
+ `);
8203
+ return { content: [{ type: "text", text: `${tasks.length} stale task(s):
8204
+ ${text}` }] };
8205
+ } catch (e) {
8206
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
8207
+ }
8208
+ });
8209
+ server.tool("search_tools", "List all tool names, optionally filtered by substring.", { query: exports_external.string().optional() }, async ({ query }) => {
7204
8210
  const all = [
7205
8211
  "create_task",
7206
8212
  "list_tasks",
@@ -7209,6 +8215,7 @@ server.tool("search_tools", "List tool names matching a query.", { query: export
7209
8215
  "delete_task",
7210
8216
  "start_task",
7211
8217
  "complete_task",
8218
+ "fail_task",
7212
8219
  "lock_task",
7213
8220
  "unlock_task",
7214
8221
  "approve_task",
@@ -7225,7 +8232,11 @@ server.tool("search_tools", "List tool names matching a query.", { query: export
7225
8232
  "register_agent",
7226
8233
  "list_agents",
7227
8234
  "get_agent",
8235
+ "rename_agent",
8236
+ "delete_agent",
7228
8237
  "get_my_tasks",
8238
+ "get_org_chart",
8239
+ "set_reports_to",
7229
8240
  "create_task_list",
7230
8241
  "list_task_lists",
7231
8242
  "get_task_list",
@@ -7233,6 +8244,10 @@ server.tool("search_tools", "List tool names matching a query.", { query: export
7233
8244
  "delete_task_list",
7234
8245
  "search_tasks",
7235
8246
  "sync",
8247
+ "clone_task",
8248
+ "move_task",
8249
+ "get_next_task",
8250
+ "claim_next_task",
7236
8251
  "get_task_history",
7237
8252
  "get_recent_activity",
7238
8253
  "create_webhook",
@@ -7242,6 +8257,13 @@ server.tool("search_tools", "List tool names matching a query.", { query: export
7242
8257
  "list_templates",
7243
8258
  "create_task_from_template",
7244
8259
  "delete_template",
8260
+ "bulk_update_tasks",
8261
+ "bulk_create_tasks",
8262
+ "get_task_stats",
8263
+ "get_task_graph",
8264
+ "get_active_work",
8265
+ "get_tasks_changed_since",
8266
+ "get_stale_tasks",
7245
8267
  "search_tools",
7246
8268
  "describe_tools"
7247
8269
  ];
@@ -7249,27 +8271,175 @@ server.tool("search_tools", "List tool names matching a query.", { query: export
7249
8271
  const matches = q ? all.filter((n) => n.includes(q)) : all;
7250
8272
  return { content: [{ type: "text", text: matches.join(", ") }] };
7251
8273
  });
7252
- server.tool("describe_tools", "Get descriptions for specific tools by name.", { names: exports_external.array(exports_external.string()) }, async ({ names }) => {
8274
+ server.tool("describe_tools", "Get detailed parameter info for specific tools by name.", { names: exports_external.array(exports_external.string()) }, async ({ names }) => {
7253
8275
  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"
8276
+ create_task: `Create a new task.
8277
+ 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)
8278
+ Example: {title: 'Daily standup', recurrence_rule: 'every weekday', priority: 'medium'}`,
8279
+ list_tasks: `List tasks with optional filters. Supports pagination.
8280
+ 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)
8281
+ Example: {status: ['pending', 'in_progress'], has_recurrence: true, limit: 20}`,
8282
+ get_task: `Get full task details with subtasks, deps, and comments.
8283
+ Params: id(string, req \u2014 task ID, short_id like 'APP-00001', or partial ID)
8284
+ Example: {id: 'a1b2c3d4'}`,
8285
+ update_task: `Update task fields. Requires version for optimistic locking (get it from get_task first).
8286
+ 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)
8287
+ Example: {id: 'a1b2c3d4', version: 3, status: 'completed'}`,
8288
+ delete_task: `Delete a task permanently. Subtasks cascade-delete. Dependencies removed.
8289
+ Params: id(string, req)
8290
+ Example: {id: 'a1b2c3d4'}`,
8291
+ start_task: `Claim, lock, and set task status to in_progress in one call.
8292
+ Params: id(string, req), agent_id(string, req \u2014 your 8-char agent ID)
8293
+ Example: {id: 'a1b2c3d4', agent_id: 'e5f6g7h8'}`,
8294
+ complete_task: `Mark task completed, release lock, set completed_at timestamp. For recurring tasks, auto-spawns next instance unless skip_recurrence is true.
8295
+ 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)
8296
+ Example: {id: 'a1b2c3d4', skip_recurrence: false}`,
8297
+ lock_task: `Acquire exclusive lock on a task. Locks auto-expire after 30 min. Re-locking by same agent is idempotent.
8298
+ Params: id(string, req), agent_id(string, req)
8299
+ Example: {id: 'a1b2c3d4', agent_id: 'e5f6g7h8'}`,
8300
+ unlock_task: `Release exclusive lock on a task.
8301
+ Params: id(string, req), agent_id(string, optional \u2014 omit to force-unlock)
8302
+ Example: {id: 'a1b2c3d4', agent_id: 'e5f6g7h8'}`,
8303
+ approve_task: `Approve a task with requires_approval=true. Must be approved before completion.
8304
+ Params: id(string, req), agent_id(string, optional \u2014 defaults to 'system')
8305
+ Example: {id: 'a1b2c3d4', agent_id: 'e5f6g7h8'}`,
8306
+ fail_task: `Mark a task as failed with structured reason and optional auto-retry. Stores failure info in metadata._failure, releases lock.
8307
+ 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)
8308
+ Example: {id: 'a1b2c3d4', reason: 'Build timeout', error_code: 'TIMEOUT', retry: true}`,
8309
+ add_dependency: `Add a dependency: task_id depends on depends_on. Prevents cycles via BFS.
8310
+ Params: task_id(string, req), depends_on(string, req)
8311
+ Example: {task_id: 'abc12345', depends_on: 'def67890'}`,
8312
+ remove_dependency: `Remove a dependency link between two tasks.
8313
+ Params: task_id(string, req), depends_on(string, req)
8314
+ Example: {task_id: 'abc12345', depends_on: 'def67890'}`,
8315
+ add_comment: `Add a comment/note to a task. Comments are append-only.
8316
+ Params: task_id(string, req), content(string, req), agent_id(string), session_id(string)
8317
+ Example: {task_id: 'a1b2c3d4', content: 'Blocked by API rate limit'}`,
8318
+ create_project: `Register a new project. Auto-generates task prefix for short IDs (e.g. APP-00001).
8319
+ Params: name(string, req), path(string, req \u2014 unique absolute path), description(string), task_list_id(string)
8320
+ Example: {name: 'my-app', path: '/Users/dev/my-app'}`,
8321
+ list_projects: "List all registered projects. No params.",
8322
+ create_plan: `Create a plan to group related tasks.
8323
+ Params: name(string, req), project_id(string), description(string), status(active|completed|archived, default:active), task_list_id(string), agent_id(string)
8324
+ Example: {name: 'Sprint 1', project_id: 'a1b2c3d4'}`,
8325
+ list_plans: `List all plans, optionally filtered by project.
8326
+ Params: project_id(string)
8327
+ Example: {project_id: 'a1b2c3d4'}`,
8328
+ get_plan: `Get plan details (name, status, description, timestamps).
8329
+ Params: id(string, req)
8330
+ Example: {id: 'a1b2c3d4'}`,
8331
+ update_plan: `Update plan fields.
8332
+ Params: id(string, req), name(string), description(string), status(active|completed|archived), task_list_id(string), agent_id(string)
8333
+ Example: {id: 'a1b2c3d4', status: 'completed'}`,
8334
+ delete_plan: `Delete a plan. Tasks in the plan are orphaned, not deleted.
8335
+ Params: id(string, req)
8336
+ Example: {id: 'a1b2c3d4'}`,
8337
+ register_agent: `Register an agent (idempotent by name). Returns existing agent if name matches.
8338
+ Params: name(string, req \u2014 e.g. 'maximus'), description(string)
8339
+ Example: {name: 'maximus', description: 'Backend developer'}`,
8340
+ list_agents: "List all registered agents with IDs, names, and last seen timestamps. No params.",
8341
+ get_agent: `Get agent details by ID or name. Provide one of id or name.
8342
+ Params: id(string), name(string)
8343
+ Example: {name: 'maximus'}`,
8344
+ rename_agent: `Rename an agent. Resolve by id or current name.
8345
+ Params: id(string), name(string \u2014 current name), new_name(string, req)
8346
+ Example: {name: 'old-name', new_name: 'new-name'}`,
8347
+ delete_agent: `Delete an agent permanently. Resolve by id or name.
8348
+ Params: id(string), name(string)
8349
+ Example: {name: 'maximus'}`,
8350
+ get_my_tasks: `Get all tasks assigned to/created by an agent, with stats (pending/active/done/rate).
8351
+ Params: agent_name(string, req)
8352
+ Example: {agent_name: 'maximus'}`,
8353
+ get_org_chart: "Get agent org chart showing reporting hierarchy. No params.",
8354
+ set_reports_to: `Set who an agent reports to in the org chart. Omit manager_name for top-level.
8355
+ Params: agent_name(string, req), manager_name(string, optional)
8356
+ Example: {agent_name: 'brutus', manager_name: 'maximus'}`,
8357
+ create_task_list: `Create a task list \u2014 a container/folder for organizing tasks.
8358
+ Params: name(string, req), slug(string \u2014 auto-generated if omitted), project_id(string), description(string)
8359
+ Example: {name: 'Sprint 1', project_id: 'a1b2c3d4'}`,
8360
+ list_task_lists: `List all task lists, optionally filtered by project.
8361
+ Params: project_id(string)
8362
+ Example: {project_id: 'a1b2c3d4'}`,
8363
+ get_task_list: `Get task list details (name, slug, project, metadata).
8364
+ Params: id(string, req)
8365
+ Example: {id: 'a1b2c3d4'}`,
8366
+ update_task_list: `Update a task list's name or description.
8367
+ Params: id(string, req), name(string), description(string)
8368
+ Example: {id: 'a1b2c3d4', name: 'Sprint 2'}`,
8369
+ delete_task_list: `Delete a task list. Tasks are orphaned (not deleted).
8370
+ Params: id(string, req)
8371
+ Example: {id: 'a1b2c3d4'}`,
8372
+ search_tasks: `Full-text search across task titles, descriptions, and tags. Supports filters.
8373
+ 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)
8374
+ Example: {query: 'auth bug', status: 'pending'}`,
8375
+ 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.
8376
+ Params: agent_id(string \u2014 prefers your tasks), project_id(string), task_list_id(string), plan_id(string), tags(string[])
8377
+ Example: {agent_id: 'a1b2c3d4', project_id: 'e5f6g7h8'}`,
8378
+ 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.
8379
+ Params: agent_id(string, req \u2014 used for lock and assignment), project_id(string), task_list_id(string), plan_id(string), tags(string[])
8380
+ Example: {agent_id: 'a1b2c3d4', project_id: 'e5f6g7h8'}`,
8381
+ sync: `Sync tasks between local DB and agent task list (e.g. Claude Code).
8382
+ 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)
8383
+ Example: {agent: 'claude', direction: 'push'}`,
8384
+ clone_task: `Duplicate a task with optional field overrides. Creates new independent copy.
8385
+ 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)
8386
+ Example: {task_id: 'a1b2c3d4', title: 'Cloned task', assigned_to: 'brutus'}`,
8387
+ move_task: `Move a task to a different list, project, or plan.
8388
+ Params: task_id(string, req), task_list_id(string|null), project_id(string|null), plan_id(string|null)
8389
+ Example: {task_id: 'a1b2c3d4', task_list_id: 'e5f6g7h8'}`,
8390
+ bulk_update_tasks: `Update multiple tasks at once with the same changes.
8391
+ Params: task_ids(string[], req), status(pending|in_progress|completed|failed|cancelled), priority(low|medium|high|critical), assigned_to(string), tags(string[])
8392
+ Example: {task_ids: ['abc12345', 'def67890'], status: 'completed'}`,
8393
+ bulk_create_tasks: `Create multiple tasks atomically. Supports inter-task dependencies via temp_id references.
8394
+ 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)
8395
+ Example: {tasks: [{temp_id: 'a', title: 'First'}, {temp_id: 'b', title: 'Second', depends_on_temp_ids: ['a']}]}`,
8396
+ get_task_stats: `Get task analytics: counts by status, priority, agent, and completion rate. All via SQL.
8397
+ Params: project_id(string), task_list_id(string), agent_id(string)
8398
+ Example: {project_id: 'a1b2c3d4'}`,
8399
+ get_task_graph: `Get full dependency tree for a task \u2014 upstream blockers and downstream dependents.
8400
+ Params: id(string, req), direction(up|down|both, default:both)
8401
+ Example: {id: 'a1b2c3d4', direction: 'up'}`,
8402
+ get_task_history: `Get audit log for a task \u2014 all field changes with timestamps and actors.
8403
+ Params: task_id(string, req)
8404
+ Example: {task_id: 'a1b2c3d4'}`,
8405
+ get_recent_activity: `Get recent task changes across all tasks \u2014 global activity feed.
8406
+ Params: limit(number, default:50)
8407
+ Example: {limit: 20}`,
8408
+ create_webhook: `Register a webhook for task change events.
8409
+ Params: url(string, req), events(string[] \u2014 empty=all), secret(string \u2014 HMAC signing)
8410
+ Example: {url: 'https://example.com/hook', events: ['task.created', 'task.completed']}`,
8411
+ list_webhooks: "List all registered webhooks. No params.",
8412
+ delete_webhook: `Delete a webhook by ID.
8413
+ Params: id(string, req)
8414
+ Example: {id: 'a1b2c3d4'}`,
8415
+ create_template: `Create a reusable task template.
8416
+ 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)
8417
+ Example: {name: 'Bug Report', title_pattern: 'Bug: {description}', priority: 'high', tags: ['bug']}`,
8418
+ list_templates: "List all task templates. No params.",
8419
+ create_task_from_template: `Create a task from a template with optional overrides.
8420
+ Params: template_id(string, req), title(string), description(string), priority(low|medium|high|critical), assigned_to(string), project_id(string)
8421
+ Example: {template_id: 'a1b2c3d4', assigned_to: 'maximus'}`,
8422
+ delete_template: `Delete a task template.
8423
+ Params: id(string, req)
8424
+ Example: {id: 'a1b2c3d4'}`,
8425
+ get_active_work: `See all in-progress tasks and who is working on them.
8426
+ Params: project_id(string, optional), task_list_id(string, optional)
8427
+ Example: {project_id: 'a1b2c3d4'}`,
8428
+ get_tasks_changed_since: `Get tasks modified after a timestamp \u2014 incremental delta sync.
8429
+ Params: since(string, req \u2014 ISO date), project_id(string, optional), task_list_id(string, optional)
8430
+ Example: {since: '2026-03-14T10:00:00Z'}`,
8431
+ get_stale_tasks: `Find stale in_progress tasks with no recent activity.
8432
+ Params: stale_minutes(number, default:30), project_id(string, optional), task_list_id(string, optional)
8433
+ Example: {stale_minutes: 60, project_id: 'a1b2c3d4'}`,
8434
+ search_tools: `List all tool names or filter by substring.
8435
+ Params: query(string, optional)
8436
+ Example: {query: 'task'}`,
8437
+ describe_tools: `Get detailed descriptions and parameter info for tools by name.
8438
+ Params: names(string[], req)
8439
+ Example: {names: ['create_task', 'update_task']}`
7271
8440
  };
7272
- const result = names.map((n) => `${n}: ${descriptions[n] || "See tool schema"}`).join(`
8441
+ const result = names.map((n) => `${n}: ${descriptions[n] || "Unknown tool. Use search_tools to list available tools."}`).join(`
8442
+
7273
8443
  `);
7274
8444
  return { content: [{ type: "text", text: result }] };
7275
8445
  });