@hasna/todos 0.9.34 → 0.9.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2258,6 +2258,8 @@ function ensureSchema(db) {
2258
2258
  ensureColumn("tasks", "requires_approval", "INTEGER NOT NULL DEFAULT 0");
2259
2259
  ensureColumn("tasks", "approved_by", "TEXT");
2260
2260
  ensureColumn("tasks", "approved_at", "TEXT");
2261
+ ensureColumn("tasks", "recurrence_rule", "TEXT");
2262
+ ensureColumn("tasks", "recurrence_parent_id", "TEXT REFERENCES tasks(id) ON DELETE SET NULL");
2261
2263
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
2262
2264
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
2263
2265
  ensureColumn("agents", "reports_to", "TEXT");
@@ -2282,6 +2284,8 @@ function ensureSchema(db) {
2282
2284
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
2283
2285
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
2284
2286
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(agent_id)");
2287
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id)");
2288
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL");
2285
2289
  }
2286
2290
  function backfillTaskTags(db) {
2287
2291
  try {
@@ -2581,17 +2585,26 @@ var init_database = __esm(() => {
2581
2585
  ALTER TABLE agents ADD COLUMN org_id TEXT REFERENCES orgs(id) ON DELETE SET NULL;
2582
2586
  ALTER TABLE projects ADD COLUMN org_id TEXT REFERENCES orgs(id) ON DELETE SET NULL;
2583
2587
  INSERT OR IGNORE INTO _migrations (id) VALUES (12);
2588
+ `,
2589
+ `
2590
+ ALTER TABLE tasks ADD COLUMN recurrence_rule TEXT;
2591
+ ALTER TABLE tasks ADD COLUMN recurrence_parent_id TEXT REFERENCES tasks(id) ON DELETE SET NULL;
2592
+ CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id);
2593
+ CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL;
2594
+ INSERT OR IGNORE INTO _migrations (id) VALUES (13);
2584
2595
  `
2585
2596
  ];
2586
2597
  });
2587
2598
 
2588
2599
  // src/types/index.ts
2589
- var VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError;
2600
+ var VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, AgentNotFoundError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError;
2590
2601
  var init_types = __esm(() => {
2591
2602
  VersionConflictError = class VersionConflictError extends Error {
2592
2603
  taskId;
2593
2604
  expectedVersion;
2594
2605
  actualVersion;
2606
+ static code = "VERSION_CONFLICT";
2607
+ static suggestion = "Fetch the task with get_task to get the current version before updating.";
2595
2608
  constructor(taskId, expectedVersion, actualVersion) {
2596
2609
  super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
2597
2610
  this.taskId = taskId;
@@ -2602,6 +2615,8 @@ var init_types = __esm(() => {
2602
2615
  };
2603
2616
  TaskNotFoundError = class TaskNotFoundError extends Error {
2604
2617
  taskId;
2618
+ static code = "TASK_NOT_FOUND";
2619
+ static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
2605
2620
  constructor(taskId) {
2606
2621
  super(`Task not found: ${taskId}`);
2607
2622
  this.taskId = taskId;
@@ -2610,6 +2625,8 @@ var init_types = __esm(() => {
2610
2625
  };
2611
2626
  ProjectNotFoundError = class ProjectNotFoundError extends Error {
2612
2627
  projectId;
2628
+ static code = "PROJECT_NOT_FOUND";
2629
+ static suggestion = "Use list_projects to see available projects.";
2613
2630
  constructor(projectId) {
2614
2631
  super(`Project not found: ${projectId}`);
2615
2632
  this.projectId = projectId;
@@ -2618,6 +2635,8 @@ var init_types = __esm(() => {
2618
2635
  };
2619
2636
  PlanNotFoundError = class PlanNotFoundError extends Error {
2620
2637
  planId;
2638
+ static code = "PLAN_NOT_FOUND";
2639
+ static suggestion = "Use list_plans to see available plans.";
2621
2640
  constructor(planId) {
2622
2641
  super(`Plan not found: ${planId}`);
2623
2642
  this.planId = planId;
@@ -2627,6 +2646,8 @@ var init_types = __esm(() => {
2627
2646
  LockError = class LockError extends Error {
2628
2647
  taskId;
2629
2648
  lockedBy;
2649
+ static code = "LOCK_ERROR";
2650
+ static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
2630
2651
  constructor(taskId, lockedBy) {
2631
2652
  super(`Task ${taskId} is locked by ${lockedBy}`);
2632
2653
  this.taskId = taskId;
@@ -2634,8 +2655,20 @@ var init_types = __esm(() => {
2634
2655
  this.name = "LockError";
2635
2656
  }
2636
2657
  };
2658
+ AgentNotFoundError = class AgentNotFoundError extends Error {
2659
+ agentId;
2660
+ static code = "AGENT_NOT_FOUND";
2661
+ static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
2662
+ constructor(agentId) {
2663
+ super(`Agent not found: ${agentId}`);
2664
+ this.agentId = agentId;
2665
+ this.name = "AgentNotFoundError";
2666
+ }
2667
+ };
2637
2668
  TaskListNotFoundError = class TaskListNotFoundError extends Error {
2638
2669
  taskListId;
2670
+ static code = "TASK_LIST_NOT_FOUND";
2671
+ static suggestion = "Use list_task_lists to see available lists.";
2639
2672
  constructor(taskListId) {
2640
2673
  super(`Task list not found: ${taskListId}`);
2641
2674
  this.taskListId = taskListId;
@@ -2645,6 +2678,8 @@ var init_types = __esm(() => {
2645
2678
  DependencyCycleError = class DependencyCycleError extends Error {
2646
2679
  taskId;
2647
2680
  dependsOn;
2681
+ static code = "DEPENDENCY_CYCLE";
2682
+ static suggestion = "Check the dependency chain with get_task to avoid circular references.";
2648
2683
  constructor(taskId, dependsOn) {
2649
2684
  super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
2650
2685
  this.taskId = taskId;
@@ -2655,6 +2690,8 @@ var init_types = __esm(() => {
2655
2690
  CompletionGuardError = class CompletionGuardError extends Error {
2656
2691
  reason;
2657
2692
  retryAfterSeconds;
2693
+ static code = "COMPLETION_BLOCKED";
2694
+ static suggestion = "Wait for the cooldown period, then retry.";
2658
2695
  constructor(reason, retryAfterSeconds) {
2659
2696
  super(reason);
2660
2697
  this.reason = reason;
@@ -2974,6 +3011,147 @@ var init_audit = __esm(() => {
2974
3011
  init_database();
2975
3012
  });
2976
3013
 
3014
+ // src/lib/recurrence.ts
3015
+ function parseRecurrenceRule(rule) {
3016
+ const normalized = rule.trim().toLowerCase();
3017
+ if (normalized === "every weekday" || normalized === "every weekdays") {
3018
+ return { type: "specific_days", days: [1, 2, 3, 4, 5] };
3019
+ }
3020
+ if (normalized === "every day" || normalized === "daily") {
3021
+ return { type: "interval", interval: 1, unit: "day" };
3022
+ }
3023
+ if (normalized === "every week" || normalized === "weekly") {
3024
+ return { type: "interval", interval: 1, unit: "week" };
3025
+ }
3026
+ if (normalized === "every month" || normalized === "monthly") {
3027
+ return { type: "interval", interval: 1, unit: "month" };
3028
+ }
3029
+ const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
3030
+ if (intervalMatch) {
3031
+ return {
3032
+ type: "interval",
3033
+ interval: parseInt(intervalMatch[1], 10),
3034
+ unit: intervalMatch[2]
3035
+ };
3036
+ }
3037
+ const daysMatch = normalized.match(/^every\s+(.+)$/);
3038
+ if (daysMatch) {
3039
+ const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
3040
+ const days = [];
3041
+ for (const part of dayParts) {
3042
+ const dayNum = DAY_NAMES[part];
3043
+ if (dayNum !== undefined) {
3044
+ days.push(dayNum);
3045
+ }
3046
+ }
3047
+ if (days.length > 0) {
3048
+ return { type: "specific_days", days: days.sort((a, b) => a - b) };
3049
+ }
3050
+ }
3051
+ 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"`);
3052
+ }
3053
+ function nextOccurrence(rule, from) {
3054
+ const parsed = parseRecurrenceRule(rule);
3055
+ const base = from || new Date;
3056
+ if (parsed.type === "interval") {
3057
+ const next = new Date(base);
3058
+ if (parsed.unit === "day") {
3059
+ next.setDate(next.getDate() + parsed.interval);
3060
+ } else if (parsed.unit === "week") {
3061
+ next.setDate(next.getDate() + parsed.interval * 7);
3062
+ } else if (parsed.unit === "month") {
3063
+ next.setMonth(next.getMonth() + parsed.interval);
3064
+ }
3065
+ return next.toISOString();
3066
+ }
3067
+ if (parsed.type === "specific_days") {
3068
+ const currentDay = base.getDay();
3069
+ const days = parsed.days;
3070
+ let daysToAdd = Infinity;
3071
+ for (const day of days) {
3072
+ let diff = day - currentDay;
3073
+ if (diff <= 0)
3074
+ diff += 7;
3075
+ if (diff < daysToAdd)
3076
+ daysToAdd = diff;
3077
+ }
3078
+ const next = new Date(base);
3079
+ next.setDate(next.getDate() + daysToAdd);
3080
+ return next.toISOString();
3081
+ }
3082
+ throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
3083
+ }
3084
+ var DAY_NAMES;
3085
+ var init_recurrence = __esm(() => {
3086
+ DAY_NAMES = {
3087
+ sunday: 0,
3088
+ sun: 0,
3089
+ monday: 1,
3090
+ mon: 1,
3091
+ tuesday: 2,
3092
+ tue: 2,
3093
+ wednesday: 3,
3094
+ wed: 3,
3095
+ thursday: 4,
3096
+ thu: 4,
3097
+ friday: 5,
3098
+ fri: 5,
3099
+ saturday: 6,
3100
+ sat: 6
3101
+ };
3102
+ });
3103
+
3104
+ // src/db/webhooks.ts
3105
+ var exports_webhooks = {};
3106
+ __export(exports_webhooks, {
3107
+ listWebhooks: () => listWebhooks,
3108
+ getWebhook: () => getWebhook,
3109
+ dispatchWebhook: () => dispatchWebhook,
3110
+ deleteWebhook: () => deleteWebhook,
3111
+ createWebhook: () => createWebhook
3112
+ });
3113
+ function rowToWebhook(row) {
3114
+ return { ...row, events: JSON.parse(row.events || "[]"), active: !!row.active };
3115
+ }
3116
+ function createWebhook(input, db) {
3117
+ const d = db || getDatabase();
3118
+ const id = uuid();
3119
+ d.run(`INSERT INTO webhooks (id, url, events, secret, created_at) VALUES (?, ?, ?, ?, ?)`, [id, input.url, JSON.stringify(input.events || []), input.secret || null, now()]);
3120
+ return getWebhook(id, d);
3121
+ }
3122
+ function getWebhook(id, db) {
3123
+ const d = db || getDatabase();
3124
+ const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
3125
+ return row ? rowToWebhook(row) : null;
3126
+ }
3127
+ function listWebhooks(db) {
3128
+ const d = db || getDatabase();
3129
+ return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
3130
+ }
3131
+ function deleteWebhook(id, db) {
3132
+ const d = db || getDatabase();
3133
+ return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
3134
+ }
3135
+ async function dispatchWebhook(event, payload, db) {
3136
+ const webhooks = listWebhooks(db).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
3137
+ for (const wh of webhooks) {
3138
+ try {
3139
+ const body = JSON.stringify({ event, payload, timestamp: now() });
3140
+ const headers = { "Content-Type": "application/json" };
3141
+ if (wh.secret) {
3142
+ const encoder = new TextEncoder;
3143
+ const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
3144
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
3145
+ headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
3146
+ }
3147
+ fetch(wh.url, { method: "POST", headers, body }).catch(() => {});
3148
+ } catch {}
3149
+ }
3150
+ }
3151
+ var init_webhooks = __esm(() => {
3152
+ init_database();
3153
+ });
3154
+
2977
3155
  // src/db/tasks.ts
2978
3156
  function rowToTask(row) {
2979
3157
  return {
@@ -3005,8 +3183,8 @@ function createTask(input, db) {
3005
3183
  const tags = input.tags || [];
3006
3184
  const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
3007
3185
  const title = shortId ? `${shortId}: ${input.title}` : input.title;
3008
- 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)
3009
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)`, [
3186
+ 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)
3187
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3010
3188
  id,
3011
3189
  shortId,
3012
3190
  input.project_id || null,
@@ -3029,12 +3207,16 @@ function createTask(input, db) {
3029
3207
  input.estimated_minutes || null,
3030
3208
  input.requires_approval ? 1 : 0,
3031
3209
  null,
3032
- null
3210
+ null,
3211
+ input.recurrence_rule || null,
3212
+ input.recurrence_parent_id || null
3033
3213
  ]);
3034
3214
  if (tags.length > 0) {
3035
3215
  insertTaskTags(id, tags, d);
3036
3216
  }
3037
- return getTask(id, d);
3217
+ const task = getTask(id, d);
3218
+ dispatchWebhook("task.created", { id: task.id, short_id: task.short_id, title: task.title, status: task.status, priority: task.priority, project_id: task.project_id, assigned_to: task.assigned_to }, d).catch(() => {});
3219
+ return task;
3038
3220
  }
3039
3221
  function getTask(id, db) {
3040
3222
  const d = db || getDatabase();
@@ -3129,6 +3311,11 @@ function listTasks(filter = {}, db) {
3129
3311
  conditions.push("task_list_id = ?");
3130
3312
  params.push(filter.task_list_id);
3131
3313
  }
3314
+ if (filter.has_recurrence === true) {
3315
+ conditions.push("recurrence_rule IS NOT NULL");
3316
+ } else if (filter.has_recurrence === false) {
3317
+ conditions.push("recurrence_rule IS NULL");
3318
+ }
3132
3319
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3133
3320
  let limitClause = "";
3134
3321
  if (filter.limit) {
@@ -3144,6 +3331,69 @@ function listTasks(filter = {}, db) {
3144
3331
  created_at DESC${limitClause}`).all(...params);
3145
3332
  return rows.map(rowToTask);
3146
3333
  }
3334
+ function countTasks(filter = {}, db) {
3335
+ const d = db || getDatabase();
3336
+ const conditions = [];
3337
+ const params = [];
3338
+ if (filter.project_id) {
3339
+ conditions.push("project_id = ?");
3340
+ params.push(filter.project_id);
3341
+ }
3342
+ if (filter.parent_id !== undefined) {
3343
+ if (filter.parent_id === null) {
3344
+ conditions.push("parent_id IS NULL");
3345
+ } else {
3346
+ conditions.push("parent_id = ?");
3347
+ params.push(filter.parent_id);
3348
+ }
3349
+ }
3350
+ if (filter.status) {
3351
+ if (Array.isArray(filter.status)) {
3352
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
3353
+ params.push(...filter.status);
3354
+ } else {
3355
+ conditions.push("status = ?");
3356
+ params.push(filter.status);
3357
+ }
3358
+ }
3359
+ if (filter.priority) {
3360
+ if (Array.isArray(filter.priority)) {
3361
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
3362
+ params.push(...filter.priority);
3363
+ } else {
3364
+ conditions.push("priority = ?");
3365
+ params.push(filter.priority);
3366
+ }
3367
+ }
3368
+ if (filter.assigned_to) {
3369
+ conditions.push("assigned_to = ?");
3370
+ params.push(filter.assigned_to);
3371
+ }
3372
+ if (filter.agent_id) {
3373
+ conditions.push("agent_id = ?");
3374
+ params.push(filter.agent_id);
3375
+ }
3376
+ if (filter.session_id) {
3377
+ conditions.push("session_id = ?");
3378
+ params.push(filter.session_id);
3379
+ }
3380
+ if (filter.tags && filter.tags.length > 0) {
3381
+ const placeholders = filter.tags.map(() => "?").join(",");
3382
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
3383
+ params.push(...filter.tags);
3384
+ }
3385
+ if (filter.plan_id) {
3386
+ conditions.push("plan_id = ?");
3387
+ params.push(filter.plan_id);
3388
+ }
3389
+ if (filter.task_list_id) {
3390
+ conditions.push("task_list_id = ?");
3391
+ params.push(filter.task_list_id);
3392
+ }
3393
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3394
+ const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
3395
+ return row.count;
3396
+ }
3147
3397
  function updateTask(id, input, db) {
3148
3398
  const d = db || getDatabase();
3149
3399
  const task = getTask(id, d);
@@ -3215,6 +3465,10 @@ function updateTask(id, input, db) {
3215
3465
  sets.push("approved_at = ?");
3216
3466
  params.push(now());
3217
3467
  }
3468
+ if (input.recurrence_rule !== undefined) {
3469
+ sets.push("recurrence_rule = ?");
3470
+ params.push(input.recurrence_rule);
3471
+ }
3218
3472
  params.push(id, input.version);
3219
3473
  const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
3220
3474
  if (result.changes === 0) {
@@ -3235,6 +3489,12 @@ function updateTask(id, input, db) {
3235
3489
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
3236
3490
  if (input.approved_by !== undefined)
3237
3491
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
3492
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
3493
+ dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
3494
+ }
3495
+ if (input.status !== undefined && input.status !== task.status) {
3496
+ dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
3497
+ }
3238
3498
  return {
3239
3499
  ...task,
3240
3500
  ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
@@ -3286,9 +3546,10 @@ function startTask(id, agentId, db) {
3286
3546
  }
3287
3547
  }
3288
3548
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
3549
+ dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
3289
3550
  return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
3290
3551
  }
3291
- function completeTask(id, agentId, db, evidence) {
3552
+ function completeTask(id, agentId, db, options) {
3292
3553
  const d = db || getDatabase();
3293
3554
  const task = getTask(id, d);
3294
3555
  if (!task)
@@ -3297,7 +3558,9 @@ function completeTask(id, agentId, db, evidence) {
3297
3558
  throw new LockError(id, task.locked_by);
3298
3559
  }
3299
3560
  checkCompletionGuard(task, agentId || null, d);
3300
- if (evidence) {
3561
+ const evidence = options ? { files_changed: options.files_changed, test_results: options.test_results, commit_hash: options.commit_hash, notes: options.notes } : undefined;
3562
+ const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes);
3563
+ if (hasEvidence) {
3301
3564
  const meta2 = { ...task.metadata, _evidence: evidence };
3302
3565
  d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
3303
3566
  }
@@ -3305,7 +3568,15 @@ function completeTask(id, agentId, db, evidence) {
3305
3568
  d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
3306
3569
  WHERE id = ?`, [timestamp, timestamp, id]);
3307
3570
  logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
3308
- const meta = evidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
3571
+ dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
3572
+ let spawnedTask = null;
3573
+ if (task.recurrence_rule && !options?.skip_recurrence) {
3574
+ spawnedTask = spawnNextRecurrence(task, d);
3575
+ }
3576
+ const meta = hasEvidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
3577
+ if (spawnedTask) {
3578
+ meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
3579
+ }
3309
3580
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
3310
3581
  }
3311
3582
  function lockTask(id, agentId, db) {
@@ -3368,6 +3639,298 @@ function getTaskDependencies(taskId, db) {
3368
3639
  const d = db || getDatabase();
3369
3640
  return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
3370
3641
  }
3642
+ function cloneTask(taskId, overrides, db) {
3643
+ const d = db || getDatabase();
3644
+ const source = getTask(taskId, d);
3645
+ if (!source)
3646
+ throw new TaskNotFoundError(taskId);
3647
+ const input = {
3648
+ title: overrides?.title ?? source.title,
3649
+ description: overrides?.description ?? source.description ?? undefined,
3650
+ priority: overrides?.priority ?? source.priority,
3651
+ project_id: overrides?.project_id ?? source.project_id ?? undefined,
3652
+ parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
3653
+ plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
3654
+ task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
3655
+ status: overrides?.status ?? "pending",
3656
+ agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
3657
+ assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
3658
+ tags: overrides?.tags ?? source.tags,
3659
+ metadata: overrides?.metadata ?? source.metadata,
3660
+ estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
3661
+ recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
3662
+ };
3663
+ return createTask(input, d);
3664
+ }
3665
+ function getTaskGraph(taskId, direction = "both", db) {
3666
+ const d = db || getDatabase();
3667
+ const task = getTask(taskId, d);
3668
+ if (!task)
3669
+ throw new TaskNotFoundError(taskId);
3670
+ function toNode(t) {
3671
+ const deps = getTaskDependencies(t.id, d);
3672
+ const hasUnfinishedDeps = deps.some((dep) => {
3673
+ const depTask = getTask(dep.depends_on, d);
3674
+ return depTask && depTask.status !== "completed";
3675
+ });
3676
+ return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
3677
+ }
3678
+ function buildUp(id, visited) {
3679
+ if (visited.has(id))
3680
+ return [];
3681
+ visited.add(id);
3682
+ const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
3683
+ return deps.map((dep) => {
3684
+ const depTask = getTask(dep.depends_on, d);
3685
+ if (!depTask)
3686
+ return null;
3687
+ return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
3688
+ }).filter(Boolean);
3689
+ }
3690
+ function buildDown(id, visited) {
3691
+ if (visited.has(id))
3692
+ return [];
3693
+ visited.add(id);
3694
+ const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
3695
+ return dependents.map((dep) => {
3696
+ const depTask = getTask(dep.task_id, d);
3697
+ if (!depTask)
3698
+ return null;
3699
+ return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
3700
+ }).filter(Boolean);
3701
+ }
3702
+ const rootNode = toNode(task);
3703
+ const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
3704
+ const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
3705
+ return { task: rootNode, depends_on, blocks };
3706
+ }
3707
+ function moveTask(taskId, target, db) {
3708
+ const d = db || getDatabase();
3709
+ const task = getTask(taskId, d);
3710
+ if (!task)
3711
+ throw new TaskNotFoundError(taskId);
3712
+ const sets = ["updated_at = ?", "version = version + 1"];
3713
+ const params = [now()];
3714
+ if (target.task_list_id !== undefined) {
3715
+ sets.push("task_list_id = ?");
3716
+ params.push(target.task_list_id);
3717
+ }
3718
+ if (target.project_id !== undefined) {
3719
+ sets.push("project_id = ?");
3720
+ params.push(target.project_id);
3721
+ }
3722
+ if (target.plan_id !== undefined) {
3723
+ sets.push("plan_id = ?");
3724
+ params.push(target.plan_id);
3725
+ }
3726
+ params.push(taskId);
3727
+ d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
3728
+ return getTask(taskId, d);
3729
+ }
3730
+ function spawnNextRecurrence(completedTask, db) {
3731
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
3732
+ let title = completedTask.title;
3733
+ if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
3734
+ title = title.slice(completedTask.short_id.length + 2);
3735
+ }
3736
+ const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
3737
+ return createTask({
3738
+ title,
3739
+ description: completedTask.description ?? undefined,
3740
+ priority: completedTask.priority,
3741
+ project_id: completedTask.project_id ?? undefined,
3742
+ task_list_id: completedTask.task_list_id ?? undefined,
3743
+ plan_id: completedTask.plan_id ?? undefined,
3744
+ assigned_to: completedTask.assigned_to ?? undefined,
3745
+ tags: completedTask.tags,
3746
+ metadata: completedTask.metadata,
3747
+ estimated_minutes: completedTask.estimated_minutes ?? undefined,
3748
+ recurrence_rule: completedTask.recurrence_rule,
3749
+ recurrence_parent_id: recurrenceParentId,
3750
+ due_at: dueAt
3751
+ }, db);
3752
+ }
3753
+ function claimNextTask(agentId, filters, db) {
3754
+ const d = db || getDatabase();
3755
+ const tx = d.transaction(() => {
3756
+ const task = getNextTask(agentId, filters, d);
3757
+ if (!task)
3758
+ return null;
3759
+ return startTask(task.id, agentId, d);
3760
+ });
3761
+ return tx();
3762
+ }
3763
+ function getNextTask(agentId, filters, db) {
3764
+ const d = db || getDatabase();
3765
+ clearExpiredLocks(d);
3766
+ const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
3767
+ const params = [lockExpiryCutoff()];
3768
+ if (filters?.project_id) {
3769
+ conditions.push("project_id = ?");
3770
+ params.push(filters.project_id);
3771
+ }
3772
+ if (filters?.task_list_id) {
3773
+ conditions.push("task_list_id = ?");
3774
+ params.push(filters.task_list_id);
3775
+ }
3776
+ if (filters?.plan_id) {
3777
+ conditions.push("plan_id = ?");
3778
+ params.push(filters.plan_id);
3779
+ }
3780
+ if (filters?.tags && filters.tags.length > 0) {
3781
+ const placeholders = filters.tags.map(() => "?").join(",");
3782
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
3783
+ params.push(...filters.tags);
3784
+ }
3785
+ 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')");
3786
+ const where = conditions.join(" AND ");
3787
+ let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
3788
+ if (agentId) {
3789
+ sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
3790
+ params.push(agentId);
3791
+ }
3792
+ 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`;
3793
+ const row = d.query(sql).get(...params);
3794
+ return row ? rowToTask(row) : null;
3795
+ }
3796
+ function getActiveWork(filters, db) {
3797
+ const d = db || getDatabase();
3798
+ clearExpiredLocks(d);
3799
+ const conditions = ["status = 'in_progress'"];
3800
+ const params = [];
3801
+ if (filters?.project_id) {
3802
+ conditions.push("project_id = ?");
3803
+ params.push(filters.project_id);
3804
+ }
3805
+ if (filters?.task_list_id) {
3806
+ conditions.push("task_list_id = ?");
3807
+ params.push(filters.task_list_id);
3808
+ }
3809
+ const where = conditions.join(" AND ");
3810
+ const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
3811
+ CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
3812
+ updated_at DESC`).all(...params);
3813
+ return rows;
3814
+ }
3815
+ function getTasksChangedSince(since, filters, db) {
3816
+ const d = db || getDatabase();
3817
+ const conditions = ["updated_at > ?"];
3818
+ const params = [since];
3819
+ if (filters?.project_id) {
3820
+ conditions.push("project_id = ?");
3821
+ params.push(filters.project_id);
3822
+ }
3823
+ if (filters?.task_list_id) {
3824
+ conditions.push("task_list_id = ?");
3825
+ params.push(filters.task_list_id);
3826
+ }
3827
+ const where = conditions.join(" AND ");
3828
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
3829
+ return rows.map(rowToTask);
3830
+ }
3831
+ function failTask(id, agentId, reason, options, db) {
3832
+ const d = db || getDatabase();
3833
+ const task = getTask(id, d);
3834
+ if (!task)
3835
+ throw new TaskNotFoundError(id);
3836
+ const meta = {
3837
+ ...task.metadata,
3838
+ _failure: {
3839
+ reason: reason || "Unknown failure",
3840
+ error_code: options?.error_code || null,
3841
+ failed_by: agentId || null,
3842
+ failed_at: now(),
3843
+ retry_requested: options?.retry || false
3844
+ }
3845
+ };
3846
+ const timestamp = now();
3847
+ d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
3848
+ WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
3849
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
3850
+ dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
3851
+ const failedTask = {
3852
+ ...task,
3853
+ status: "failed",
3854
+ locked_by: null,
3855
+ locked_at: null,
3856
+ metadata: meta,
3857
+ version: task.version + 1,
3858
+ updated_at: timestamp
3859
+ };
3860
+ let retryTask;
3861
+ if (options?.retry) {
3862
+ let title = task.title;
3863
+ if (task.short_id && title.startsWith(task.short_id + ": ")) {
3864
+ title = title.slice(task.short_id.length + 2);
3865
+ }
3866
+ retryTask = createTask({
3867
+ title,
3868
+ description: task.description ?? undefined,
3869
+ priority: task.priority,
3870
+ project_id: task.project_id ?? undefined,
3871
+ task_list_id: task.task_list_id ?? undefined,
3872
+ plan_id: task.plan_id ?? undefined,
3873
+ assigned_to: task.assigned_to ?? undefined,
3874
+ tags: task.tags,
3875
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_after: options.retry_after || null, failure_reason: reason } },
3876
+ estimated_minutes: task.estimated_minutes ?? undefined,
3877
+ recurrence_rule: task.recurrence_rule ?? undefined,
3878
+ due_at: options.retry_after || task.due_at || undefined
3879
+ }, d);
3880
+ }
3881
+ return { task: failedTask, retryTask };
3882
+ }
3883
+ function getStaleTasks(staleMinutes = 30, filters, db) {
3884
+ const d = db || getDatabase();
3885
+ const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
3886
+ const conditions = [
3887
+ "status = 'in_progress'",
3888
+ "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
3889
+ ];
3890
+ const params = [cutoff, cutoff];
3891
+ if (filters?.project_id) {
3892
+ conditions.push("project_id = ?");
3893
+ params.push(filters.project_id);
3894
+ }
3895
+ if (filters?.task_list_id) {
3896
+ conditions.push("task_list_id = ?");
3897
+ params.push(filters.task_list_id);
3898
+ }
3899
+ const where = conditions.join(" AND ");
3900
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
3901
+ return rows.map(rowToTask);
3902
+ }
3903
+ function getStatus(filters, agentId, db) {
3904
+ const d = db || getDatabase();
3905
+ const pending = countTasks({ ...filters, status: "pending" }, d);
3906
+ const in_progress = countTasks({ ...filters, status: "in_progress" }, d);
3907
+ const completed = countTasks({ ...filters, status: "completed" }, d);
3908
+ const total = countTasks(filters || {}, d);
3909
+ const active_work = getActiveWork(filters, d);
3910
+ const next_task = getNextTask(agentId, filters, d);
3911
+ const stale = getStaleTasks(30, filters, d);
3912
+ const conditions = ["recurrence_rule IS NOT NULL", "status = 'pending'", "due_at < ?"];
3913
+ const params = [now()];
3914
+ if (filters?.project_id) {
3915
+ conditions.push("project_id = ?");
3916
+ params.push(filters.project_id);
3917
+ }
3918
+ if (filters?.task_list_id) {
3919
+ conditions.push("task_list_id = ?");
3920
+ params.push(filters.task_list_id);
3921
+ }
3922
+ const overdueRow = d.query(`SELECT COUNT(*) as count FROM tasks WHERE ${conditions.join(" AND ")}`).get(...params);
3923
+ return {
3924
+ pending,
3925
+ in_progress,
3926
+ completed,
3927
+ total,
3928
+ active_work,
3929
+ next_task,
3930
+ stale_count: stale.length,
3931
+ overdue_recurring: overdueRow.count
3932
+ };
3933
+ }
3371
3934
  function wouldCreateCycle(taskId, dependsOn, db) {
3372
3935
  const visited = new Set;
3373
3936
  const queue = [dependsOn];
@@ -3385,12 +3948,99 @@ function wouldCreateCycle(taskId, dependsOn, db) {
3385
3948
  }
3386
3949
  return false;
3387
3950
  }
3951
+ function getTaskStats(filters, db) {
3952
+ const d = db || getDatabase();
3953
+ const conditions = [];
3954
+ const params = [];
3955
+ if (filters?.project_id) {
3956
+ conditions.push("project_id = ?");
3957
+ params.push(filters.project_id);
3958
+ }
3959
+ if (filters?.task_list_id) {
3960
+ conditions.push("task_list_id = ?");
3961
+ params.push(filters.task_list_id);
3962
+ }
3963
+ if (filters?.agent_id) {
3964
+ conditions.push("(agent_id = ? OR assigned_to = ?)");
3965
+ params.push(filters.agent_id, filters.agent_id);
3966
+ }
3967
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3968
+ const totalRow = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
3969
+ const statusRows = d.query(`SELECT status, COUNT(*) as count FROM tasks ${where} GROUP BY status`).all(...params);
3970
+ const by_status = {};
3971
+ for (const r of statusRows)
3972
+ by_status[r.status] = r.count;
3973
+ const priorityRows = d.query(`SELECT priority, COUNT(*) as count FROM tasks ${where} GROUP BY priority`).all(...params);
3974
+ const by_priority = {};
3975
+ for (const r of priorityRows)
3976
+ by_priority[r.priority] = r.count;
3977
+ const agentRows = d.query(`SELECT COALESCE(assigned_to, agent_id, 'unassigned') as agent, COUNT(*) as count FROM tasks ${where} GROUP BY agent`).all(...params);
3978
+ const by_agent = {};
3979
+ for (const r of agentRows)
3980
+ by_agent[r.agent] = r.count;
3981
+ const completed = by_status["completed"] || 0;
3982
+ const completion_rate = totalRow.count > 0 ? Math.round(completed / totalRow.count * 100) : 0;
3983
+ return { total: totalRow.count, by_status, by_priority, completion_rate, by_agent };
3984
+ }
3985
+ function bulkCreateTasks(inputs, db) {
3986
+ const d = db || getDatabase();
3987
+ const tempIdToRealId = new Map;
3988
+ const created = [];
3989
+ const tx = d.transaction(() => {
3990
+ for (const input of inputs) {
3991
+ const { temp_id, depends_on_temp_ids: _deps, ...createInput } = input;
3992
+ const task = createTask(createInput, d);
3993
+ if (temp_id)
3994
+ tempIdToRealId.set(temp_id, task.id);
3995
+ created.push({ temp_id: temp_id || null, id: task.id, short_id: task.short_id, title: task.title });
3996
+ }
3997
+ for (const input of inputs) {
3998
+ if (input.depends_on_temp_ids && input.depends_on_temp_ids.length > 0) {
3999
+ const taskId = input.temp_id ? tempIdToRealId.get(input.temp_id) : null;
4000
+ if (!taskId)
4001
+ continue;
4002
+ for (const depTempId of input.depends_on_temp_ids) {
4003
+ const depRealId = tempIdToRealId.get(depTempId);
4004
+ if (depRealId) {
4005
+ addDependency(taskId, depRealId, d);
4006
+ }
4007
+ }
4008
+ }
4009
+ }
4010
+ });
4011
+ tx();
4012
+ return { created };
4013
+ }
4014
+ function bulkUpdateTasks(taskIds, updates, db) {
4015
+ const d = db || getDatabase();
4016
+ let updated = 0;
4017
+ const failed = [];
4018
+ const tx = d.transaction(() => {
4019
+ for (const id of taskIds) {
4020
+ try {
4021
+ const task = getTask(id, d);
4022
+ if (!task) {
4023
+ failed.push({ id, error: "Task not found" });
4024
+ continue;
4025
+ }
4026
+ updateTask(id, { ...updates, version: task.version }, d);
4027
+ updated++;
4028
+ } catch (e) {
4029
+ failed.push({ id, error: e instanceof Error ? e.message : String(e) });
4030
+ }
4031
+ }
4032
+ });
4033
+ tx();
4034
+ return { updated, failed };
4035
+ }
3388
4036
  var init_tasks = __esm(() => {
3389
4037
  init_types();
3390
4038
  init_database();
3391
4039
  init_projects();
3392
4040
  init_completion_guard();
3393
4041
  init_audit();
4042
+ init_recurrence();
4043
+ init_webhooks();
3394
4044
  });
3395
4045
 
3396
4046
  // src/db/agents.ts
@@ -3715,19 +4365,64 @@ function rowToTask2(row) {
3715
4365
  requires_approval: Boolean(row.requires_approval)
3716
4366
  };
3717
4367
  }
3718
- function searchTasks(query, projectId, taskListId, db) {
4368
+ function searchTasks(options, projectId, taskListId, db) {
4369
+ const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
3719
4370
  const d = db || getDatabase();
3720
4371
  clearExpiredLocks(d);
3721
- const pattern = `%${query}%`;
4372
+ const pattern = `%${opts.query}%`;
3722
4373
  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 ?))`;
3723
4374
  const params = [pattern, pattern, pattern];
3724
- if (projectId) {
4375
+ if (opts.project_id) {
3725
4376
  sql += " AND project_id = ?";
3726
- params.push(projectId);
4377
+ params.push(opts.project_id);
3727
4378
  }
3728
- if (taskListId) {
4379
+ if (opts.task_list_id) {
3729
4380
  sql += " AND task_list_id = ?";
3730
- params.push(taskListId);
4381
+ params.push(opts.task_list_id);
4382
+ }
4383
+ if (opts.status) {
4384
+ if (Array.isArray(opts.status)) {
4385
+ sql += ` AND status IN (${opts.status.map(() => "?").join(",")})`;
4386
+ params.push(...opts.status);
4387
+ } else {
4388
+ sql += " AND status = ?";
4389
+ params.push(opts.status);
4390
+ }
4391
+ }
4392
+ if (opts.priority) {
4393
+ if (Array.isArray(opts.priority)) {
4394
+ sql += ` AND priority IN (${opts.priority.map(() => "?").join(",")})`;
4395
+ params.push(...opts.priority);
4396
+ } else {
4397
+ sql += " AND priority = ?";
4398
+ params.push(opts.priority);
4399
+ }
4400
+ }
4401
+ if (opts.assigned_to) {
4402
+ sql += " AND assigned_to = ?";
4403
+ params.push(opts.assigned_to);
4404
+ }
4405
+ if (opts.agent_id) {
4406
+ sql += " AND agent_id = ?";
4407
+ params.push(opts.agent_id);
4408
+ }
4409
+ if (opts.created_after) {
4410
+ sql += " AND created_at > ?";
4411
+ params.push(opts.created_after);
4412
+ }
4413
+ if (opts.updated_after) {
4414
+ sql += " AND updated_at > ?";
4415
+ params.push(opts.updated_after);
4416
+ }
4417
+ if (opts.has_dependencies === true) {
4418
+ sql += " AND id IN (SELECT task_id FROM task_dependencies)";
4419
+ } else if (opts.has_dependencies === false) {
4420
+ sql += " AND id NOT IN (SELECT task_id FROM task_dependencies)";
4421
+ }
4422
+ if (opts.is_blocked === true) {
4423
+ 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')";
4424
+ } else if (opts.is_blocked === false) {
4425
+ 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')";
3731
4426
  }
3732
4427
  sql += ` ORDER BY
3733
4428
  CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
@@ -8304,81 +8999,43 @@ var init_zod = __esm(() => {
8304
8999
  init_external();
8305
9000
  });
8306
9001
 
8307
- // src/db/webhooks.ts
8308
- var exports_webhooks = {};
8309
- __export(exports_webhooks, {
8310
- listWebhooks: () => listWebhooks,
8311
- getWebhook: () => getWebhook,
8312
- dispatchWebhook: () => dispatchWebhook,
8313
- deleteWebhook: () => deleteWebhook,
8314
- createWebhook: () => createWebhook
8315
- });
8316
- function rowToWebhook(row) {
8317
- return { ...row, events: JSON.parse(row.events || "[]"), active: !!row.active };
8318
- }
8319
- function createWebhook(input, db) {
8320
- const d = db || getDatabase();
8321
- const id = uuid();
8322
- d.run(`INSERT INTO webhooks (id, url, events, secret, created_at) VALUES (?, ?, ?, ?, ?)`, [id, input.url, JSON.stringify(input.events || []), input.secret || null, now()]);
8323
- return getWebhook(id, d);
8324
- }
8325
- function getWebhook(id, db) {
8326
- const d = db || getDatabase();
8327
- const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
8328
- return row ? rowToWebhook(row) : null;
8329
- }
8330
- function listWebhooks(db) {
8331
- const d = db || getDatabase();
8332
- return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
8333
- }
8334
- function deleteWebhook(id, db) {
8335
- const d = db || getDatabase();
8336
- return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
8337
- }
8338
- async function dispatchWebhook(event, payload, db) {
8339
- const webhooks = listWebhooks(db).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
8340
- for (const wh of webhooks) {
8341
- try {
8342
- const body = JSON.stringify({ event, payload, timestamp: now() });
8343
- const headers = { "Content-Type": "application/json" };
8344
- if (wh.secret) {
8345
- const encoder = new TextEncoder;
8346
- const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
8347
- const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
8348
- headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
8349
- }
8350
- fetch(wh.url, { method: "POST", headers, body }).catch(() => {});
8351
- } catch {}
8352
- }
8353
- }
8354
- var init_webhooks = __esm(() => {
8355
- init_database();
8356
- });
8357
-
8358
9002
  // src/mcp/index.ts
8359
9003
  var exports_mcp = {};
8360
9004
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8361
9005
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8362
9006
  function formatError(error) {
8363
- if (error instanceof VersionConflictError)
8364
- return `Version conflict: ${error.message}`;
8365
- if (error instanceof TaskNotFoundError)
8366
- return `Not found: ${error.message}`;
8367
- if (error instanceof PlanNotFoundError)
8368
- return `Not found: ${error.message}`;
8369
- if (error instanceof TaskListNotFoundError)
8370
- return `Not found: ${error.message}`;
8371
- if (error instanceof LockError)
8372
- return `Lock error: ${error.message}`;
8373
- if (error instanceof DependencyCycleError)
8374
- return `Dependency cycle: ${error.message}`;
9007
+ if (error instanceof VersionConflictError) {
9008
+ return JSON.stringify({ code: VersionConflictError.code, message: error.message, suggestion: VersionConflictError.suggestion });
9009
+ }
9010
+ if (error instanceof TaskNotFoundError) {
9011
+ return JSON.stringify({ code: TaskNotFoundError.code, message: error.message, suggestion: TaskNotFoundError.suggestion });
9012
+ }
9013
+ if (error instanceof ProjectNotFoundError) {
9014
+ return JSON.stringify({ code: ProjectNotFoundError.code, message: error.message, suggestion: ProjectNotFoundError.suggestion });
9015
+ }
9016
+ if (error instanceof PlanNotFoundError) {
9017
+ return JSON.stringify({ code: PlanNotFoundError.code, message: error.message, suggestion: PlanNotFoundError.suggestion });
9018
+ }
9019
+ if (error instanceof TaskListNotFoundError) {
9020
+ return JSON.stringify({ code: TaskListNotFoundError.code, message: error.message, suggestion: TaskListNotFoundError.suggestion });
9021
+ }
9022
+ if (error instanceof LockError) {
9023
+ return JSON.stringify({ code: LockError.code, message: error.message, suggestion: LockError.suggestion });
9024
+ }
9025
+ if (error instanceof AgentNotFoundError) {
9026
+ return JSON.stringify({ code: AgentNotFoundError.code, message: error.message, suggestion: AgentNotFoundError.suggestion });
9027
+ }
9028
+ if (error instanceof DependencyCycleError) {
9029
+ return JSON.stringify({ code: DependencyCycleError.code, message: error.message, suggestion: DependencyCycleError.suggestion });
9030
+ }
8375
9031
  if (error instanceof CompletionGuardError) {
8376
- const retry = error.retryAfterSeconds ? ` (retry after ${error.retryAfterSeconds}s)` : "";
8377
- return `Completion blocked: ${error.reason}${retry}`;
9032
+ const retry = error.retryAfterSeconds ? { retryAfterSeconds: error.retryAfterSeconds } : {};
9033
+ return JSON.stringify({ code: CompletionGuardError.code, message: error.reason, suggestion: CompletionGuardError.suggestion, ...retry });
9034
+ }
9035
+ if (error instanceof Error) {
9036
+ return JSON.stringify({ code: "UNKNOWN_ERROR", message: error.message });
8378
9037
  }
8379
- if (error instanceof Error)
8380
- return error.message;
8381
- return String(error);
9038
+ return JSON.stringify({ code: "UNKNOWN_ERROR", message: String(error) });
8382
9039
  }
8383
9040
  function resolveId(partialId, table = "tasks") {
8384
9041
  const db = getDatabase();
@@ -8401,7 +9058,8 @@ function formatTask(task) {
8401
9058
  const id = task.short_id || task.id.slice(0, 8);
8402
9059
  const assigned = task.assigned_to ? ` -> ${task.assigned_to}` : "";
8403
9060
  const lock = task.locked_by ? ` [locked:${task.locked_by}]` : "";
8404
- return `${id} ${task.status.padEnd(11)} ${task.priority.padEnd(8)} ${task.title}${assigned}${lock}`;
9061
+ const recur = task.recurrence_rule ? ` [\u21BB]` : "";
9062
+ return `${id} ${task.status.padEnd(11)} ${task.priority.padEnd(8)} ${task.title}${assigned}${lock}${recur}`;
8405
9063
  }
8406
9064
  function formatTaskDetail(task) {
8407
9065
  const parts = [
@@ -8426,6 +9084,10 @@ function formatTaskDetail(task) {
8426
9084
  parts.push(`Plan: ${task.plan_id}`);
8427
9085
  if (task.tags.length > 0)
8428
9086
  parts.push(`Tags: ${task.tags.join(", ")}`);
9087
+ if (task.recurrence_rule)
9088
+ parts.push(`Recurrence: ${task.recurrence_rule}`);
9089
+ if (task.recurrence_parent_id)
9090
+ parts.push(`Recurrence parent: ${task.recurrence_parent_id}`);
8429
9091
  parts.push(`Version: ${task.version}`);
8430
9092
  parts.push(`Created: ${task.created_at}`);
8431
9093
  if (task.completed_at)
@@ -8453,7 +9115,7 @@ var init_mcp = __esm(() => {
8453
9115
  init_types();
8454
9116
  server = new McpServer({
8455
9117
  name: "todos",
8456
- version: "0.9.33"
9118
+ version: "0.9.35"
8457
9119
  });
8458
9120
  server.tool("create_task", "Create a new task", {
8459
9121
  title: exports_external.string(),
@@ -8471,7 +9133,8 @@ var init_mcp = __esm(() => {
8471
9133
  tags: exports_external.array(exports_external.string()).optional(),
8472
9134
  metadata: exports_external.record(exports_external.unknown()).optional(),
8473
9135
  estimated_minutes: exports_external.number().optional(),
8474
- requires_approval: exports_external.boolean().optional()
9136
+ requires_approval: exports_external.boolean().optional(),
9137
+ recurrence_rule: exports_external.string().optional()
8475
9138
  }, async (params) => {
8476
9139
  try {
8477
9140
  const resolved = { ...params };
@@ -8489,7 +9152,7 @@ var init_mcp = __esm(() => {
8489
9152
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8490
9153
  }
8491
9154
  });
8492
- server.tool("list_tasks", "List tasks with optional filters", {
9155
+ server.tool("list_tasks", "List tasks with optional filters and pagination.", {
8493
9156
  project_id: exports_external.string().optional(),
8494
9157
  status: exports_external.union([
8495
9158
  exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]),
@@ -8502,7 +9165,10 @@ var init_mcp = __esm(() => {
8502
9165
  assigned_to: exports_external.string().optional(),
8503
9166
  tags: exports_external.array(exports_external.string()).optional(),
8504
9167
  plan_id: exports_external.string().optional(),
8505
- task_list_id: exports_external.string().optional()
9168
+ task_list_id: exports_external.string().optional(),
9169
+ has_recurrence: exports_external.boolean().optional(),
9170
+ limit: exports_external.number().optional(),
9171
+ offset: exports_external.number().optional()
8506
9172
  }, async (params) => {
8507
9173
  try {
8508
9174
  const resolved = { ...params };
@@ -8513,8 +9179,10 @@ var init_mcp = __esm(() => {
8513
9179
  if (resolved.task_list_id)
8514
9180
  resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
8515
9181
  const tasks = listTasks(resolved);
9182
+ const { limit: _limit, offset: _offset, ...countFilter } = resolved;
9183
+ const total = countTasks(countFilter);
8516
9184
  if (tasks.length === 0) {
8517
- return { content: [{ type: "text", text: "No tasks found." }] };
9185
+ return { content: [{ type: "text", text: total > 0 ? `No tasks in this page (total: ${total}).` : "No tasks found." }] };
8518
9186
  }
8519
9187
  const text = tasks.map((t) => {
8520
9188
  const lock = t.locked_by ? ` [locked by ${t.locked_by}]` : "";
@@ -8522,13 +9190,15 @@ var init_mcp = __esm(() => {
8522
9190
  return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned}${lock}`;
8523
9191
  }).join(`
8524
9192
  `);
9193
+ const pagination = resolved.limit ? `
9194
+ (showing ${tasks.length} of ${total}, offset: ${resolved.offset || 0})` : "";
8525
9195
  return { content: [{ type: "text", text: `${tasks.length} task(s):
8526
- ${text}` }] };
9196
+ ${text}${pagination}` }] };
8527
9197
  } catch (e) {
8528
9198
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8529
9199
  }
8530
9200
  });
8531
- server.tool("get_task", "Get full task details with relations", {
9201
+ server.tool("get_task", "Get full task details with subtasks, deps, and comments.", {
8532
9202
  id: exports_external.string()
8533
9203
  }, async ({ id }) => {
8534
9204
  try {
@@ -8597,7 +9267,7 @@ Parent: ${task.parent.id.slice(0, 8)} | ${task.parent.title}`);
8597
9267
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8598
9268
  }
8599
9269
  });
8600
- server.tool("delete_task", "Delete a task permanently", {
9270
+ server.tool("delete_task", "Delete a task permanently. Subtasks cascade-deleted.", {
8601
9271
  id: exports_external.string()
8602
9272
  }, async ({ id }) => {
8603
9273
  try {
@@ -8613,7 +9283,7 @@ Parent: ${task.parent.id.slice(0, 8)} | ${task.parent.title}`);
8613
9283
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8614
9284
  }
8615
9285
  });
8616
- server.tool("start_task", "Claim, lock, and set task status to in_progress.", {
9286
+ server.tool("start_task", "Claim, lock, and set task to in_progress.", {
8617
9287
  id: exports_external.string(),
8618
9288
  agent_id: exports_external.string()
8619
9289
  }, async ({ id, agent_id }) => {
@@ -8625,19 +9295,26 @@ Parent: ${task.parent.id.slice(0, 8)} | ${task.parent.title}`);
8625
9295
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8626
9296
  }
8627
9297
  });
8628
- server.tool("complete_task", "Mark task completed and release lock.", {
9298
+ server.tool("complete_task", "Complete a task. For recurring tasks, auto-spawns next instance.", {
8629
9299
  id: exports_external.string(),
8630
- agent_id: exports_external.string().optional()
8631
- }, async ({ id, agent_id }) => {
9300
+ agent_id: exports_external.string().optional(),
9301
+ skip_recurrence: exports_external.boolean().optional()
9302
+ }, async ({ id, agent_id, skip_recurrence }) => {
8632
9303
  try {
8633
9304
  const resolvedId = resolveId(id);
8634
- const task = completeTask(resolvedId, agent_id);
8635
- return { content: [{ type: "text", text: `completed: ${formatTask(task)}` }] };
9305
+ const task = completeTask(resolvedId, agent_id, undefined, { skip_recurrence });
9306
+ let text = `completed: ${formatTask(task)}`;
9307
+ if (task.metadata._next_recurrence) {
9308
+ const next = task.metadata._next_recurrence;
9309
+ text += `
9310
+ next: ${next.short_id || next.id.slice(0, 8)} due ${next.due_at}`;
9311
+ }
9312
+ return { content: [{ type: "text", text }] };
8636
9313
  } catch (e) {
8637
9314
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8638
9315
  }
8639
9316
  });
8640
- server.tool("lock_task", "Acquire exclusive lock on a task", {
9317
+ server.tool("lock_task", "Acquire exclusive lock. Expires after 30 min. Idempotent per agent.", {
8641
9318
  id: exports_external.string(),
8642
9319
  agent_id: exports_external.string()
8643
9320
  }, async ({ id, agent_id }) => {
@@ -8652,7 +9329,7 @@ Parent: ${task.parent.id.slice(0, 8)} | ${task.parent.title}`);
8652
9329
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8653
9330
  }
8654
9331
  });
8655
- server.tool("unlock_task", "Release exclusive lock on a task", {
9332
+ server.tool("unlock_task", "Release exclusive lock on a task.", {
8656
9333
  id: exports_external.string(),
8657
9334
  agent_id: exports_external.string().optional()
8658
9335
  }, async ({ id, agent_id }) => {
@@ -8664,7 +9341,7 @@ Parent: ${task.parent.id.slice(0, 8)} | ${task.parent.title}`);
8664
9341
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8665
9342
  }
8666
9343
  });
8667
- server.tool("add_dependency", "Add a dependency: task_id depends on depends_on.", {
9344
+ server.tool("add_dependency", "Add a dependency. Prevents cycles via BFS detection.", {
8668
9345
  task_id: exports_external.string(),
8669
9346
  depends_on: exports_external.string()
8670
9347
  }, async ({ task_id, depends_on }) => {
@@ -8677,7 +9354,7 @@ Parent: ${task.parent.id.slice(0, 8)} | ${task.parent.title}`);
8677
9354
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8678
9355
  }
8679
9356
  });
8680
- server.tool("remove_dependency", "Remove a dependency between tasks", {
9357
+ server.tool("remove_dependency", "Remove a dependency link between two tasks.", {
8681
9358
  task_id: exports_external.string(),
8682
9359
  depends_on: exports_external.string()
8683
9360
  }, async ({ task_id, depends_on }) => {
@@ -8695,7 +9372,7 @@ Parent: ${task.parent.id.slice(0, 8)} | ${task.parent.title}`);
8695
9372
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8696
9373
  }
8697
9374
  });
8698
- server.tool("add_comment", "Add a comment/note to a task", {
9375
+ server.tool("add_comment", "Add a comment or note to a task. Comments are append-only.", {
8699
9376
  task_id: exports_external.string(),
8700
9377
  content: exports_external.string(),
8701
9378
  agent_id: exports_external.string().optional(),
@@ -8726,7 +9403,7 @@ ${text}` }] };
8726
9403
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8727
9404
  }
8728
9405
  });
8729
- server.tool("create_project", "Register a new project", {
9406
+ server.tool("create_project", "Register a new project with auto-generated task prefix.", {
8730
9407
  name: exports_external.string(),
8731
9408
  path: exports_external.string(),
8732
9409
  description: exports_external.string().optional(),
@@ -8745,7 +9422,7 @@ ${text}` }] };
8745
9422
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8746
9423
  }
8747
9424
  });
8748
- server.tool("create_plan", "Create a new plan", {
9425
+ server.tool("create_plan", "Create a plan to group related tasks.", {
8749
9426
  name: exports_external.string(),
8750
9427
  project_id: exports_external.string().optional(),
8751
9428
  description: exports_external.string().optional(),
@@ -8770,7 +9447,7 @@ ${text}` }] };
8770
9447
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8771
9448
  }
8772
9449
  });
8773
- server.tool("list_plans", "List plans with optional project filter", {
9450
+ server.tool("list_plans", "List all plans, optionally filtered by project.", {
8774
9451
  project_id: exports_external.string().optional()
8775
9452
  }, async ({ project_id }) => {
8776
9453
  try {
@@ -8790,7 +9467,7 @@ ${text}` }] };
8790
9467
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8791
9468
  }
8792
9469
  });
8793
- server.tool("get_plan", "Get plan details", {
9470
+ server.tool("get_plan", "Get plan details including status and timestamps.", {
8794
9471
  id: exports_external.string()
8795
9472
  }, async ({ id }) => {
8796
9473
  try {
@@ -8815,7 +9492,7 @@ ${text}` }] };
8815
9492
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8816
9493
  }
8817
9494
  });
8818
- server.tool("update_plan", "Update a plan", {
9495
+ server.tool("update_plan", "Update plan fields (name, description, status).", {
8819
9496
  id: exports_external.string(),
8820
9497
  name: exports_external.string().optional(),
8821
9498
  description: exports_external.string().optional(),
@@ -8839,7 +9516,7 @@ ${text}` }] };
8839
9516
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8840
9517
  }
8841
9518
  });
8842
- server.tool("delete_plan", "Delete a plan", {
9519
+ server.tool("delete_plan", "Delete a plan. Tasks in the plan are orphaned (not deleted).", {
8843
9520
  id: exports_external.string()
8844
9521
  }, async ({ id }) => {
8845
9522
  try {
@@ -8855,15 +9532,34 @@ ${text}` }] };
8855
9532
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8856
9533
  }
8857
9534
  });
8858
- server.tool("search_tasks", "Full-text search across task titles, descriptions, tags.", {
9535
+ server.tool("search_tasks", "Full-text search across tasks with filters.", {
8859
9536
  query: exports_external.string(),
8860
9537
  project_id: exports_external.string().optional(),
8861
- task_list_id: exports_external.string().optional()
8862
- }, async ({ query, project_id, task_list_id }) => {
9538
+ task_list_id: exports_external.string().optional(),
9539
+ status: exports_external.union([
9540
+ exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]),
9541
+ exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]))
9542
+ ]).optional(),
9543
+ priority: exports_external.union([
9544
+ exports_external.enum(["low", "medium", "high", "critical"]),
9545
+ exports_external.array(exports_external.enum(["low", "medium", "high", "critical"]))
9546
+ ]).optional(),
9547
+ assigned_to: exports_external.string().optional(),
9548
+ agent_id: exports_external.string().optional(),
9549
+ created_after: exports_external.string().optional(),
9550
+ updated_after: exports_external.string().optional(),
9551
+ has_dependencies: exports_external.boolean().optional(),
9552
+ is_blocked: exports_external.boolean().optional()
9553
+ }, async ({ query, project_id, task_list_id, ...filters }) => {
8863
9554
  try {
8864
9555
  const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
8865
9556
  const resolvedTaskListId = task_list_id ? resolveId(task_list_id, "task_lists") : undefined;
8866
- const tasks = searchTasks(query, resolvedProjectId, resolvedTaskListId);
9557
+ const tasks = searchTasks({
9558
+ query,
9559
+ project_id: resolvedProjectId,
9560
+ task_list_id: resolvedTaskListId,
9561
+ ...filters
9562
+ });
8867
9563
  if (tasks.length === 0) {
8868
9564
  return { content: [{ type: "text", text: `No tasks matching "${query}".` }] };
8869
9565
  }
@@ -8875,7 +9571,7 @@ ${text}` }] };
8875
9571
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8876
9572
  }
8877
9573
  });
8878
- server.tool("sync", "Sync tasks with an agent task list.", {
9574
+ server.tool("sync", "Sync tasks between local DB and agent task list.", {
8879
9575
  task_list_id: exports_external.string().optional(),
8880
9576
  agent: exports_external.string().optional(),
8881
9577
  all_agents: exports_external.boolean().optional(),
@@ -8923,7 +9619,7 @@ ${text}` }] };
8923
9619
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8924
9620
  }
8925
9621
  });
8926
- server.tool("register_agent", "Register an agent (idempotent by name).", {
9622
+ server.tool("register_agent", "Register an agent (idempotent by name). Updates last_seen_at.", {
8927
9623
  name: exports_external.string(),
8928
9624
  description: exports_external.string().optional()
8929
9625
  }, async ({ name, description }) => {
@@ -8960,7 +9656,7 @@ ${text}` }] };
8960
9656
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
8961
9657
  }
8962
9658
  });
8963
- server.tool("get_agent", "Get agent details by ID or name", {
9659
+ server.tool("get_agent", "Get agent details by ID or name. Provide one of id or name.", {
8964
9660
  id: exports_external.string().optional(),
8965
9661
  name: exports_external.string().optional()
8966
9662
  }, async ({ id, name }) => {
@@ -8989,9 +9685,9 @@ ${text}` }] };
8989
9685
  }
8990
9686
  });
8991
9687
  server.tool("rename_agent", "Rename an agent. Resolve by id or current name.", {
8992
- id: exports_external.string().optional().describe("Agent ID"),
8993
- name: exports_external.string().optional().describe("Current agent name"),
8994
- new_name: exports_external.string().describe("New name for the agent")
9688
+ id: exports_external.string().optional(),
9689
+ name: exports_external.string().optional(),
9690
+ new_name: exports_external.string()
8995
9691
  }, async ({ id, name, new_name }) => {
8996
9692
  try {
8997
9693
  if (!id && !name) {
@@ -9014,8 +9710,8 @@ ID: ${updated.id}`
9014
9710
  }
9015
9711
  });
9016
9712
  server.tool("delete_agent", "Delete an agent permanently. Resolve by id or name.", {
9017
- id: exports_external.string().optional().describe("Agent ID"),
9018
- name: exports_external.string().optional().describe("Agent name")
9713
+ id: exports_external.string().optional(),
9714
+ name: exports_external.string().optional()
9019
9715
  }, async ({ id, name }) => {
9020
9716
  try {
9021
9717
  if (!id && !name) {
@@ -9037,7 +9733,7 @@ ID: ${updated.id}`
9037
9733
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9038
9734
  }
9039
9735
  });
9040
- server.tool("create_task_list", "Create a new task list", {
9736
+ server.tool("create_task_list", "Create a task list container for organizing tasks.", {
9041
9737
  name: exports_external.string(),
9042
9738
  slug: exports_external.string().optional(),
9043
9739
  project_id: exports_external.string().optional(),
@@ -9063,7 +9759,7 @@ Description: ${list.description}` : ""}`
9063
9759
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9064
9760
  }
9065
9761
  });
9066
- server.tool("list_task_lists", "List task lists, optionally filtered by project", {
9762
+ server.tool("list_task_lists", "List all task lists, optionally filtered by project.", {
9067
9763
  project_id: exports_external.string().optional()
9068
9764
  }, async ({ project_id }) => {
9069
9765
  try {
@@ -9083,7 +9779,7 @@ ${text}` }] };
9083
9779
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9084
9780
  }
9085
9781
  });
9086
- server.tool("get_task_list", "Get task list details", {
9782
+ server.tool("get_task_list", "Get task list details including slug and metadata.", {
9087
9783
  id: exports_external.string()
9088
9784
  }, async ({ id }) => {
9089
9785
  try {
@@ -9111,7 +9807,7 @@ ${text}` }] };
9111
9807
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9112
9808
  }
9113
9809
  });
9114
- server.tool("update_task_list", "Update a task list", {
9810
+ server.tool("update_task_list", "Update a task list's name or description.", {
9115
9811
  id: exports_external.string(),
9116
9812
  name: exports_external.string().optional(),
9117
9813
  description: exports_external.string().optional()
@@ -9132,7 +9828,7 @@ Slug: ${list.slug}`
9132
9828
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9133
9829
  }
9134
9830
  });
9135
- server.tool("delete_task_list", "Delete a task list. Tasks lose association but keep data.", {
9831
+ server.tool("delete_task_list", "Delete a task list. Tasks are orphaned, not deleted.", {
9136
9832
  id: exports_external.string()
9137
9833
  }, async ({ id }) => {
9138
9834
  try {
@@ -9148,7 +9844,7 @@ Slug: ${list.slug}`
9148
9844
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9149
9845
  }
9150
9846
  });
9151
- server.tool("get_task_history", "Get audit log for a task.", {
9847
+ server.tool("get_task_history", "Get audit log \u2014 field changes with timestamps and actors.", {
9152
9848
  task_id: exports_external.string()
9153
9849
  }, async ({ task_id }) => {
9154
9850
  try {
@@ -9165,7 +9861,7 @@ ${text}` }] };
9165
9861
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9166
9862
  }
9167
9863
  });
9168
- server.tool("get_recent_activity", "Get recent task changes across all tasks.", {
9864
+ server.tool("get_recent_activity", "Get recent task changes \u2014 global activity feed.", {
9169
9865
  limit: exports_external.number().optional()
9170
9866
  }, async ({ limit }) => {
9171
9867
  try {
@@ -9181,7 +9877,7 @@ ${text}` }] };
9181
9877
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9182
9878
  }
9183
9879
  });
9184
- server.tool("create_webhook", "Register a webhook to receive task change events.", {
9880
+ server.tool("create_webhook", "Register a webhook for task change events.", {
9185
9881
  url: exports_external.string(),
9186
9882
  events: exports_external.array(exports_external.string()).optional(),
9187
9883
  secret: exports_external.string().optional()
@@ -9208,7 +9904,7 @@ ${text}` }] };
9208
9904
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9209
9905
  }
9210
9906
  });
9211
- server.tool("delete_webhook", "Delete a webhook", {
9907
+ server.tool("delete_webhook", "Delete a webhook by ID.", {
9212
9908
  id: exports_external.string()
9213
9909
  }, async ({ id }) => {
9214
9910
  try {
@@ -9274,7 +9970,7 @@ ${task.id.slice(0, 8)} | ${task.priority} | ${task.title}` }] };
9274
9970
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9275
9971
  }
9276
9972
  });
9277
- server.tool("delete_template", "Delete a task template", { id: exports_external.string() }, async ({ id }) => {
9973
+ server.tool("delete_template", "Delete a task template by ID.", { id: exports_external.string() }, async ({ id }) => {
9278
9974
  try {
9279
9975
  const { deleteTemplate: deleteTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
9280
9976
  const deleted = deleteTemplate2(id);
@@ -9283,7 +9979,7 @@ ${task.id.slice(0, 8)} | ${task.priority} | ${task.title}` }] };
9283
9979
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9284
9980
  }
9285
9981
  });
9286
- server.tool("approve_task", "Approve a task that requires approval.", {
9982
+ server.tool("approve_task", "Approve a task with requires_approval=true.", {
9287
9983
  id: exports_external.string(),
9288
9984
  agent_id: exports_external.string().optional()
9289
9985
  }, async ({ id, agent_id }) => {
@@ -9302,7 +9998,31 @@ ${task.id.slice(0, 8)} | ${task.priority} | ${task.title}` }] };
9302
9998
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9303
9999
  }
9304
10000
  });
9305
- server.tool("get_my_tasks", "Get assigned tasks and stats for an agent.", {
10001
+ server.tool("fail_task", "Mark a task as failed with structured reason and optional auto-retry.", {
10002
+ id: exports_external.string(),
10003
+ agent_id: exports_external.string().optional(),
10004
+ reason: exports_external.string().optional(),
10005
+ error_code: exports_external.string().optional(),
10006
+ retry: exports_external.boolean().optional(),
10007
+ retry_after: exports_external.string().optional()
10008
+ }, async ({ id, agent_id, reason, error_code, retry, retry_after }) => {
10009
+ try {
10010
+ const resolvedId = resolveId(id);
10011
+ const result = failTask(resolvedId, agent_id, reason, { retry, retry_after, error_code });
10012
+ let text = `failed: ${formatTask(result.task)}`;
10013
+ if (reason)
10014
+ text += `
10015
+ Reason: ${reason}`;
10016
+ if (result.retryTask) {
10017
+ text += `
10018
+ Retry task created: ${formatTask(result.retryTask)}`;
10019
+ }
10020
+ return { content: [{ type: "text", text }] };
10021
+ } catch (e) {
10022
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10023
+ }
10024
+ });
10025
+ server.tool("get_my_tasks", "Get tasks assigned to/created by an agent with stats.", {
9306
10026
  agent_name: exports_external.string()
9307
10027
  }, async ({ agent_name }) => {
9308
10028
  try {
@@ -9335,7 +10055,7 @@ In Progress:`);
9335
10055
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9336
10056
  }
9337
10057
  });
9338
- server.tool("get_org_chart", "Get agent org chart \u2014 who reports to who.", {}, async () => {
10058
+ server.tool("get_org_chart", "Get agent org chart showing reporting hierarchy.", {}, async () => {
9339
10059
  try {
9340
10060
  let render = function(nodes, indent = 0) {
9341
10061
  return nodes.map((n) => {
@@ -9357,7 +10077,7 @@ In Progress:`);
9357
10077
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9358
10078
  }
9359
10079
  });
9360
- server.tool("set_reports_to", "Set who an agent reports to in the org chart.", {
10080
+ server.tool("set_reports_to", "Set agent reporting relationship in org chart.", {
9361
10081
  agent_name: exports_external.string(),
9362
10082
  manager_name: exports_external.string().optional()
9363
10083
  }, async ({ agent_name, manager_name }) => {
@@ -9380,7 +10100,367 @@ In Progress:`);
9380
10100
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9381
10101
  }
9382
10102
  });
9383
- server.tool("search_tools", "List tool names matching a query.", { query: exports_external.string().optional() }, async ({ query }) => {
10103
+ server.tool("bulk_update_tasks", "Update multiple tasks at once with the same changes.", {
10104
+ task_ids: exports_external.array(exports_external.string()),
10105
+ status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
10106
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
10107
+ assigned_to: exports_external.string().optional(),
10108
+ tags: exports_external.array(exports_external.string()).optional()
10109
+ }, async ({ task_ids, ...updates }) => {
10110
+ try {
10111
+ const resolvedIds = task_ids.map((id) => resolveId(id));
10112
+ const result = bulkUpdateTasks(resolvedIds, updates);
10113
+ const parts = [`Updated ${result.updated} task(s).`];
10114
+ if (result.failed.length > 0) {
10115
+ parts.push(`Failed ${result.failed.length}:`);
10116
+ for (const f of result.failed)
10117
+ parts.push(` ${f.id.slice(0, 8)}: ${f.error}`);
10118
+ }
10119
+ return { content: [{ type: "text", text: parts.join(`
10120
+ `) }] };
10121
+ } catch (e) {
10122
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10123
+ }
10124
+ });
10125
+ server.tool("clone_task", "Duplicate a task with optional field overrides.", {
10126
+ task_id: exports_external.string(),
10127
+ title: exports_external.string().optional(),
10128
+ description: exports_external.string().optional(),
10129
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
10130
+ status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
10131
+ project_id: exports_external.string().optional(),
10132
+ plan_id: exports_external.string().optional(),
10133
+ task_list_id: exports_external.string().optional(),
10134
+ assigned_to: exports_external.string().optional(),
10135
+ tags: exports_external.array(exports_external.string()).optional(),
10136
+ estimated_minutes: exports_external.number().optional()
10137
+ }, async ({ task_id, ...overrides }) => {
10138
+ try {
10139
+ const resolvedId = resolveId(task_id);
10140
+ const resolved = { ...overrides };
10141
+ if (resolved.project_id)
10142
+ resolved.project_id = resolveId(resolved.project_id, "projects");
10143
+ if (resolved.plan_id)
10144
+ resolved.plan_id = resolveId(resolved.plan_id, "plans");
10145
+ if (resolved.task_list_id)
10146
+ resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
10147
+ const task = cloneTask(resolvedId, resolved);
10148
+ return { content: [{ type: "text", text: `cloned: ${formatTask(task)}` }] };
10149
+ } catch (e) {
10150
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10151
+ }
10152
+ });
10153
+ server.tool("get_task_stats", "Get task analytics: counts by status, priority, agent.", {
10154
+ project_id: exports_external.string().optional(),
10155
+ task_list_id: exports_external.string().optional(),
10156
+ agent_id: exports_external.string().optional()
10157
+ }, async ({ project_id, task_list_id, agent_id }) => {
10158
+ try {
10159
+ const filters = {};
10160
+ if (project_id)
10161
+ filters.project_id = resolveId(project_id, "projects");
10162
+ if (task_list_id)
10163
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
10164
+ if (agent_id)
10165
+ filters.agent_id = agent_id;
10166
+ const stats = getTaskStats(Object.keys(filters).length > 0 ? filters : undefined);
10167
+ return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
10168
+ } catch (e) {
10169
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10170
+ }
10171
+ });
10172
+ server.tool("get_task_graph", "Get full dependency tree for a task.", {
10173
+ id: exports_external.string(),
10174
+ direction: exports_external.enum(["up", "down", "both"]).optional()
10175
+ }, async ({ id, direction }) => {
10176
+ try {
10177
+ let formatNode = function(node, indent) {
10178
+ const prefix = " ".repeat(indent);
10179
+ const idLabel = node.task.short_id || node.task.id.slice(0, 8);
10180
+ const blocked = node.task.is_blocked ? " (blocked: yes)" : "";
10181
+ let out = `${prefix}[${node.task.status}] ${idLabel} | ${node.task.title}${blocked}
10182
+ `;
10183
+ if (node.depends_on.length > 0) {
10184
+ out += `${prefix} Depends on:
10185
+ `;
10186
+ for (const dep of node.depends_on) {
10187
+ out += formatNode(dep, indent + 2);
10188
+ }
10189
+ }
10190
+ if (node.blocks.length > 0) {
10191
+ out += `${prefix} Blocks:
10192
+ `;
10193
+ for (const dep of node.blocks) {
10194
+ out += formatNode(dep, indent + 2);
10195
+ }
10196
+ }
10197
+ return out;
10198
+ };
10199
+ const taskId = resolveId(id, "tasks");
10200
+ const graph = getTaskGraph(taskId, direction || "both");
10201
+ let text = `Task: ${formatNode(graph, 0)}`;
10202
+ if (graph.depends_on.length > 0) {
10203
+ text += `
10204
+ Depends on:
10205
+ `;
10206
+ for (const dep of graph.depends_on) {
10207
+ text += formatNode(dep, 1);
10208
+ }
10209
+ }
10210
+ if (graph.blocks.length > 0) {
10211
+ text += `
10212
+ Blocks:
10213
+ `;
10214
+ for (const dep of graph.blocks) {
10215
+ text += formatNode(dep, 1);
10216
+ }
10217
+ }
10218
+ return { content: [{ type: "text", text }] };
10219
+ } catch (e) {
10220
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10221
+ }
10222
+ });
10223
+ server.tool("bulk_create_tasks", "Create multiple tasks atomically with dependency support.", {
10224
+ tasks: exports_external.array(exports_external.object({
10225
+ temp_id: exports_external.string().optional(),
10226
+ title: exports_external.string(),
10227
+ description: exports_external.string().optional(),
10228
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
10229
+ status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
10230
+ project_id: exports_external.string().optional(),
10231
+ plan_id: exports_external.string().optional(),
10232
+ task_list_id: exports_external.string().optional(),
10233
+ agent_id: exports_external.string().optional(),
10234
+ assigned_to: exports_external.string().optional(),
10235
+ tags: exports_external.array(exports_external.string()).optional(),
10236
+ estimated_minutes: exports_external.number().optional(),
10237
+ depends_on_temp_ids: exports_external.array(exports_external.string()).optional()
10238
+ })),
10239
+ project_id: exports_external.string().optional(),
10240
+ plan_id: exports_external.string().optional(),
10241
+ task_list_id: exports_external.string().optional()
10242
+ }, async ({ tasks, project_id, plan_id, task_list_id }) => {
10243
+ try {
10244
+ const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
10245
+ const resolvedPlanId = plan_id ? resolveId(plan_id, "plans") : undefined;
10246
+ const resolvedTaskListId = task_list_id ? resolveId(task_list_id, "task_lists") : undefined;
10247
+ const enrichedTasks = tasks.map((t) => ({
10248
+ ...t,
10249
+ project_id: t.project_id || resolvedProjectId,
10250
+ plan_id: t.plan_id || resolvedPlanId,
10251
+ task_list_id: t.task_list_id || resolvedTaskListId
10252
+ }));
10253
+ const result = bulkCreateTasks(enrichedTasks);
10254
+ const lines = result.created.map((t) => {
10255
+ const tid = t.temp_id ? `[${t.temp_id}] ` : "";
10256
+ const sid = t.short_id || t.id.slice(0, 8);
10257
+ return ` ${tid}${sid} | ${t.title}`;
10258
+ });
10259
+ return { content: [{ type: "text", text: `Created ${result.created.length} task(s):
10260
+ ${lines.join(`
10261
+ `)}` }] };
10262
+ } catch (e) {
10263
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10264
+ }
10265
+ });
10266
+ server.tool("move_task", "Move a task to a different list, project, or plan.", {
10267
+ task_id: exports_external.string(),
10268
+ task_list_id: exports_external.string().nullable().optional(),
10269
+ project_id: exports_external.string().nullable().optional(),
10270
+ plan_id: exports_external.string().nullable().optional()
10271
+ }, async ({ task_id, ...target }) => {
10272
+ try {
10273
+ const resolvedId = resolveId(task_id);
10274
+ const resolvedTarget = {};
10275
+ if (target.task_list_id !== undefined)
10276
+ resolvedTarget.task_list_id = target.task_list_id ? resolveId(target.task_list_id, "task_lists") : null;
10277
+ if (target.project_id !== undefined)
10278
+ resolvedTarget.project_id = target.project_id ? resolveId(target.project_id, "projects") : null;
10279
+ if (target.plan_id !== undefined)
10280
+ resolvedTarget.plan_id = target.plan_id ? resolveId(target.plan_id, "plans") : null;
10281
+ const task = moveTask(resolvedId, resolvedTarget);
10282
+ return { content: [{ type: "text", text: `moved: ${formatTask(task)}` }] };
10283
+ } catch (e) {
10284
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10285
+ }
10286
+ });
10287
+ server.tool("get_next_task", "Get the best pending task to work on next.", {
10288
+ agent_id: exports_external.string().optional(),
10289
+ project_id: exports_external.string().optional(),
10290
+ task_list_id: exports_external.string().optional(),
10291
+ plan_id: exports_external.string().optional(),
10292
+ tags: exports_external.array(exports_external.string()).optional()
10293
+ }, async ({ agent_id, project_id, task_list_id, plan_id, tags }) => {
10294
+ try {
10295
+ const filters = {};
10296
+ if (project_id)
10297
+ filters.project_id = resolveId(project_id, "projects");
10298
+ if (task_list_id)
10299
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
10300
+ if (plan_id)
10301
+ filters.plan_id = resolveId(plan_id, "plans");
10302
+ if (tags)
10303
+ filters.tags = tags;
10304
+ const task = getNextTask(agent_id, Object.keys(filters).length > 0 ? filters : undefined);
10305
+ if (!task) {
10306
+ return { content: [{ type: "text", text: "No tasks available \u2014 all pending tasks are blocked, locked, or none exist." }] };
10307
+ }
10308
+ return { content: [{ type: "text", text: `next: ${formatTask(task)}
10309
+ ${formatTaskDetail(task)}` }] };
10310
+ } catch (e) {
10311
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10312
+ }
10313
+ });
10314
+ server.tool("get_active_work", "See all in-progress tasks and who is working on them.", {
10315
+ project_id: exports_external.string().optional(),
10316
+ task_list_id: exports_external.string().optional()
10317
+ }, async ({ project_id, task_list_id }) => {
10318
+ try {
10319
+ const filters = {};
10320
+ if (project_id)
10321
+ filters.project_id = resolveId(project_id, "projects");
10322
+ if (task_list_id)
10323
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
10324
+ const work = getActiveWork(Object.keys(filters).length > 0 ? filters : undefined);
10325
+ if (work.length === 0) {
10326
+ return { content: [{ type: "text", text: "No active work \u2014 no tasks are currently in progress." }] };
10327
+ }
10328
+ const text = work.map((w) => {
10329
+ const id = w.short_id || w.id.slice(0, 8);
10330
+ const agent = w.assigned_to || w.locked_by || "unassigned";
10331
+ const since = w.updated_at;
10332
+ return `${agent.padEnd(12)} | ${w.priority.padEnd(8)} | ${id} | ${w.title} (since ${since})`;
10333
+ }).join(`
10334
+ `);
10335
+ return { content: [{ type: "text", text: `${work.length} active task(s):
10336
+ ${text}` }] };
10337
+ } catch (e) {
10338
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10339
+ }
10340
+ });
10341
+ server.tool("get_tasks_changed_since", "Get tasks modified after a timestamp for incremental sync.", {
10342
+ since: exports_external.string(),
10343
+ project_id: exports_external.string().optional(),
10344
+ task_list_id: exports_external.string().optional()
10345
+ }, async ({ since, project_id, task_list_id }) => {
10346
+ try {
10347
+ const filters = {};
10348
+ if (project_id)
10349
+ filters.project_id = resolveId(project_id, "projects");
10350
+ if (task_list_id)
10351
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
10352
+ const tasks = getTasksChangedSince(since, Object.keys(filters).length > 0 ? filters : undefined);
10353
+ if (tasks.length === 0) {
10354
+ return { content: [{ type: "text", text: `No tasks changed since ${since}.` }] };
10355
+ }
10356
+ const text = tasks.map((t) => {
10357
+ const assigned = t.assigned_to ? ` -> ${t.assigned_to}` : "";
10358
+ return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned} (updated: ${t.updated_at})`;
10359
+ }).join(`
10360
+ `);
10361
+ return { content: [{ type: "text", text: `${tasks.length} task(s) changed since ${since}:
10362
+ ${text}` }] };
10363
+ } catch (e) {
10364
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10365
+ }
10366
+ });
10367
+ server.tool("claim_next_task", "Atomically claim, lock, and start the best pending task.", {
10368
+ agent_id: exports_external.string(),
10369
+ project_id: exports_external.string().optional(),
10370
+ task_list_id: exports_external.string().optional(),
10371
+ plan_id: exports_external.string().optional(),
10372
+ tags: exports_external.array(exports_external.string()).optional()
10373
+ }, async ({ agent_id, project_id, task_list_id, plan_id, tags }) => {
10374
+ try {
10375
+ const filters = {};
10376
+ if (project_id)
10377
+ filters.project_id = resolveId(project_id, "projects");
10378
+ if (task_list_id)
10379
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
10380
+ if (plan_id)
10381
+ filters.plan_id = resolveId(plan_id, "plans");
10382
+ if (tags)
10383
+ filters.tags = tags;
10384
+ const task = claimNextTask(agent_id, Object.keys(filters).length > 0 ? filters : undefined);
10385
+ if (!task) {
10386
+ return { content: [{ type: "text", text: "No tasks available to claim." }] };
10387
+ }
10388
+ return { content: [{ type: "text", text: `claimed: ${formatTask(task)}
10389
+ ${formatTaskDetail(task)}` }] };
10390
+ } catch (e) {
10391
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10392
+ }
10393
+ });
10394
+ server.tool("get_stale_tasks", "Find stale in_progress tasks with no recent activity.", {
10395
+ stale_minutes: exports_external.number().optional(),
10396
+ project_id: exports_external.string().optional(),
10397
+ task_list_id: exports_external.string().optional()
10398
+ }, async ({ stale_minutes, project_id, task_list_id }) => {
10399
+ try {
10400
+ const filters = {};
10401
+ if (project_id)
10402
+ filters.project_id = resolveId(project_id, "projects");
10403
+ if (task_list_id)
10404
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
10405
+ const tasks = getStaleTasks(stale_minutes || 30, Object.keys(filters).length > 0 ? filters : undefined);
10406
+ if (tasks.length === 0) {
10407
+ return { content: [{ type: "text", text: "No stale tasks found." }] };
10408
+ }
10409
+ const text = tasks.map((t) => {
10410
+ const id = t.short_id || t.id.slice(0, 8);
10411
+ const agent = t.locked_by || t.assigned_to || "unknown";
10412
+ const staleFor = Math.round((Date.now() - new Date(t.updated_at).getTime()) / 60000);
10413
+ return `${id} | ${agent} | ${t.title} (stale ${staleFor}min)`;
10414
+ }).join(`
10415
+ `);
10416
+ return { content: [{ type: "text", text: `${tasks.length} stale task(s):
10417
+ ${text}` }] };
10418
+ } catch (e) {
10419
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10420
+ }
10421
+ });
10422
+ server.tool("get_status", "Get a full project health snapshot \u2014 counts, active work, next task, stale/overdue summary.", {
10423
+ agent_id: exports_external.string().optional(),
10424
+ project_id: exports_external.string().optional(),
10425
+ task_list_id: exports_external.string().optional()
10426
+ }, async ({ agent_id, project_id, task_list_id }) => {
10427
+ try {
10428
+ const filters = {};
10429
+ if (project_id)
10430
+ filters.project_id = resolveId(project_id, "projects");
10431
+ if (task_list_id)
10432
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
10433
+ const status = getStatus(Object.keys(filters).length > 0 ? filters : undefined, agent_id);
10434
+ const lines = [
10435
+ `Tasks: ${status.pending} pending | ${status.in_progress} active | ${status.completed} done | ${status.total} total`
10436
+ ];
10437
+ if (status.stale_count > 0)
10438
+ lines.push(`\u26A0\uFE0F ${status.stale_count} stale (stuck in_progress)`);
10439
+ if (status.overdue_recurring > 0)
10440
+ lines.push(`\uD83D\uDD01 ${status.overdue_recurring} overdue recurring`);
10441
+ if (status.active_work.length > 0) {
10442
+ lines.push(`
10443
+ Active (${status.active_work.length}):`);
10444
+ for (const w of status.active_work.slice(0, 5)) {
10445
+ const id = w.short_id || w.id.slice(0, 8);
10446
+ lines.push(` ${id} | ${w.assigned_to || w.locked_by || "?"} | ${w.title}`);
10447
+ }
10448
+ }
10449
+ if (status.next_task) {
10450
+ lines.push(`
10451
+ Next up:`);
10452
+ lines.push(` ${formatTask(status.next_task)}`);
10453
+ } else {
10454
+ lines.push(`
10455
+ No pending tasks available.`);
10456
+ }
10457
+ return { content: [{ type: "text", text: lines.join(`
10458
+ `) }] };
10459
+ } catch (e) {
10460
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10461
+ }
10462
+ });
10463
+ server.tool("search_tools", "List all tool names, optionally filtered by substring.", { query: exports_external.string().optional() }, async ({ query }) => {
9384
10464
  const all = [
9385
10465
  "create_task",
9386
10466
  "list_tasks",
@@ -9389,6 +10469,7 @@ In Progress:`);
9389
10469
  "delete_task",
9390
10470
  "start_task",
9391
10471
  "complete_task",
10472
+ "fail_task",
9392
10473
  "lock_task",
9393
10474
  "unlock_task",
9394
10475
  "approve_task",
@@ -9405,7 +10486,11 @@ In Progress:`);
9405
10486
  "register_agent",
9406
10487
  "list_agents",
9407
10488
  "get_agent",
10489
+ "rename_agent",
10490
+ "delete_agent",
9408
10491
  "get_my_tasks",
10492
+ "get_org_chart",
10493
+ "set_reports_to",
9409
10494
  "create_task_list",
9410
10495
  "list_task_lists",
9411
10496
  "get_task_list",
@@ -9413,6 +10498,10 @@ In Progress:`);
9413
10498
  "delete_task_list",
9414
10499
  "search_tasks",
9415
10500
  "sync",
10501
+ "clone_task",
10502
+ "move_task",
10503
+ "get_next_task",
10504
+ "claim_next_task",
9416
10505
  "get_task_history",
9417
10506
  "get_recent_activity",
9418
10507
  "create_webhook",
@@ -9422,6 +10511,14 @@ In Progress:`);
9422
10511
  "list_templates",
9423
10512
  "create_task_from_template",
9424
10513
  "delete_template",
10514
+ "bulk_update_tasks",
10515
+ "bulk_create_tasks",
10516
+ "get_task_stats",
10517
+ "get_task_graph",
10518
+ "get_active_work",
10519
+ "get_tasks_changed_since",
10520
+ "get_stale_tasks",
10521
+ "get_status",
9425
10522
  "search_tools",
9426
10523
  "describe_tools"
9427
10524
  ];
@@ -9429,27 +10526,178 @@ In Progress:`);
9429
10526
  const matches = q ? all.filter((n) => n.includes(q)) : all;
9430
10527
  return { content: [{ type: "text", text: matches.join(", ") }] };
9431
10528
  });
9432
- server.tool("describe_tools", "Get descriptions for specific tools by name.", { names: exports_external.array(exports_external.string()) }, async ({ names }) => {
10529
+ server.tool("describe_tools", "Get detailed parameter info for specific tools by name.", { names: exports_external.array(exports_external.string()) }, async ({ names }) => {
9433
10530
  const descriptions = {
9434
- create_task: "Create a task. Params: title(req), description, priority, project_id, plan_id, tags, assigned_to, estimated_minutes, requires_approval",
9435
- list_tasks: "List tasks. Params: status, priority, project_id, plan_id, assigned_to, tags, limit",
9436
- get_task: "Get full task details. Params: id",
9437
- update_task: "Update task fields. Params: id, version(req), title, description, status, priority, tags, assigned_to, due_at",
9438
- delete_task: "Delete a task. Params: id",
9439
- start_task: "Claim, lock, and start a task. Params: id",
9440
- complete_task: "Mark task completed. Params: id, agent_id",
9441
- approve_task: "Approve task requiring approval. Params: id, agent_id",
9442
- create_plan: "Create a plan. Params: name, description, project_id, task_list_id, agent_id, status",
9443
- list_plans: "List plans. Params: project_id",
9444
- get_plan: "Get plan with tasks. Params: id",
9445
- search_tasks: "Full-text search tasks. Params: query, project_id, task_list_id",
9446
- get_my_tasks: "Get your tasks and stats. Params: agent_name",
9447
- get_task_history: "Get task audit log. Params: task_id",
9448
- get_recent_activity: "Recent changes across all tasks. Params: limit",
9449
- create_template: "Create task template. Params: name, title_pattern, description, priority, tags",
9450
- create_task_from_template: "Create task from template. Params: template_id, title, priority, assigned_to"
10531
+ create_task: `Create a new task.
10532
+ 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)
10533
+ Example: {title: 'Daily standup', recurrence_rule: 'every weekday', priority: 'medium'}`,
10534
+ list_tasks: `List tasks with optional filters. Supports pagination.
10535
+ 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)
10536
+ Example: {status: ['pending', 'in_progress'], has_recurrence: true, limit: 20}`,
10537
+ get_task: `Get full task details with subtasks, deps, and comments.
10538
+ Params: id(string, req \u2014 task ID, short_id like 'APP-00001', or partial ID)
10539
+ Example: {id: 'a1b2c3d4'}`,
10540
+ update_task: `Update task fields. Requires version for optimistic locking (get it from get_task first).
10541
+ 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)
10542
+ Example: {id: 'a1b2c3d4', version: 3, status: 'completed'}`,
10543
+ delete_task: `Delete a task permanently. Subtasks cascade-delete. Dependencies removed.
10544
+ Params: id(string, req)
10545
+ Example: {id: 'a1b2c3d4'}`,
10546
+ start_task: `Claim, lock, and set task status to in_progress in one call.
10547
+ Params: id(string, req), agent_id(string, req \u2014 your 8-char agent ID)
10548
+ Example: {id: 'a1b2c3d4', agent_id: 'e5f6g7h8'}`,
10549
+ complete_task: `Mark task completed, release lock, set completed_at timestamp. For recurring tasks, auto-spawns next instance unless skip_recurrence is true.
10550
+ 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)
10551
+ Example: {id: 'a1b2c3d4', skip_recurrence: false}`,
10552
+ lock_task: `Acquire exclusive lock on a task. Locks auto-expire after 30 min. Re-locking by same agent is idempotent.
10553
+ Params: id(string, req), agent_id(string, req)
10554
+ Example: {id: 'a1b2c3d4', agent_id: 'e5f6g7h8'}`,
10555
+ unlock_task: `Release exclusive lock on a task.
10556
+ Params: id(string, req), agent_id(string, optional \u2014 omit to force-unlock)
10557
+ Example: {id: 'a1b2c3d4', agent_id: 'e5f6g7h8'}`,
10558
+ approve_task: `Approve a task with requires_approval=true. Must be approved before completion.
10559
+ Params: id(string, req), agent_id(string, optional \u2014 defaults to 'system')
10560
+ Example: {id: 'a1b2c3d4', agent_id: 'e5f6g7h8'}`,
10561
+ fail_task: `Mark a task as failed with structured reason and optional auto-retry. Stores failure info in metadata._failure, releases lock.
10562
+ 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)
10563
+ Example: {id: 'a1b2c3d4', reason: 'Build timeout', error_code: 'TIMEOUT', retry: true}`,
10564
+ add_dependency: `Add a dependency: task_id depends on depends_on. Prevents cycles via BFS.
10565
+ Params: task_id(string, req), depends_on(string, req)
10566
+ Example: {task_id: 'abc12345', depends_on: 'def67890'}`,
10567
+ remove_dependency: `Remove a dependency link between two tasks.
10568
+ Params: task_id(string, req), depends_on(string, req)
10569
+ Example: {task_id: 'abc12345', depends_on: 'def67890'}`,
10570
+ add_comment: `Add a comment/note to a task. Comments are append-only.
10571
+ Params: task_id(string, req), content(string, req), agent_id(string), session_id(string)
10572
+ Example: {task_id: 'a1b2c3d4', content: 'Blocked by API rate limit'}`,
10573
+ create_project: `Register a new project. Auto-generates task prefix for short IDs (e.g. APP-00001).
10574
+ Params: name(string, req), path(string, req \u2014 unique absolute path), description(string), task_list_id(string)
10575
+ Example: {name: 'my-app', path: '/Users/dev/my-app'}`,
10576
+ list_projects: "List all registered projects. No params.",
10577
+ create_plan: `Create a plan to group related tasks.
10578
+ Params: name(string, req), project_id(string), description(string), status(active|completed|archived, default:active), task_list_id(string), agent_id(string)
10579
+ Example: {name: 'Sprint 1', project_id: 'a1b2c3d4'}`,
10580
+ list_plans: `List all plans, optionally filtered by project.
10581
+ Params: project_id(string)
10582
+ Example: {project_id: 'a1b2c3d4'}`,
10583
+ get_plan: `Get plan details (name, status, description, timestamps).
10584
+ Params: id(string, req)
10585
+ Example: {id: 'a1b2c3d4'}`,
10586
+ update_plan: `Update plan fields.
10587
+ Params: id(string, req), name(string), description(string), status(active|completed|archived), task_list_id(string), agent_id(string)
10588
+ Example: {id: 'a1b2c3d4', status: 'completed'}`,
10589
+ delete_plan: `Delete a plan. Tasks in the plan are orphaned, not deleted.
10590
+ Params: id(string, req)
10591
+ Example: {id: 'a1b2c3d4'}`,
10592
+ register_agent: `Register an agent (idempotent by name). Returns existing agent if name matches.
10593
+ Params: name(string, req \u2014 e.g. 'maximus'), description(string)
10594
+ Example: {name: 'maximus', description: 'Backend developer'}`,
10595
+ list_agents: "List all registered agents with IDs, names, and last seen timestamps. No params.",
10596
+ get_agent: `Get agent details by ID or name. Provide one of id or name.
10597
+ Params: id(string), name(string)
10598
+ Example: {name: 'maximus'}`,
10599
+ rename_agent: `Rename an agent. Resolve by id or current name.
10600
+ Params: id(string), name(string \u2014 current name), new_name(string, req)
10601
+ Example: {name: 'old-name', new_name: 'new-name'}`,
10602
+ delete_agent: `Delete an agent permanently. Resolve by id or name.
10603
+ Params: id(string), name(string)
10604
+ Example: {name: 'maximus'}`,
10605
+ get_my_tasks: `Get all tasks assigned to/created by an agent, with stats (pending/active/done/rate).
10606
+ Params: agent_name(string, req)
10607
+ Example: {agent_name: 'maximus'}`,
10608
+ get_org_chart: "Get agent org chart showing reporting hierarchy. No params.",
10609
+ set_reports_to: `Set who an agent reports to in the org chart. Omit manager_name for top-level.
10610
+ Params: agent_name(string, req), manager_name(string, optional)
10611
+ Example: {agent_name: 'brutus', manager_name: 'maximus'}`,
10612
+ create_task_list: `Create a task list \u2014 a container/folder for organizing tasks.
10613
+ Params: name(string, req), slug(string \u2014 auto-generated if omitted), project_id(string), description(string)
10614
+ Example: {name: 'Sprint 1', project_id: 'a1b2c3d4'}`,
10615
+ list_task_lists: `List all task lists, optionally filtered by project.
10616
+ Params: project_id(string)
10617
+ Example: {project_id: 'a1b2c3d4'}`,
10618
+ get_task_list: `Get task list details (name, slug, project, metadata).
10619
+ Params: id(string, req)
10620
+ Example: {id: 'a1b2c3d4'}`,
10621
+ update_task_list: `Update a task list's name or description.
10622
+ Params: id(string, req), name(string), description(string)
10623
+ Example: {id: 'a1b2c3d4', name: 'Sprint 2'}`,
10624
+ delete_task_list: `Delete a task list. Tasks are orphaned (not deleted).
10625
+ Params: id(string, req)
10626
+ Example: {id: 'a1b2c3d4'}`,
10627
+ search_tasks: `Full-text search across task titles, descriptions, and tags. Supports filters.
10628
+ 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)
10629
+ Example: {query: 'auth bug', status: 'pending'}`,
10630
+ 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.
10631
+ Params: agent_id(string \u2014 prefers your tasks), project_id(string), task_list_id(string), plan_id(string), tags(string[])
10632
+ Example: {agent_id: 'a1b2c3d4', project_id: 'e5f6g7h8'}`,
10633
+ 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.
10634
+ Params: agent_id(string, req \u2014 used for lock and assignment), project_id(string), task_list_id(string), plan_id(string), tags(string[])
10635
+ Example: {agent_id: 'a1b2c3d4', project_id: 'e5f6g7h8'}`,
10636
+ sync: `Sync tasks between local DB and agent task list (e.g. Claude Code).
10637
+ 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)
10638
+ Example: {agent: 'claude', direction: 'push'}`,
10639
+ clone_task: `Duplicate a task with optional field overrides. Creates new independent copy.
10640
+ 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)
10641
+ Example: {task_id: 'a1b2c3d4', title: 'Cloned task', assigned_to: 'brutus'}`,
10642
+ move_task: `Move a task to a different list, project, or plan.
10643
+ Params: task_id(string, req), task_list_id(string|null), project_id(string|null), plan_id(string|null)
10644
+ Example: {task_id: 'a1b2c3d4', task_list_id: 'e5f6g7h8'}`,
10645
+ bulk_update_tasks: `Update multiple tasks at once with the same changes.
10646
+ Params: task_ids(string[], req), status(pending|in_progress|completed|failed|cancelled), priority(low|medium|high|critical), assigned_to(string), tags(string[])
10647
+ Example: {task_ids: ['abc12345', 'def67890'], status: 'completed'}`,
10648
+ bulk_create_tasks: `Create multiple tasks atomically. Supports inter-task dependencies via temp_id references.
10649
+ 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)
10650
+ Example: {tasks: [{temp_id: 'a', title: 'First'}, {temp_id: 'b', title: 'Second', depends_on_temp_ids: ['a']}]}`,
10651
+ get_task_stats: `Get task analytics: counts by status, priority, agent, and completion rate. All via SQL.
10652
+ Params: project_id(string), task_list_id(string), agent_id(string)
10653
+ Example: {project_id: 'a1b2c3d4'}`,
10654
+ get_task_graph: `Get full dependency tree for a task \u2014 upstream blockers and downstream dependents.
10655
+ Params: id(string, req), direction(up|down|both, default:both)
10656
+ Example: {id: 'a1b2c3d4', direction: 'up'}`,
10657
+ get_task_history: `Get audit log for a task \u2014 all field changes with timestamps and actors.
10658
+ Params: task_id(string, req)
10659
+ Example: {task_id: 'a1b2c3d4'}`,
10660
+ get_recent_activity: `Get recent task changes across all tasks \u2014 global activity feed.
10661
+ Params: limit(number, default:50)
10662
+ Example: {limit: 20}`,
10663
+ create_webhook: `Register a webhook for task change events.
10664
+ Params: url(string, req), events(string[] \u2014 empty=all), secret(string \u2014 HMAC signing)
10665
+ Example: {url: 'https://example.com/hook', events: ['task.created', 'task.completed']}`,
10666
+ list_webhooks: "List all registered webhooks. No params.",
10667
+ delete_webhook: `Delete a webhook by ID.
10668
+ Params: id(string, req)
10669
+ Example: {id: 'a1b2c3d4'}`,
10670
+ create_template: `Create a reusable task template.
10671
+ 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)
10672
+ Example: {name: 'Bug Report', title_pattern: 'Bug: {description}', priority: 'high', tags: ['bug']}`,
10673
+ list_templates: "List all task templates. No params.",
10674
+ create_task_from_template: `Create a task from a template with optional overrides.
10675
+ Params: template_id(string, req), title(string), description(string), priority(low|medium|high|critical), assigned_to(string), project_id(string)
10676
+ Example: {template_id: 'a1b2c3d4', assigned_to: 'maximus'}`,
10677
+ delete_template: `Delete a task template.
10678
+ Params: id(string, req)
10679
+ Example: {id: 'a1b2c3d4'}`,
10680
+ get_active_work: `See all in-progress tasks and who is working on them.
10681
+ Params: project_id(string, optional), task_list_id(string, optional)
10682
+ Example: {project_id: 'a1b2c3d4'}`,
10683
+ get_tasks_changed_since: `Get tasks modified after a timestamp \u2014 incremental delta sync.
10684
+ Params: since(string, req \u2014 ISO date), project_id(string, optional), task_list_id(string, optional)
10685
+ Example: {since: '2026-03-14T10:00:00Z'}`,
10686
+ get_stale_tasks: `Find stale in_progress tasks with no recent activity.
10687
+ Params: stale_minutes(number, default:30), project_id(string, optional), task_list_id(string, optional)
10688
+ Example: {stale_minutes: 60, project_id: 'a1b2c3d4'}`,
10689
+ get_status: `Get a full project health snapshot \u2014 pending/in_progress/completed counts, active work, next recommended task, stale task count, overdue recurring tasks. Saves 4+ round trips at session start.
10690
+ Params: agent_id(string, optional \u2014 prefers tasks assigned to this agent for next_task), project_id(string, optional), task_list_id(string, optional)
10691
+ Example: {agent_id: 'a1b2c3d4', project_id: 'e5f6g7h8'}`,
10692
+ search_tools: `List all tool names or filter by substring.
10693
+ Params: query(string, optional)
10694
+ Example: {query: 'task'}`,
10695
+ describe_tools: `Get detailed descriptions and parameter info for tools by name.
10696
+ Params: names(string[], req)
10697
+ Example: {names: ['create_task', 'update_task']}`
9451
10698
  };
9452
- const result = names.map((n) => `${n}: ${descriptions[n] || "See tool schema"}`).join(`
10699
+ const result = names.map((n) => `${n}: ${descriptions[n] || "Unknown tool. Use search_tools to list available tools."}`).join(`
10700
+
9453
10701
  `);
9454
10702
  return { content: [{ type: "text", text: result }] };
9455
10703
  });