@hasna/todos 0.10.19 → 0.10.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -391,6 +391,7 @@ function ensureSchema(db) {
391
391
  ensureColumn("tasks", "spawned_from_session", "TEXT");
392
392
  ensureColumn("tasks", "assigned_by", "TEXT");
393
393
  ensureColumn("tasks", "assigned_from_project", "TEXT");
394
+ ensureColumn("tasks", "started_at", "TEXT");
394
395
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
395
396
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
396
397
  ensureColumn("agents", "reports_to", "TEXT");
@@ -508,6 +509,16 @@ function resolvePartialId(db, table, partialId) {
508
509
  return shortIdRows[0].id;
509
510
  }
510
511
  }
512
+ if (table === "task_lists") {
513
+ const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
514
+ if (slugRow)
515
+ return slugRow.id;
516
+ }
517
+ if (table === "projects") {
518
+ const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
519
+ if (nameRow)
520
+ return nameRow.id;
521
+ }
511
522
  return null;
512
523
  }
513
524
  var LOCK_EXPIRY_MINUTES = 30, MIGRATIONS, _db = null;
@@ -986,6 +997,15 @@ var init_database = __esm(() => {
986
997
  CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
987
998
  CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
988
999
  INSERT OR IGNORE INTO _migrations (id) VALUES (33);
1000
+ `,
1001
+ `
1002
+ ALTER TABLE tasks ADD COLUMN started_at TEXT;
1003
+ INSERT OR IGNORE INTO _migrations (id) VALUES (34);
1004
+ `,
1005
+ `
1006
+ ALTER TABLE tasks ADD COLUMN task_type TEXT;
1007
+ CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type);
1008
+ INSERT OR IGNORE INTO _migrations (id) VALUES (35);
989
1009
  `
990
1010
  ];
991
1011
  });
@@ -1273,7 +1293,8 @@ var exports_audit = {};
1273
1293
  __export(exports_audit, {
1274
1294
  logTaskChange: () => logTaskChange,
1275
1295
  getTaskHistory: () => getTaskHistory,
1276
- getRecentActivity: () => getRecentActivity
1296
+ getRecentActivity: () => getRecentActivity,
1297
+ getRecap: () => getRecap
1277
1298
  });
1278
1299
  function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
1279
1300
  const d = db || getDatabase();
@@ -1291,6 +1312,32 @@ function getRecentActivity(limit = 50, db) {
1291
1312
  const d = db || getDatabase();
1292
1313
  return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
1293
1314
  }
1315
+ function getRecap(hours = 8, projectId, db) {
1316
+ const d = db || getDatabase();
1317
+ const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
1318
+ const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
1319
+ const pf = projectId ? " AND project_id = ?" : "";
1320
+ const tpf = projectId ? " AND t.project_id = ?" : "";
1321
+ const completed = projectId ? d.query(`SELECT id, short_id, title, assigned_to, completed_at, started_at FROM tasks WHERE status = 'completed' AND completed_at > ?${pf} ORDER BY completed_at DESC`).all(since, projectId) : d.query(`SELECT id, short_id, title, assigned_to, completed_at, started_at FROM tasks WHERE status = 'completed' AND completed_at > ? ORDER BY completed_at DESC`).all(since);
1322
+ const created = projectId ? d.query(`SELECT id, short_id, title, agent_id, created_at FROM tasks WHERE created_at > ?${pf} ORDER BY created_at DESC`).all(since, projectId) : d.query(`SELECT id, short_id, title, agent_id, created_at FROM tasks WHERE created_at > ? ORDER BY created_at DESC`).all(since);
1323
+ const in_progress = projectId ? d.query(`SELECT id, short_id, title, assigned_to, started_at FROM tasks WHERE status = 'in_progress' AND project_id = ? ORDER BY updated_at DESC`).all(projectId) : d.query(`SELECT id, short_id, title, assigned_to, started_at FROM tasks WHERE status = 'in_progress' ORDER BY updated_at DESC`).all();
1324
+ const blocked = projectId ? d.query(`SELECT DISTINCT t.id, t.short_id, t.title, t.assigned_to FROM tasks t JOIN task_dependencies td ON td.task_id = t.id JOIN tasks dep ON dep.id = td.depends_on AND dep.status NOT IN ('completed','cancelled') WHERE t.status = 'pending'${tpf}`).all(projectId) : d.query(`SELECT DISTINCT t.id, t.short_id, t.title, t.assigned_to FROM tasks t JOIN task_dependencies td ON td.task_id = t.id JOIN tasks dep ON dep.id = td.depends_on AND dep.status NOT IN ('completed','cancelled') WHERE t.status = 'pending'`).all();
1325
+ const stale = projectId ? d.query(`SELECT id, short_id, title, assigned_to, updated_at FROM tasks WHERE status = 'in_progress' AND updated_at < ? AND project_id = ? ORDER BY updated_at ASC`).all(staleWindow, projectId) : d.query(`SELECT id, short_id, title, assigned_to, updated_at FROM tasks WHERE status = 'in_progress' AND updated_at < ? ORDER BY updated_at ASC`).all(staleWindow);
1326
+ const agents = projectId ? d.query(`SELECT a.name, a.last_seen_at, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'completed' AND t.completed_at > ?${tpf}) as completed_count, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'in_progress'${tpf}) as in_progress_count FROM agents a WHERE a.status = 'active' AND a.last_seen_at > ? ORDER BY completed_count DESC`).all(since, projectId, projectId, since) : d.query(`SELECT a.name, a.last_seen_at, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'completed' AND t.completed_at > ?) as completed_count, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'in_progress') as in_progress_count FROM agents a WHERE a.status = 'active' AND a.last_seen_at > ? ORDER BY completed_count DESC`).all(since, since);
1327
+ return {
1328
+ hours,
1329
+ since,
1330
+ completed: completed.map((r) => ({
1331
+ ...r,
1332
+ duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
1333
+ })),
1334
+ created,
1335
+ in_progress,
1336
+ blocked,
1337
+ stale,
1338
+ agents
1339
+ };
1340
+ }
1294
1341
  var init_audit = __esm(() => {
1295
1342
  init_database();
1296
1343
  });
@@ -1619,8 +1666,8 @@ function createTask(input, db) {
1619
1666
  const title = shortId ? `${shortId}: ${input.title}` : input.title;
1620
1667
  const assignedBy = input.assigned_by || input.agent_id;
1621
1668
  const assignedFromProject = input.assigned_from_project || null;
1622
- d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project)
1623
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1669
+ d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type)
1670
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1624
1671
  id,
1625
1672
  shortId,
1626
1673
  input.project_id || null,
@@ -1650,7 +1697,8 @@ function createTask(input, db) {
1650
1697
  input.reason || null,
1651
1698
  input.spawned_from_session || null,
1652
1699
  assignedBy || null,
1653
- assignedFromProject || null
1700
+ assignedFromProject || null,
1701
+ input.task_type || null
1654
1702
  ]);
1655
1703
  if (tags.length > 0) {
1656
1704
  insertTaskTags(id, tags, d);
@@ -1759,6 +1807,15 @@ function listTasks(filter = {}, db) {
1759
1807
  } else if (filter.has_recurrence === false) {
1760
1808
  conditions.push("recurrence_rule IS NULL");
1761
1809
  }
1810
+ if (filter.task_type) {
1811
+ if (Array.isArray(filter.task_type)) {
1812
+ conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
1813
+ params.push(...filter.task_type);
1814
+ } else {
1815
+ conditions.push("task_type = ?");
1816
+ params.push(filter.task_type);
1817
+ }
1818
+ }
1762
1819
  const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
1763
1820
  if (filter.cursor) {
1764
1821
  try {
@@ -1918,6 +1975,10 @@ function updateTask(id, input, db) {
1918
1975
  sets.push("recurrence_rule = ?");
1919
1976
  params.push(input.recurrence_rule);
1920
1977
  }
1978
+ if (input.task_type !== undefined) {
1979
+ sets.push("task_type = ?");
1980
+ params.push(input.task_type ?? null);
1981
+ }
1921
1982
  params.push(id, input.version);
1922
1983
  const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
1923
1984
  if (result.changes === 0) {
@@ -1987,8 +2048,8 @@ function startTask(id, agentId, db) {
1987
2048
  }
1988
2049
  const cutoff = lockExpiryCutoff();
1989
2050
  const timestamp = now();
1990
- const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
1991
- WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, id, agentId, cutoff]);
2051
+ const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
2052
+ WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
1992
2053
  if (result.changes === 0) {
1993
2054
  if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
1994
2055
  throw new LockError(id, task.locked_by);
@@ -1996,7 +2057,7 @@ function startTask(id, agentId, db) {
1996
2057
  }
1997
2058
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
1998
2059
  dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
1999
- return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
2060
+ return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
2000
2061
  }
2001
2062
  function completeTask(id, agentId, db, options) {
2002
2063
  const d = db || getDatabase();
@@ -2647,6 +2708,7 @@ __export(exports_agents, {
2647
2708
  updateAgentActivity: () => updateAgentActivity,
2648
2709
  updateAgent: () => updateAgent,
2649
2710
  unarchiveAgent: () => unarchiveAgent,
2711
+ releaseAgent: () => releaseAgent,
2650
2712
  registerAgent: () => registerAgent,
2651
2713
  matchCapabilities: () => matchCapabilities,
2652
2714
  listAgents: () => listAgents,
@@ -2658,10 +2720,29 @@ __export(exports_agents, {
2658
2720
  getAgentByName: () => getAgentByName,
2659
2721
  getAgent: () => getAgent,
2660
2722
  deleteAgent: () => deleteAgent,
2723
+ autoReleaseStaleAgents: () => autoReleaseStaleAgents,
2661
2724
  archiveAgent: () => archiveAgent
2662
2725
  });
2726
+ function getActiveWindowMs() {
2727
+ const env = process.env["TODOS_AGENT_TIMEOUT_MS"];
2728
+ if (env) {
2729
+ const parsed = parseInt(env, 10);
2730
+ if (!isNaN(parsed) && parsed > 0)
2731
+ return parsed;
2732
+ }
2733
+ return 30 * 60 * 1000;
2734
+ }
2735
+ function autoReleaseStaleAgents(db) {
2736
+ if (process.env["TODOS_AGENT_AUTO_RELEASE"] !== "true")
2737
+ return 0;
2738
+ const d = db || getDatabase();
2739
+ const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
2740
+ const result = d.run("UPDATE agents SET session_id = NULL WHERE status = 'active' AND session_id IS NOT NULL AND last_seen_at < ?", [cutoff]);
2741
+ return result.changes;
2742
+ }
2663
2743
  function getAvailableNamesFromPool(pool, db) {
2664
- const cutoff = new Date(Date.now() - AGENT_ACTIVE_WINDOW_MS).toISOString();
2744
+ autoReleaseStaleAgents(db);
2745
+ const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
2665
2746
  const activeNames = new Set(db.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
2666
2747
  return pool.filter((name) => !activeNames.has(name.toLowerCase()));
2667
2748
  }
@@ -2683,22 +2764,19 @@ function registerAgent(input, db) {
2683
2764
  const existing = getAgentByName(normalizedName, d);
2684
2765
  if (existing) {
2685
2766
  const lastSeenMs = new Date(existing.last_seen_at).getTime();
2686
- const isActive = Date.now() - lastSeenMs < AGENT_ACTIVE_WINDOW_MS;
2767
+ const activeWindowMs = getActiveWindowMs();
2768
+ const isActive = Date.now() - lastSeenMs < activeWindowMs;
2687
2769
  const sameSession = input.session_id && existing.session_id && input.session_id === existing.session_id;
2688
2770
  const differentSession = input.session_id && existing.session_id && input.session_id !== existing.session_id;
2689
- if (isActive && differentSession) {
2690
- const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
2691
- const suggestions = input.pool ? getAvailableNamesFromPool(input.pool, d) : [];
2692
- return {
2693
- conflict: true,
2694
- existing_id: existing.id,
2695
- existing_name: existing.name,
2696
- last_seen_at: existing.last_seen_at,
2697
- session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
2698
- working_dir: existing.working_dir,
2699
- suggestions: suggestions.slice(0, 5),
2700
- message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session \u2026${existing.session_id?.slice(-4)}, dir: ${existing.working_dir ?? "unknown"}). Cannot reclaim an active agent \u2014 choose a different name, or wait for the session to go stale.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
2701
- };
2771
+ const callerHasNoSession = !input.session_id;
2772
+ const existingHasActiveSession = existing.session_id && isActive;
2773
+ if (!input.force) {
2774
+ if (isActive && differentSession) {
2775
+ return buildConflictError(existing, lastSeenMs, input.pool, d);
2776
+ }
2777
+ if (callerHasNoSession && existingHasActiveSession) {
2778
+ return buildConflictError(existing, lastSeenMs, input.pool, d);
2779
+ }
2702
2780
  }
2703
2781
  const updates = ["last_seen_at = ?", "status = 'active'"];
2704
2782
  const params = [now()];
@@ -2743,6 +2821,32 @@ function registerAgent(input, db) {
2743
2821
  function isAgentConflict(result) {
2744
2822
  return result.conflict === true;
2745
2823
  }
2824
+ function buildConflictError(existing, lastSeenMs, pool, d) {
2825
+ const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
2826
+ const suggestions = pool ? getAvailableNamesFromPool(pool, d) : [];
2827
+ return {
2828
+ conflict: true,
2829
+ existing_id: existing.id,
2830
+ existing_name: existing.name,
2831
+ last_seen_at: existing.last_seen_at,
2832
+ session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
2833
+ working_dir: existing.working_dir,
2834
+ suggestions: suggestions.slice(0, 5),
2835
+ message: `Agent "${existing.name}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id ? "\u2026" + existing.session_id.slice(-4) : "none"}, dir: ${existing.working_dir ?? "unknown"}). Pass force: true to take over, or choose a different name.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
2836
+ };
2837
+ }
2838
+ function releaseAgent(id, session_id, db) {
2839
+ const d = db || getDatabase();
2840
+ const agent = getAgent(id, d);
2841
+ if (!agent)
2842
+ return false;
2843
+ if (session_id && agent.session_id && agent.session_id !== session_id) {
2844
+ return false;
2845
+ }
2846
+ const epoch = new Date(0).toISOString();
2847
+ d.run("UPDATE agents SET session_id = NULL, last_seen_at = ? WHERE id = ?", [epoch, id]);
2848
+ return true;
2849
+ }
2746
2850
  function getAgent(id, db) {
2747
2851
  const d = db || getDatabase();
2748
2852
  const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
@@ -2763,6 +2867,7 @@ function listAgents(opts, db) {
2763
2867
  includeArchived = opts?.include_archived ?? false;
2764
2868
  d = db || getDatabase();
2765
2869
  }
2870
+ autoReleaseStaleAgents(d);
2766
2871
  if (includeArchived) {
2767
2872
  return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
2768
2873
  }
@@ -2780,8 +2885,19 @@ function updateAgent(id, input, db) {
2780
2885
  const sets = ["last_seen_at = ?"];
2781
2886
  const params = [now()];
2782
2887
  if (input.name !== undefined) {
2888
+ const newName = input.name.trim().toLowerCase();
2889
+ const holder = getAgentByName(newName, d);
2890
+ if (holder && holder.id !== id) {
2891
+ const lastSeenMs = new Date(holder.last_seen_at).getTime();
2892
+ const isActive = Date.now() - lastSeenMs < getActiveWindowMs();
2893
+ if (isActive && holder.status === "active") {
2894
+ throw new Error(`Cannot rename: name "${newName}" is held by active agent ${holder.id} (last seen ${Math.round((Date.now() - lastSeenMs) / 60000)}m ago)`);
2895
+ }
2896
+ const evictedName = `${holder.name}__evicted_${holder.id}`;
2897
+ d.run("UPDATE agents SET name = ? WHERE id = ?", [evictedName, holder.id]);
2898
+ }
2783
2899
  sets.push("name = ?");
2784
- params.push(input.name.trim().toLowerCase());
2900
+ params.push(newName);
2785
2901
  }
2786
2902
  if (input.description !== undefined) {
2787
2903
  sets.push("description = ?");
@@ -2878,10 +2994,8 @@ function getCapableAgents(capabilities, opts, db) {
2878
2994
  })).filter((entry) => entry.score >= minScore).sort((a, b) => b.score - a.score);
2879
2995
  return opts?.limit ? scored.slice(0, opts.limit) : scored;
2880
2996
  }
2881
- var AGENT_ACTIVE_WINDOW_MS;
2882
2997
  var init_agents = __esm(() => {
2883
2998
  init_database();
2884
- AGENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
2885
2999
  });
2886
3000
 
2887
3001
  // src/db/task-commits.ts
@@ -2928,160 +3042,63 @@ var init_task_commits = __esm(() => {
2928
3042
  init_database();
2929
3043
  });
2930
3044
 
2931
- // src/lib/auto-assign.ts
2932
- var exports_auto_assign = {};
2933
- __export(exports_auto_assign, {
2934
- findBestAgent: () => findBestAgent,
2935
- autoAssignTask: () => autoAssignTask
3045
+ // src/lib/github.ts
3046
+ var exports_github = {};
3047
+ __export(exports_github, {
3048
+ parseGitHubUrl: () => parseGitHubUrl,
3049
+ issueToTask: () => issueToTask,
3050
+ fetchGitHubIssue: () => fetchGitHubIssue
2936
3051
  });
2937
- function findBestAgent(_task, db) {
2938
- const d = db || getDatabase();
2939
- const agents = listAgents(d).filter((a) => (a.role || "agent") === "agent");
2940
- if (agents.length === 0)
3052
+ import { execSync } from "child_process";
3053
+ function parseGitHubUrl(url) {
3054
+ const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
3055
+ if (!match)
2941
3056
  return null;
2942
- const inProgressTasks = listTasks({ status: "in_progress" }, d);
2943
- const load = new Map;
2944
- for (const a of agents)
2945
- load.set(a.name, 0);
2946
- for (const t of inProgressTasks) {
2947
- const name = t.assigned_to || t.agent_id;
2948
- if (name && load.has(name)) {
2949
- load.set(name, (load.get(name) || 0) + 1);
2950
- }
2951
- }
2952
- let bestAgent = agents[0].name;
2953
- let bestLoad = load.get(bestAgent) ?? 0;
2954
- for (const a of agents) {
2955
- const l = load.get(a.name) ?? 0;
2956
- if (l < bestLoad) {
2957
- bestAgent = a.name;
2958
- bestLoad = l;
2959
- }
2960
- }
2961
- return bestAgent;
3057
+ return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
2962
3058
  }
2963
- function getAgentWorkloads(d) {
2964
- const rows = d.query("SELECT assigned_to, COUNT(*) as count FROM tasks WHERE status = 'in_progress' AND assigned_to IS NOT NULL GROUP BY assigned_to").all();
2965
- return new Map(rows.map((r) => [r.assigned_to, r.count]));
2966
- }
2967
- function buildPrompt(task, agents) {
2968
- const agentList = agents.map((a) => `- ${a.name} (role: ${a.role}, caps: [${a.capabilities.join(", ")}], active_tasks: ${a.in_progress_tasks})`).join(`
2969
- `);
2970
- return `You are a task routing assistant. Given a task and available agents, choose the SINGLE best agent.
2971
-
2972
- TASK:
2973
- Title: ${task.title}
2974
- Priority: ${task.priority}
2975
- Tags: ${task.tags.join(", ") || "none"}
2976
- Description: ${task.description?.slice(0, 300) || "none"}
2977
-
2978
- AVAILABLE AGENTS:
2979
- ${agentList}
2980
-
2981
- Rules:
2982
- - Match task tags/content to agent capabilities
2983
- - Prefer agents with fewer active tasks
2984
- - Prefer agents whose role fits the task (lead for critical, developer for features, qa for testing)
2985
- - If no clear match, pick the agent with fewest active tasks
2986
-
2987
- Respond with ONLY a JSON object: {"agent_name": "<name>", "reason": "<one sentence>"}`;
2988
- }
2989
- async function callCerebras(prompt, apiKey) {
2990
- try {
2991
- const resp = await fetch(CEREBRAS_API_URL, {
2992
- method: "POST",
2993
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
2994
- body: JSON.stringify({
2995
- model: CEREBRAS_MODEL,
2996
- messages: [{ role: "user", content: prompt }],
2997
- max_tokens: 150,
2998
- temperature: 0
2999
- }),
3000
- signal: AbortSignal.timeout(1e4)
3001
- });
3002
- if (!resp.ok)
3003
- return null;
3004
- const data = await resp.json();
3005
- const content = data?.choices?.[0]?.message?.content?.trim();
3006
- if (!content)
3007
- return null;
3008
- const match = content.match(/\{[^}]+\}/s);
3009
- if (!match)
3010
- return null;
3011
- return JSON.parse(match[0]);
3012
- } catch {
3013
- return null;
3014
- }
3059
+ function fetchGitHubIssue(owner, repo, number) {
3060
+ const json = execSync(`gh api repos/${owner}/${repo}/issues/${number}`, { encoding: "utf-8", timeout: 15000 });
3061
+ const data = JSON.parse(json);
3062
+ return {
3063
+ number: data.number,
3064
+ title: data.title,
3065
+ body: data.body,
3066
+ labels: (data.labels || []).map((l) => l.name),
3067
+ state: data.state,
3068
+ assignee: data.assignee?.login || null,
3069
+ url: data.html_url
3070
+ };
3015
3071
  }
3016
- async function autoAssignTask(taskId, db) {
3017
- const d = db || getDatabase();
3018
- const task = getTask(taskId, d);
3019
- if (!task)
3020
- throw new Error(`Task ${taskId} not found`);
3021
- const agents = listAgents(d).filter((a) => a.status === "active");
3022
- if (agents.length === 0) {
3023
- return { task_id: taskId, assigned_to: null, agent_name: null, method: "no_agents" };
3024
- }
3025
- const workloads = getAgentWorkloads(d);
3026
- const apiKey = process.env["CEREBRAS_API_KEY"];
3027
- let selectedAgent = null;
3028
- let method = "capability_match";
3029
- let reason;
3030
- if (apiKey) {
3031
- const agentData = agents.map((a) => ({
3032
- id: a.id,
3033
- name: a.name,
3034
- role: a.role || "agent",
3035
- capabilities: a.capabilities || [],
3036
- in_progress_tasks: workloads.get(a.id) ?? 0
3037
- }));
3038
- const result = await callCerebras(buildPrompt({
3039
- title: task.title,
3040
- description: task.description,
3041
- priority: task.priority,
3042
- tags: task.tags || []
3043
- }, agentData), apiKey);
3044
- if (result?.agent_name) {
3045
- selectedAgent = agents.find((a) => a.name === result.agent_name) ?? null;
3046
- if (selectedAgent) {
3047
- method = "cerebras";
3048
- reason = result.reason;
3049
- }
3050
- }
3051
- }
3052
- if (!selectedAgent) {
3053
- const taskTags = task.tags || [];
3054
- const capable = getCapableAgents(taskTags, { min_score: 0, limit: 10 }, d);
3055
- if (capable.length > 0) {
3056
- const sorted = capable.sort((a, b) => {
3057
- if (b.score !== a.score)
3058
- return b.score - a.score;
3059
- return (workloads.get(a.agent.id) ?? 0) - (workloads.get(b.agent.id) ?? 0);
3060
- });
3061
- selectedAgent = sorted[0].agent;
3062
- reason = `Capability match (score: ${sorted[0].score.toFixed(2)})`;
3063
- } else {
3064
- selectedAgent = agents.slice().sort((a, b) => (workloads.get(a.id) ?? 0) - (workloads.get(b.id) ?? 0))[0];
3065
- reason = `Least busy agent (${workloads.get(selectedAgent.id) ?? 0} active tasks)`;
3072
+ function issueToTask(issue, opts) {
3073
+ const labelToPriority = {
3074
+ critical: "critical",
3075
+ "priority:critical": "critical",
3076
+ high: "high",
3077
+ "priority:high": "high",
3078
+ urgent: "high",
3079
+ low: "low",
3080
+ "priority:low": "low"
3081
+ };
3082
+ let priority = "medium";
3083
+ for (const label of issue.labels) {
3084
+ const mapped = labelToPriority[label.toLowerCase()];
3085
+ if (mapped) {
3086
+ priority = mapped;
3087
+ break;
3066
3088
  }
3067
3089
  }
3068
- if (selectedAgent) {
3069
- updateTask(taskId, { assigned_to: selectedAgent.id, version: task.version }, d);
3070
- }
3071
3090
  return {
3072
- task_id: taskId,
3073
- assigned_to: selectedAgent?.id ?? null,
3074
- agent_name: selectedAgent?.name ?? null,
3075
- method,
3076
- reason
3091
+ title: `[GH#${issue.number}] ${issue.title}`,
3092
+ description: issue.body ? issue.body.slice(0, 4000) : undefined,
3093
+ tags: issue.labels.slice(0, 10),
3094
+ priority,
3095
+ metadata: { github_url: issue.url, github_number: issue.number, github_state: issue.state },
3096
+ project_id: opts?.project_id,
3097
+ task_list_id: opts?.task_list_id,
3098
+ agent_id: opts?.agent_id
3077
3099
  };
3078
3100
  }
3079
- var CEREBRAS_API_URL = "https://api.cerebras.ai/v1/chat/completions", CEREBRAS_MODEL = "llama-3.3-70b";
3080
- var init_auto_assign = __esm(() => {
3081
- init_database();
3082
- init_tasks();
3083
- init_agents();
3084
- });
3101
+ var init_github = () => {};
3085
3102
 
3086
3103
  // src/db/task-files.ts
3087
3104
  var exports_task_files = {};
@@ -3241,12 +3258,254 @@ function bulkAddTaskFiles(taskId, paths, agentId, db) {
3241
3258
  for (const path of paths) {
3242
3259
  results.push(addTaskFile({ task_id: taskId, path, agent_id: agentId }, d));
3243
3260
  }
3244
- });
3245
- tx();
3246
- return results;
3261
+ });
3262
+ tx();
3263
+ return results;
3264
+ }
3265
+ var init_task_files = __esm(() => {
3266
+ init_database();
3267
+ });
3268
+
3269
+ // src/lib/burndown.ts
3270
+ var exports_burndown = {};
3271
+ __export(exports_burndown, {
3272
+ getBurndown: () => getBurndown
3273
+ });
3274
+ function getBurndown(opts, db) {
3275
+ const d = db || getDatabase();
3276
+ const conditions = [];
3277
+ const params = [];
3278
+ if (opts.plan_id) {
3279
+ conditions.push("plan_id = ?");
3280
+ params.push(opts.plan_id);
3281
+ }
3282
+ if (opts.project_id) {
3283
+ conditions.push("project_id = ?");
3284
+ params.push(opts.project_id);
3285
+ }
3286
+ if (opts.task_list_id) {
3287
+ conditions.push("task_list_id = ?");
3288
+ params.push(opts.task_list_id);
3289
+ }
3290
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3291
+ const total = d.query(`SELECT COUNT(*) as c FROM tasks ${where}`).get(...params).c;
3292
+ const completed = d.query(`SELECT COUNT(*) as c FROM tasks ${where}${where ? " AND" : " WHERE"} status = 'completed'`).get(...params).c;
3293
+ const completions = d.query(`SELECT DATE(completed_at) as date, COUNT(*) as count FROM tasks ${where}${where ? " AND" : " WHERE"} status = 'completed' AND completed_at IS NOT NULL GROUP BY DATE(completed_at) ORDER BY date`).all(...params);
3294
+ const firstTask = d.query(`SELECT MIN(created_at) as min_date FROM tasks ${where}`).get(...params);
3295
+ const startDate = firstTask?.min_date ? new Date(firstTask.min_date) : new Date;
3296
+ const endDate = new Date;
3297
+ const days = [];
3298
+ const totalDays = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)));
3299
+ let cumulative = 0;
3300
+ const completionMap = new Map(completions.map((c) => [c.date, c.count]));
3301
+ const current = new Date(startDate);
3302
+ for (let i = 0;i <= totalDays; i++) {
3303
+ const dateStr = current.toISOString().slice(0, 10);
3304
+ cumulative += completionMap.get(dateStr) || 0;
3305
+ days.push({
3306
+ date: dateStr,
3307
+ completed_cumulative: cumulative,
3308
+ ideal: Math.round(total / totalDays * i)
3309
+ });
3310
+ current.setDate(current.getDate() + 1);
3311
+ }
3312
+ const chart = renderBurndownChart(total, days);
3313
+ return { total, completed, remaining: total - completed, days, chart };
3314
+ }
3315
+ function renderBurndownChart(total, days) {
3316
+ const height = 12;
3317
+ const width = Math.min(60, days.length);
3318
+ const step = Math.max(1, Math.floor(days.length / width));
3319
+ const sampled = days.filter((_, i) => i % step === 0 || i === days.length - 1).slice(0, width);
3320
+ const lines = [];
3321
+ lines.push(` ${total} \u2524`);
3322
+ for (let row = height - 1;row >= 0; row--) {
3323
+ const threshold = Math.round(total / height * row);
3324
+ let line = "";
3325
+ for (const day of sampled) {
3326
+ const remaining = total - day.completed_cumulative;
3327
+ const idealRemaining = total - day.ideal;
3328
+ if (remaining >= threshold && remaining > threshold - Math.round(total / height)) {
3329
+ line += "\u2588";
3330
+ } else if (idealRemaining >= threshold && idealRemaining > threshold - Math.round(total / height)) {
3331
+ line += "\xB7";
3332
+ } else {
3333
+ line += " ";
3334
+ }
3335
+ }
3336
+ const label = String(threshold).padStart(4);
3337
+ lines.push(`${label} \u2524${line}`);
3338
+ }
3339
+ lines.push(` 0 \u2524${"\u2500".repeat(sampled.length)}`);
3340
+ lines.push(` \u2514${"\u2500".repeat(sampled.length)}`);
3341
+ if (sampled.length > 0) {
3342
+ const first = sampled[0].date.slice(5);
3343
+ const last = sampled[sampled.length - 1].date.slice(5);
3344
+ const pad = sampled.length - first.length - last.length;
3345
+ lines.push(` ${first}${" ".repeat(Math.max(1, pad))}${last}`);
3346
+ }
3347
+ lines.push("");
3348
+ lines.push(` \u2588 actual remaining \xB7 ideal burndown`);
3349
+ return lines.join(`
3350
+ `);
3351
+ }
3352
+ var init_burndown = __esm(() => {
3353
+ init_database();
3354
+ });
3355
+
3356
+ // src/lib/auto-assign.ts
3357
+ var exports_auto_assign = {};
3358
+ __export(exports_auto_assign, {
3359
+ findBestAgent: () => findBestAgent,
3360
+ autoAssignTask: () => autoAssignTask
3361
+ });
3362
+ function findBestAgent(_task, db) {
3363
+ const d = db || getDatabase();
3364
+ const agents = listAgents(d).filter((a) => (a.role || "agent") === "agent");
3365
+ if (agents.length === 0)
3366
+ return null;
3367
+ const inProgressTasks = listTasks({ status: "in_progress" }, d);
3368
+ const load = new Map;
3369
+ for (const a of agents)
3370
+ load.set(a.name, 0);
3371
+ for (const t of inProgressTasks) {
3372
+ const name = t.assigned_to || t.agent_id;
3373
+ if (name && load.has(name)) {
3374
+ load.set(name, (load.get(name) || 0) + 1);
3375
+ }
3376
+ }
3377
+ let bestAgent = agents[0].name;
3378
+ let bestLoad = load.get(bestAgent) ?? 0;
3379
+ for (const a of agents) {
3380
+ const l = load.get(a.name) ?? 0;
3381
+ if (l < bestLoad) {
3382
+ bestAgent = a.name;
3383
+ bestLoad = l;
3384
+ }
3385
+ }
3386
+ return bestAgent;
3387
+ }
3388
+ function getAgentWorkloads(d) {
3389
+ const rows = d.query("SELECT assigned_to, COUNT(*) as count FROM tasks WHERE status = 'in_progress' AND assigned_to IS NOT NULL GROUP BY assigned_to").all();
3390
+ return new Map(rows.map((r) => [r.assigned_to, r.count]));
3391
+ }
3392
+ function buildPrompt(task, agents) {
3393
+ const agentList = agents.map((a) => `- ${a.name} (role: ${a.role}, caps: [${a.capabilities.join(", ")}], active_tasks: ${a.in_progress_tasks})`).join(`
3394
+ `);
3395
+ return `You are a task routing assistant. Given a task and available agents, choose the SINGLE best agent.
3396
+
3397
+ TASK:
3398
+ Title: ${task.title}
3399
+ Priority: ${task.priority}
3400
+ Tags: ${task.tags.join(", ") || "none"}
3401
+ Description: ${task.description?.slice(0, 300) || "none"}
3402
+
3403
+ AVAILABLE AGENTS:
3404
+ ${agentList}
3405
+
3406
+ Rules:
3407
+ - Match task tags/content to agent capabilities
3408
+ - Prefer agents with fewer active tasks
3409
+ - Prefer agents whose role fits the task (lead for critical, developer for features, qa for testing)
3410
+ - If no clear match, pick the agent with fewest active tasks
3411
+
3412
+ Respond with ONLY a JSON object: {"agent_name": "<name>", "reason": "<one sentence>"}`;
3413
+ }
3414
+ async function callCerebras(prompt, apiKey) {
3415
+ try {
3416
+ const resp = await fetch(CEREBRAS_API_URL, {
3417
+ method: "POST",
3418
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
3419
+ body: JSON.stringify({
3420
+ model: CEREBRAS_MODEL,
3421
+ messages: [{ role: "user", content: prompt }],
3422
+ max_tokens: 150,
3423
+ temperature: 0
3424
+ }),
3425
+ signal: AbortSignal.timeout(1e4)
3426
+ });
3427
+ if (!resp.ok)
3428
+ return null;
3429
+ const data = await resp.json();
3430
+ const content = data?.choices?.[0]?.message?.content?.trim();
3431
+ if (!content)
3432
+ return null;
3433
+ const match = content.match(/\{[^}]+\}/s);
3434
+ if (!match)
3435
+ return null;
3436
+ return JSON.parse(match[0]);
3437
+ } catch {
3438
+ return null;
3439
+ }
3440
+ }
3441
+ async function autoAssignTask(taskId, db) {
3442
+ const d = db || getDatabase();
3443
+ const task = getTask(taskId, d);
3444
+ if (!task)
3445
+ throw new Error(`Task ${taskId} not found`);
3446
+ const agents = listAgents(d).filter((a) => a.status === "active");
3447
+ if (agents.length === 0) {
3448
+ return { task_id: taskId, assigned_to: null, agent_name: null, method: "no_agents" };
3449
+ }
3450
+ const workloads = getAgentWorkloads(d);
3451
+ const apiKey = process.env["CEREBRAS_API_KEY"];
3452
+ let selectedAgent = null;
3453
+ let method = "capability_match";
3454
+ let reason;
3455
+ if (apiKey) {
3456
+ const agentData = agents.map((a) => ({
3457
+ id: a.id,
3458
+ name: a.name,
3459
+ role: a.role || "agent",
3460
+ capabilities: a.capabilities || [],
3461
+ in_progress_tasks: workloads.get(a.id) ?? 0
3462
+ }));
3463
+ const result = await callCerebras(buildPrompt({
3464
+ title: task.title,
3465
+ description: task.description,
3466
+ priority: task.priority,
3467
+ tags: task.tags || []
3468
+ }, agentData), apiKey);
3469
+ if (result?.agent_name) {
3470
+ selectedAgent = agents.find((a) => a.name === result.agent_name) ?? null;
3471
+ if (selectedAgent) {
3472
+ method = "cerebras";
3473
+ reason = result.reason;
3474
+ }
3475
+ }
3476
+ }
3477
+ if (!selectedAgent) {
3478
+ const taskTags = task.tags || [];
3479
+ const capable = getCapableAgents(taskTags, { min_score: 0, limit: 10 }, d);
3480
+ if (capable.length > 0) {
3481
+ const sorted = capable.sort((a, b) => {
3482
+ if (b.score !== a.score)
3483
+ return b.score - a.score;
3484
+ return (workloads.get(a.agent.id) ?? 0) - (workloads.get(b.agent.id) ?? 0);
3485
+ });
3486
+ selectedAgent = sorted[0].agent;
3487
+ reason = `Capability match (score: ${sorted[0].score.toFixed(2)})`;
3488
+ } else {
3489
+ selectedAgent = agents.slice().sort((a, b) => (workloads.get(a.id) ?? 0) - (workloads.get(b.id) ?? 0))[0];
3490
+ reason = `Least busy agent (${workloads.get(selectedAgent.id) ?? 0} active tasks)`;
3491
+ }
3492
+ }
3493
+ if (selectedAgent) {
3494
+ updateTask(taskId, { assigned_to: selectedAgent.id, version: task.version }, d);
3495
+ }
3496
+ return {
3497
+ task_id: taskId,
3498
+ assigned_to: selectedAgent?.id ?? null,
3499
+ agent_name: selectedAgent?.name ?? null,
3500
+ method,
3501
+ reason
3502
+ };
3247
3503
  }
3248
- var init_task_files = __esm(() => {
3504
+ var CEREBRAS_API_URL = "https://api.cerebras.ai/v1/chat/completions", CEREBRAS_MODEL = "llama-3.3-70b";
3505
+ var init_auto_assign = __esm(() => {
3249
3506
  init_database();
3507
+ init_tasks();
3508
+ init_agents();
3250
3509
  });
3251
3510
 
3252
3511
  // src/db/file-locks.ts
@@ -4041,6 +4300,212 @@ var init_agent_metrics = __esm(() => {
4041
4300
  init_database();
4042
4301
  });
4043
4302
 
4303
+ // src/lib/extract.ts
4304
+ var exports_extract = {};
4305
+ __export(exports_extract, {
4306
+ tagToPriority: () => tagToPriority,
4307
+ extractTodos: () => extractTodos,
4308
+ extractFromSource: () => extractFromSource,
4309
+ EXTRACT_TAGS: () => EXTRACT_TAGS
4310
+ });
4311
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
4312
+ import { relative, resolve as resolve2, join as join6 } from "path";
4313
+ function tagToPriority(tag) {
4314
+ switch (tag) {
4315
+ case "BUG":
4316
+ case "FIXME":
4317
+ return "high";
4318
+ case "HACK":
4319
+ case "XXX":
4320
+ return "medium";
4321
+ case "TODO":
4322
+ return "medium";
4323
+ case "NOTE":
4324
+ return "low";
4325
+ }
4326
+ }
4327
+ function buildTagRegex(tags) {
4328
+ const tagPattern = tags.join("|");
4329
+ return new RegExp(`(?:^|\\s)(?:\\/\\/|\\/\\*|#|\\*|--|;;|%|<!--|\\{-)\\s*(?:@?)(${tagPattern})\\s*[:(]?\\s*(.*)$`, "i");
4330
+ }
4331
+ function extractFromSource(source, filePath, tags = [...EXTRACT_TAGS]) {
4332
+ const regex = buildTagRegex(tags);
4333
+ const results = [];
4334
+ const lines = source.split(`
4335
+ `);
4336
+ for (let i = 0;i < lines.length; i++) {
4337
+ const line = lines[i];
4338
+ const match = line.match(regex);
4339
+ if (match) {
4340
+ const tag = match[1].toUpperCase();
4341
+ let message = match[2].trim();
4342
+ message = message.replace(/\s*\*\/\s*$/, "").replace(/\s*-->\s*$/, "").replace(/\s*-\}\s*$/, "").trim();
4343
+ if (message) {
4344
+ results.push({
4345
+ tag,
4346
+ message,
4347
+ file: filePath,
4348
+ line: i + 1,
4349
+ raw: line
4350
+ });
4351
+ }
4352
+ }
4353
+ }
4354
+ return results;
4355
+ }
4356
+ function collectFiles(basePath, extensions) {
4357
+ const stat = statSync2(basePath);
4358
+ if (stat.isFile()) {
4359
+ return [basePath];
4360
+ }
4361
+ const glob = new Bun.Glob("**/*");
4362
+ const files = [];
4363
+ for (const entry of glob.scanSync({ cwd: basePath, onlyFiles: true, dot: false })) {
4364
+ const parts = entry.split("/");
4365
+ if (parts.some((p) => SKIP_DIRS.has(p)))
4366
+ continue;
4367
+ const dotIdx = entry.lastIndexOf(".");
4368
+ if (dotIdx === -1)
4369
+ continue;
4370
+ const ext = entry.slice(dotIdx);
4371
+ if (!extensions.has(ext))
4372
+ continue;
4373
+ files.push(entry);
4374
+ }
4375
+ return files.sort();
4376
+ }
4377
+ function extractTodos(options, db) {
4378
+ const basePath = resolve2(options.path);
4379
+ const tags = options.patterns || [...EXTRACT_TAGS];
4380
+ const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
4381
+ const files = collectFiles(basePath, extensions);
4382
+ const allComments = [];
4383
+ for (const file of files) {
4384
+ const fullPath = statSync2(basePath).isFile() ? basePath : join6(basePath, file);
4385
+ try {
4386
+ const source = readFileSync3(fullPath, "utf-8");
4387
+ const relPath = statSync2(basePath).isFile() ? relative(resolve2(basePath, ".."), fullPath) : file;
4388
+ const comments = extractFromSource(source, relPath, tags);
4389
+ allComments.push(...comments);
4390
+ } catch {}
4391
+ }
4392
+ if (options.dry_run) {
4393
+ return { comments: allComments, tasks: [], skipped: 0 };
4394
+ }
4395
+ const tasks = [];
4396
+ let skipped = 0;
4397
+ const existingTasks = options.project_id ? listTasks({ project_id: options.project_id, tags: ["extracted"] }, db) : listTasks({ tags: ["extracted"] }, db);
4398
+ const existingKeys = new Set;
4399
+ for (const t of existingTasks) {
4400
+ const meta = t.metadata;
4401
+ if (meta?.["source_file"] && meta?.["source_line"]) {
4402
+ existingKeys.add(`${meta["source_file"]}:${meta["source_line"]}`);
4403
+ }
4404
+ }
4405
+ for (const comment of allComments) {
4406
+ const dedupKey = `${comment.file}:${comment.line}`;
4407
+ if (existingKeys.has(dedupKey)) {
4408
+ skipped++;
4409
+ continue;
4410
+ }
4411
+ const taskTags = ["extracted", comment.tag.toLowerCase(), ...options.tags || []];
4412
+ const task = createTask({
4413
+ title: `[${comment.tag}] ${comment.message}`,
4414
+ description: `Extracted from code comment in \`${comment.file}\` at line ${comment.line}:
4415
+ \`\`\`
4416
+ ${comment.raw.trim()}
4417
+ \`\`\``,
4418
+ priority: tagToPriority(comment.tag),
4419
+ project_id: options.project_id,
4420
+ task_list_id: options.task_list_id,
4421
+ assigned_to: options.assigned_to,
4422
+ agent_id: options.agent_id,
4423
+ tags: taskTags,
4424
+ metadata: {
4425
+ source: "code_comment",
4426
+ comment_type: comment.tag,
4427
+ source_file: comment.file,
4428
+ source_line: comment.line
4429
+ }
4430
+ }, db);
4431
+ addTaskFile({
4432
+ task_id: task.id,
4433
+ path: comment.file,
4434
+ note: `Line ${comment.line}: ${comment.tag} comment`
4435
+ }, db);
4436
+ tasks.push(task);
4437
+ existingKeys.add(dedupKey);
4438
+ }
4439
+ return { comments: allComments, tasks, skipped };
4440
+ }
4441
+ var EXTRACT_TAGS, DEFAULT_EXTENSIONS, SKIP_DIRS;
4442
+ var init_extract = __esm(() => {
4443
+ init_tasks();
4444
+ init_task_files();
4445
+ EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
4446
+ DEFAULT_EXTENSIONS = new Set([
4447
+ ".ts",
4448
+ ".tsx",
4449
+ ".js",
4450
+ ".jsx",
4451
+ ".mjs",
4452
+ ".cjs",
4453
+ ".py",
4454
+ ".rb",
4455
+ ".go",
4456
+ ".rs",
4457
+ ".c",
4458
+ ".cpp",
4459
+ ".h",
4460
+ ".hpp",
4461
+ ".java",
4462
+ ".kt",
4463
+ ".swift",
4464
+ ".cs",
4465
+ ".php",
4466
+ ".sh",
4467
+ ".bash",
4468
+ ".zsh",
4469
+ ".lua",
4470
+ ".sql",
4471
+ ".r",
4472
+ ".R",
4473
+ ".yaml",
4474
+ ".yml",
4475
+ ".toml",
4476
+ ".css",
4477
+ ".scss",
4478
+ ".less",
4479
+ ".vue",
4480
+ ".svelte",
4481
+ ".ex",
4482
+ ".exs",
4483
+ ".erl",
4484
+ ".hs",
4485
+ ".ml",
4486
+ ".mli",
4487
+ ".clj",
4488
+ ".cljs"
4489
+ ]);
4490
+ SKIP_DIRS = new Set([
4491
+ "node_modules",
4492
+ ".git",
4493
+ "dist",
4494
+ "build",
4495
+ "out",
4496
+ ".next",
4497
+ ".turbo",
4498
+ "coverage",
4499
+ "__pycache__",
4500
+ ".venv",
4501
+ "venv",
4502
+ "vendor",
4503
+ "target",
4504
+ ".cache",
4505
+ ".parcel-cache"
4506
+ ]);
4507
+ });
4508
+
4044
4509
  // src/mcp/index.ts
4045
4510
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4046
4511
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -8828,14 +9293,14 @@ init_config();
8828
9293
  init_database();
8829
9294
  init_checklists();
8830
9295
  init_types();
8831
- import { readFileSync as readFileSync3 } from "fs";
8832
- import { join as join6, dirname as dirname2 } from "path";
9296
+ import { readFileSync as readFileSync4 } from "fs";
9297
+ import { join as join7, dirname as dirname2 } from "path";
8833
9298
  import { fileURLToPath } from "url";
8834
9299
  function getMcpVersion() {
8835
9300
  try {
8836
9301
  const __dir = dirname2(fileURLToPath(import.meta.url));
8837
- const pkgPath = join6(__dir, "..", "package.json");
8838
- return JSON.parse(readFileSync3(pkgPath, "utf-8")).version || "0.0.0";
9302
+ const pkgPath = join7(__dir, "..", "package.json");
9303
+ return JSON.parse(readFileSync4(pkgPath, "utf-8")).version || "0.0.0";
8839
9304
  } catch {
8840
9305
  return "0.0.0";
8841
9306
  }
@@ -8857,7 +9322,8 @@ var MINIMAL_TOOLS = new Set([
8857
9322
  "get_next_task",
8858
9323
  "bootstrap",
8859
9324
  "get_tasks_changed_since",
8860
- "heartbeat"
9325
+ "heartbeat",
9326
+ "release_agent"
8861
9327
  ]);
8862
9328
  var STANDARD_EXCLUDED = new Set([
8863
9329
  "rename_agent",
@@ -9018,7 +9484,8 @@ if (shouldRegisterTool("create_task")) {
9018
9484
  spawns_template_id: exports_external.string().optional().describe("Template ID to auto-create as next task when this task is completed (pipeline/handoff chains)"),
9019
9485
  reason: exports_external.string().optional().describe("Why this task exists \u2014 context for agents picking it up"),
9020
9486
  spawned_from_session: exports_external.string().optional().describe("Session ID that created this task (for tracing task lineage)"),
9021
- assigned_from_project: exports_external.string().optional().describe("Override: project ID the assigning agent is working from. Auto-detected from agent focus if omitted.")
9487
+ assigned_from_project: exports_external.string().optional().describe("Override: project ID the assigning agent is working from. Auto-detected from agent focus if omitted."),
9488
+ task_type: exports_external.string().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or any custom string")
9022
9489
  }, async (params) => {
9023
9490
  try {
9024
9491
  if (!params.agent_id) {
@@ -9064,6 +9531,7 @@ if (shouldRegisterTool("list_tasks")) {
9064
9531
  has_recurrence: exports_external.boolean().optional(),
9065
9532
  due_today: exports_external.boolean().optional(),
9066
9533
  overdue: exports_external.boolean().optional(),
9534
+ task_type: exports_external.union([exports_external.string(), exports_external.array(exports_external.string())]).optional().describe("Filter by task type: bug, feature, chore, improvement, docs, test, security, or custom"),
9067
9535
  limit: exports_external.number().optional(),
9068
9536
  offset: exports_external.number().optional(),
9069
9537
  summary_only: exports_external.boolean().optional().describe("When true, return only id, short_id, title, status, priority \u2014 minimal tokens for navigation"),
@@ -9198,11 +9666,17 @@ if (shouldRegisterTool("update_task")) {
9198
9666
  tags: exports_external.array(exports_external.string()).optional(),
9199
9667
  metadata: exports_external.record(exports_external.unknown()).optional(),
9200
9668
  plan_id: exports_external.string().optional(),
9201
- task_list_id: exports_external.string().optional()
9669
+ task_list_id: exports_external.string().optional(),
9670
+ task_type: exports_external.string().nullable().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or custom. null to clear.")
9202
9671
  }, async ({ id, ...rest }) => {
9203
9672
  try {
9204
9673
  const resolvedId = resolveId(id);
9205
- const task = updateTask(resolvedId, rest);
9674
+ const resolved = { ...rest };
9675
+ if (resolved.task_list_id)
9676
+ resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
9677
+ if (resolved.plan_id)
9678
+ resolved.plan_id = resolveId(resolved.plan_id, "plans");
9679
+ const task = updateTask(resolvedId, resolved);
9206
9680
  return { content: [{ type: "text", text: `updated: ${formatTask(task)}` }] };
9207
9681
  } catch (e) {
9208
9682
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -9842,11 +10316,12 @@ if (shouldRegisterTool("register_agent")) {
9842
10316
  description: exports_external.string().optional(),
9843
10317
  capabilities: exports_external.array(exports_external.string()).optional().describe("Agent capabilities/skills for task routing (e.g. ['typescript', 'testing', 'devops'])"),
9844
10318
  session_id: exports_external.string().optional().describe("Unique ID for this coding session (e.g. process PID + timestamp, or env var). Used to detect name collisions across sessions. Store it and pass on every register_agent call."),
9845
- working_dir: exports_external.string().optional().describe("Working directory of this session \u2014 used to look up the project's agent pool and identify who holds the name in a conflict")
9846
- }, async ({ name, description, capabilities, session_id, working_dir }) => {
10319
+ working_dir: exports_external.string().optional().describe("Working directory of this session \u2014 used to look up the project's agent pool and identify who holds the name in a conflict"),
10320
+ force: exports_external.boolean().optional().describe("Force takeover of an active agent's name. Use with caution \u2014 only when you know the previous session is dead.")
10321
+ }, async ({ name, description, capabilities, session_id, working_dir, force }) => {
9847
10322
  try {
9848
10323
  const pool = getAgentPoolForProject(working_dir);
9849
- const result = registerAgent({ name, description, capabilities, session_id, working_dir, pool: pool || undefined });
10324
+ const result = registerAgent({ name, description, capabilities, session_id, working_dir, force, pool: pool || undefined });
9850
10325
  if (isAgentConflict(result)) {
9851
10326
  const suggestLine = result.suggestions && result.suggestions.length > 0 ? `
9852
10327
  Available names: ${result.suggestions.join(", ")}` : "";
@@ -10065,6 +10540,31 @@ if (shouldRegisterTool("heartbeat")) {
10065
10540
  }
10066
10541
  });
10067
10542
  }
10543
+ if (shouldRegisterTool("release_agent")) {
10544
+ server.tool("release_agent", "Explicitly release/logout an agent \u2014 clears session binding and makes the name immediately available. Call this when your session ends instead of waiting for the 30-minute stale timeout.", {
10545
+ agent_id: exports_external.string().describe("Your agent ID or name."),
10546
+ session_id: exports_external.string().optional().describe("Your session ID \u2014 if provided, release only succeeds if it matches (prevents other sessions from releasing your agent).")
10547
+ }, async ({ agent_id, session_id }) => {
10548
+ try {
10549
+ const agent = getAgent(agent_id) || getAgentByName(agent_id);
10550
+ if (!agent) {
10551
+ return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
10552
+ }
10553
+ const released = releaseAgent(agent.id, session_id);
10554
+ if (!released) {
10555
+ return { content: [{ type: "text", text: `Release denied: session_id does not match agent's current session.` }], isError: true };
10556
+ }
10557
+ return {
10558
+ content: [{
10559
+ type: "text",
10560
+ text: `Agent released: ${agent.name} (${agent.id}) \u2014 session cleared, name is now available.`
10561
+ }]
10562
+ };
10563
+ } catch (e) {
10564
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10565
+ }
10566
+ });
10567
+ }
10068
10568
  if (shouldRegisterTool("create_task_list")) {
10069
10569
  server.tool("create_task_list", "Create a task list container for organizing tasks.", {
10070
10570
  name: exports_external.string(),
@@ -10223,6 +10723,178 @@ ${text}` }] };
10223
10723
  }
10224
10724
  });
10225
10725
  }
10726
+ if (shouldRegisterTool("recap")) {
10727
+ server.tool("recap", "Get a summary of what happened in the last N hours \u2014 completed tasks with duration, new tasks, in-progress work, blockers, stale tasks, and agent activity. Great for session start or standup prep.", {
10728
+ hours: exports_external.number().optional().describe("Look back N hours (default: 8)"),
10729
+ project_id: exports_external.string().optional().describe("Filter to a specific project")
10730
+ }, async ({ hours, project_id }) => {
10731
+ try {
10732
+ const { getRecap: getRecap2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
10733
+ const recap = getRecap2(hours || 8, project_id);
10734
+ const lines = [`Recap \u2014 last ${recap.hours}h (since ${recap.since})`];
10735
+ if (recap.completed.length > 0) {
10736
+ lines.push(`
10737
+ Completed (${recap.completed.length}):`);
10738
+ for (const t of recap.completed) {
10739
+ const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
10740
+ lines.push(` \u2713 ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
10741
+ }
10742
+ }
10743
+ if (recap.in_progress.length > 0) {
10744
+ lines.push(`
10745
+ In Progress (${recap.in_progress.length}):`);
10746
+ for (const t of recap.in_progress)
10747
+ lines.push(` \u2192 ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
10748
+ }
10749
+ if (recap.blocked.length > 0) {
10750
+ lines.push(`
10751
+ Blocked (${recap.blocked.length}):`);
10752
+ for (const t of recap.blocked)
10753
+ lines.push(` \u2717 ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
10754
+ }
10755
+ if (recap.stale.length > 0) {
10756
+ lines.push(`
10757
+ Stale (${recap.stale.length}):`);
10758
+ for (const t of recap.stale)
10759
+ lines.push(` ! ${t.short_id || t.id.slice(0, 8)} ${t.title} \u2014 updated ${t.updated_at}`);
10760
+ }
10761
+ if (recap.agents.length > 0) {
10762
+ lines.push(`
10763
+ Agents:`);
10764
+ for (const a of recap.agents)
10765
+ lines.push(` ${a.name}: ${a.completed_count} done, ${a.in_progress_count} active (seen ${a.last_seen_at})`);
10766
+ }
10767
+ lines.push(`
10768
+ Created: ${recap.created.length} new tasks`);
10769
+ return { content: [{ type: "text", text: lines.join(`
10770
+ `) }] };
10771
+ } catch (e) {
10772
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10773
+ }
10774
+ });
10775
+ }
10776
+ if (shouldRegisterTool("standup")) {
10777
+ server.tool("standup", "Generate standup notes \u2014 completed tasks since yesterday grouped by agent, in-progress work, and blockers. Copy-paste ready.", {
10778
+ hours: exports_external.number().optional().describe("Look back N hours (default: 24)"),
10779
+ project_id: exports_external.string().optional()
10780
+ }, async ({ hours, project_id }) => {
10781
+ try {
10782
+ const { getRecap: getRecap2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
10783
+ const recap = getRecap2(hours || 24, project_id);
10784
+ const lines = [`Standup \u2014 last ${recap.hours}h`];
10785
+ const byAgent = new Map;
10786
+ for (const t of recap.completed) {
10787
+ const agent = t.assigned_to || "unassigned";
10788
+ if (!byAgent.has(agent))
10789
+ byAgent.set(agent, []);
10790
+ byAgent.get(agent).push(t);
10791
+ }
10792
+ if (byAgent.size > 0) {
10793
+ lines.push(`
10794
+ Done:`);
10795
+ for (const [agent, tasks] of byAgent) {
10796
+ lines.push(` ${agent}:`);
10797
+ for (const t of tasks) {
10798
+ const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
10799
+ lines.push(` \u2713 ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}`);
10800
+ }
10801
+ }
10802
+ } else {
10803
+ lines.push(`
10804
+ Nothing completed.`);
10805
+ }
10806
+ if (recap.in_progress.length > 0) {
10807
+ lines.push(`
10808
+ In Progress:`);
10809
+ for (const t of recap.in_progress)
10810
+ lines.push(` \u2192 ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
10811
+ }
10812
+ if (recap.blocked.length > 0) {
10813
+ lines.push(`
10814
+ Blocked:`);
10815
+ for (const t of recap.blocked)
10816
+ lines.push(` \u2717 ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
10817
+ }
10818
+ return { content: [{ type: "text", text: lines.join(`
10819
+ `) }] };
10820
+ } catch (e) {
10821
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10822
+ }
10823
+ });
10824
+ }
10825
+ if (shouldRegisterTool("import_github_issue")) {
10826
+ server.tool("import_github_issue", "Import a GitHub issue as a task. Requires gh CLI installed and authenticated.", {
10827
+ url: exports_external.string().describe("GitHub issue URL (e.g. https://github.com/owner/repo/issues/42)"),
10828
+ project_id: exports_external.string().optional(),
10829
+ task_list_id: exports_external.string().optional()
10830
+ }, async ({ url, project_id, task_list_id }) => {
10831
+ try {
10832
+ const { parseGitHubUrl: parseGitHubUrl2, fetchGitHubIssue: fetchGitHubIssue2, issueToTask: issueToTask2 } = await Promise.resolve().then(() => (init_github(), exports_github));
10833
+ const parsed = parseGitHubUrl2(url);
10834
+ if (!parsed)
10835
+ return { content: [{ type: "text", text: "Invalid GitHub issue URL." }], isError: true };
10836
+ const issue = fetchGitHubIssue2(parsed.owner, parsed.repo, parsed.number);
10837
+ const input = issueToTask2(issue, { project_id, task_list_id });
10838
+ const task = createTask(input);
10839
+ return { content: [{ type: "text", text: `Imported GH#${issue.number}: ${issue.title}
10840
+ Task: ${task.short_id || task.id} [${task.priority}]
10841
+ Labels: ${issue.labels.join(", ") || "none"}` }] };
10842
+ } catch (e) {
10843
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10844
+ }
10845
+ });
10846
+ }
10847
+ if (shouldRegisterTool("blame")) {
10848
+ server.tool("blame", "Show which tasks and agents touched a file \u2014 combines task_files and task_commits data.", {
10849
+ path: exports_external.string().describe("File path to look up")
10850
+ }, async ({ path }) => {
10851
+ try {
10852
+ const { findTasksByFile: findTasksByFile2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
10853
+ const db = getDatabase();
10854
+ const taskFiles = findTasksByFile2(path, db);
10855
+ const commitRows = db.query("SELECT tc.*, t.title, t.short_id FROM task_commits tc JOIN tasks t ON t.id = tc.task_id WHERE tc.files_changed LIKE ? ORDER BY tc.committed_at DESC").all(`%${path}%`);
10856
+ const lines = [`Blame: ${path}`];
10857
+ if (taskFiles.length > 0) {
10858
+ lines.push(`
10859
+ Task File Links (${taskFiles.length}):`);
10860
+ for (const tf of taskFiles) {
10861
+ const task = getTask(tf.task_id, db);
10862
+ lines.push(` ${task?.short_id || tf.task_id.slice(0, 8)} ${task?.title || "?"} \u2014 ${tf.role || "file"}`);
10863
+ }
10864
+ }
10865
+ if (commitRows.length > 0) {
10866
+ lines.push(`
10867
+ Commit Links (${commitRows.length}):`);
10868
+ for (const c of commitRows)
10869
+ lines.push(` ${c.sha?.slice(0, 7)} ${c.short_id || c.task_id.slice(0, 8)} ${c.title || ""} \u2014 ${c.author || ""}`);
10870
+ }
10871
+ if (taskFiles.length === 0 && commitRows.length === 0) {
10872
+ lines.push("No task or commit links found.");
10873
+ }
10874
+ return { content: [{ type: "text", text: lines.join(`
10875
+ `) }] };
10876
+ } catch (e) {
10877
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10878
+ }
10879
+ });
10880
+ }
10881
+ if (shouldRegisterTool("burndown")) {
10882
+ server.tool("burndown", "ASCII burndown chart showing actual vs ideal progress for a plan, project, or task list.", {
10883
+ plan_id: exports_external.string().optional(),
10884
+ project_id: exports_external.string().optional(),
10885
+ task_list_id: exports_external.string().optional()
10886
+ }, async ({ plan_id, project_id, task_list_id }) => {
10887
+ try {
10888
+ const { getBurndown: getBurndown2 } = await Promise.resolve().then(() => (init_burndown(), exports_burndown));
10889
+ const data = getBurndown2({ plan_id, project_id, task_list_id });
10890
+ return { content: [{ type: "text", text: `Burndown: ${data.completed}/${data.total} done, ${data.remaining} remaining
10891
+
10892
+ ${data.chart}` }] };
10893
+ } catch (e) {
10894
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10895
+ }
10896
+ });
10897
+ }
10226
10898
  if (shouldRegisterTool("create_webhook")) {
10227
10899
  server.tool("create_webhook", "Register a webhook for task change events.", {
10228
10900
  url: exports_external.string(),
@@ -11028,6 +11700,91 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
11028
11700
  }
11029
11701
  });
11030
11702
  }
11703
+ if (shouldRegisterTool("task_context")) {
11704
+ server.tool("task_context", "Full orientation for a specific task \u2014 details, description, dependencies (with blocked status), files, commits, comments, checklist. Use when starting work on a task.", {
11705
+ id: exports_external.string().describe("Task ID, short_id, or partial ID")
11706
+ }, async ({ id }) => {
11707
+ try {
11708
+ const resolvedId = resolveId(id, "tasks");
11709
+ const task = getTaskWithRelations(resolvedId);
11710
+ if (!task)
11711
+ return { content: [{ type: "text", text: `Task not found: ${id}` }], isError: true };
11712
+ const lines = [];
11713
+ const sid = task.short_id || task.id.slice(0, 8);
11714
+ lines.push(`${sid} [${task.status}] [${task.priority}] ${task.title}`);
11715
+ if (task.description)
11716
+ lines.push(`
11717
+ Description:
11718
+ ${task.description}`);
11719
+ if (task.assigned_to)
11720
+ lines.push(`Assigned: ${task.assigned_to}`);
11721
+ if (task.started_at)
11722
+ lines.push(`Started: ${task.started_at}`);
11723
+ if (task.completed_at) {
11724
+ lines.push(`Completed: ${task.completed_at}`);
11725
+ if (task.started_at) {
11726
+ const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
11727
+ lines.push(`Duration: ${dur}m`);
11728
+ }
11729
+ }
11730
+ if (task.tags.length > 0)
11731
+ lines.push(`Tags: ${task.tags.join(", ")}`);
11732
+ if (task.dependencies.length > 0) {
11733
+ lines.push(`
11734
+ Depends on (${task.dependencies.length}):`);
11735
+ for (const dep of task.dependencies) {
11736
+ const blocked = dep.status !== "completed" && dep.status !== "cancelled";
11737
+ lines.push(` ${blocked ? "\u2717" : "\u2713"} ${dep.short_id || dep.id.slice(0, 8)} [${dep.status}] ${dep.title}`);
11738
+ }
11739
+ const unfinished = task.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
11740
+ if (unfinished.length > 0)
11741
+ lines.push(`\u26A0 BLOCKED by ${unfinished.length} unfinished dep(s)`);
11742
+ }
11743
+ if (task.blocked_by.length > 0) {
11744
+ lines.push(`
11745
+ Blocks (${task.blocked_by.length}):`);
11746
+ for (const b of task.blocked_by)
11747
+ lines.push(` ${b.short_id || b.id.slice(0, 8)} [${b.status}] ${b.title}`);
11748
+ }
11749
+ if (task.subtasks.length > 0) {
11750
+ lines.push(`
11751
+ Subtasks (${task.subtasks.length}):`);
11752
+ for (const st of task.subtasks)
11753
+ lines.push(` ${st.short_id || st.id.slice(0, 8)} [${st.status}] ${st.title}`);
11754
+ }
11755
+ try {
11756
+ const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
11757
+ const files = listTaskFiles2(task.id);
11758
+ if (files.length > 0) {
11759
+ lines.push(`
11760
+ Files (${files.length}):`);
11761
+ for (const f of files)
11762
+ lines.push(` ${f.role || "file"}: ${f.path}`);
11763
+ }
11764
+ } catch {}
11765
+ try {
11766
+ const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
11767
+ const commits = getTaskCommits2(task.id);
11768
+ if (commits.length > 0) {
11769
+ lines.push(`
11770
+ Commits (${commits.length}):`);
11771
+ for (const c of commits)
11772
+ lines.push(` ${c.commit_hash?.slice(0, 7)} ${c.message || ""}`);
11773
+ }
11774
+ } catch {}
11775
+ if (task.comments.length > 0) {
11776
+ lines.push(`
11777
+ Comments (${task.comments.length}):`);
11778
+ for (const c of task.comments)
11779
+ lines.push(` [${c.agent_id || "?"}] ${c.created_at}: ${c.content}`);
11780
+ }
11781
+ return { content: [{ type: "text", text: lines.join(`
11782
+ `) }] };
11783
+ } catch (e) {
11784
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11785
+ }
11786
+ });
11787
+ }
11031
11788
  if (shouldRegisterTool("get_context")) {
11032
11789
  server.tool("get_context", "Get a compact task summary for agent prompt injection. Returns formatted text.", {
11033
11790
  agent_id: exports_external.string().optional(),
@@ -11290,6 +12047,7 @@ if (shouldRegisterTool("search_tools")) {
11290
12047
  "delete_agent",
11291
12048
  "unarchive_agent",
11292
12049
  "heartbeat",
12050
+ "release_agent",
11293
12051
  "get_my_tasks",
11294
12052
  "get_org_chart",
11295
12053
  "set_reports_to",
@@ -11306,6 +12064,12 @@ if (shouldRegisterTool("search_tools")) {
11306
12064
  "claim_next_task",
11307
12065
  "get_task_history",
11308
12066
  "get_recent_activity",
12067
+ "recap",
12068
+ "task_context",
12069
+ "standup",
12070
+ "burndown",
12071
+ "blame",
12072
+ "import_github_issue",
11309
12073
  "create_webhook",
11310
12074
  "list_webhooks",
11311
12075
  "delete_webhook",
@@ -11430,8 +12194,8 @@ if (shouldRegisterTool("describe_tools")) {
11430
12194
  suggest_agent_name: `Check available agent names before registering. Shows active agents and, if a pool is configured, which pool names are free.
11431
12195
  Params: working_dir(string \u2014 your working directory, used to look up project pool from config)
11432
12196
  Example: {working_dir: '/workspace/platform'}`,
11433
- register_agent: `Register an agent. Any name is allowed \u2014 pool is advisory. Returns CONFLICT if name is held by a recently-active agent.
11434
- Params: name(string, req), description(string), capabilities(string[]), session_id(string \u2014 unique per session), working_dir(string \u2014 used to determine project pool)
12197
+ register_agent: `Register an agent. Any name is allowed \u2014 pool is advisory. Returns CONFLICT if name is held by a recently-active agent. Use force:true to take over.
12198
+ Params: name(string, req), description(string), capabilities(string[]), session_id(string \u2014 unique per session), working_dir(string \u2014 used to determine project pool), force(boolean \u2014 skip conflict check)
11435
12199
  Example: {name: 'my-agent', session_id: 'abc123-1741952000', working_dir: '/workspace/platform'}`,
11436
12200
  list_agents: `List all registered agents (active by default). Set include_archived: true to see archived agents.
11437
12201
  Params: include_archived(boolean, optional)
@@ -11451,6 +12215,9 @@ if (shouldRegisterTool("describe_tools")) {
11451
12215
  heartbeat: `Update last_seen_at timestamp to signal you're still active. Call periodically during long tasks.
11452
12216
  Params: agent_id(string, req \u2014 your agent ID or name)
11453
12217
  Example: {agent_id: 'maximus'}`,
12218
+ release_agent: `Explicitly release/logout an agent \u2014 clears session binding and makes name immediately available. Call when session ends.
12219
+ Params: agent_id(string, req), session_id(string \u2014 only releases if matching)
12220
+ Example: {agent_id: 'maximus', session_id: 'my-session-123'}`,
11454
12221
  get_my_tasks: `Get all tasks assigned to/created by an agent, with stats (pending/active/done/rate).
11455
12222
  Params: agent_name(string, req)
11456
12223
  Example: {agent_name: 'maximus'}`,
@@ -11511,6 +12278,24 @@ if (shouldRegisterTool("describe_tools")) {
11511
12278
  get_recent_activity: `Get recent task changes across all tasks \u2014 global activity feed.
11512
12279
  Params: limit(number, default:50)
11513
12280
  Example: {limit: 20}`,
12281
+ recap: `Summary of what happened in the last N hours \u2014 completed tasks with durations, new tasks, in-progress, blocked, stale, agent activity.
12282
+ Params: hours(number, default:8), project_id(string)
12283
+ Example: {hours: 4}`,
12284
+ task_context: `Full orientation for a specific task \u2014 description, dependencies with blocked status, files, commits, comments, checklist, duration. Use before starting work.
12285
+ Params: id(string, req)
12286
+ Example: {id: 'OPE-00042'}`,
12287
+ standup: `Generate standup notes \u2014 completed tasks grouped by agent, in-progress, blocked. Copy-paste ready.
12288
+ Params: hours(number, default:24), project_id(string)
12289
+ Example: {hours: 24}`,
12290
+ import_github_issue: `Import a GitHub issue as a task. Requires gh CLI.
12291
+ Params: url(string, req), project_id(string), task_list_id(string)
12292
+ Example: {url: 'https://github.com/owner/repo/issues/42'}`,
12293
+ blame: `Show which tasks/agents touched a file \u2014 combines task_files and task_commits.
12294
+ Params: path(string, req)
12295
+ Example: {path: 'src/db/agents.ts'}`,
12296
+ burndown: `ASCII burndown chart \u2014 actual vs ideal progress for a plan, project, or task list.
12297
+ Params: plan_id(string), project_id(string), task_list_id(string)
12298
+ Example: {plan_id: 'abc123'}`,
11514
12299
  create_webhook: `Register a webhook for task change events.
11515
12300
  Params: url(string, req), events(string[] \u2014 empty=all), secret(string \u2014 HMAC signing)
11516
12301
  Example: {url: 'https://example.com/hook', events: ['task.created', 'task.completed']}`,
@@ -12292,6 +13077,48 @@ ${lines.join(`
12292
13077
  }
12293
13078
  });
12294
13079
  }
13080
+ if (shouldRegisterTool("extract_todos")) {
13081
+ server.tool("extract_todos", "Scan source files for TODO/FIXME/HACK/BUG/XXX/NOTE comments and create tasks from them. Deduplicates on re-runs.", {
13082
+ path: exports_external.string().describe("Directory or file path to scan"),
13083
+ project_id: exports_external.string().optional().describe("Project to assign tasks to"),
13084
+ task_list_id: exports_external.string().optional().describe("Task list to add tasks to"),
13085
+ patterns: exports_external.array(exports_external.enum(["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"])).optional().describe("Tags to search for (default: all)"),
13086
+ tags: exports_external.array(exports_external.string()).optional().describe("Extra tags to add to created tasks"),
13087
+ assigned_to: exports_external.string().optional().describe("Agent to assign tasks to"),
13088
+ agent_id: exports_external.string().optional().describe("Agent performing the extraction"),
13089
+ dry_run: exports_external.boolean().optional().describe("If true, return found comments without creating tasks"),
13090
+ extensions: exports_external.array(exports_external.string()).optional().describe("File extensions to scan (e.g. ['ts', 'py'])")
13091
+ }, async (params) => {
13092
+ try {
13093
+ const { extractTodos: extractTodos2 } = (init_extract(), __toCommonJS(exports_extract));
13094
+ const resolved = { ...params };
13095
+ if (resolved["project_id"])
13096
+ resolved["project_id"] = resolveId(resolved["project_id"], "projects");
13097
+ if (resolved["task_list_id"])
13098
+ resolved["task_list_id"] = resolveId(resolved["task_list_id"], "task_lists");
13099
+ const result = extractTodos2(resolved);
13100
+ if (params.dry_run) {
13101
+ const lines = result.comments.map((c) => `[${c.tag}] ${c.message} \u2014 ${c.file}:${c.line}`);
13102
+ return { content: [{ type: "text", text: `Found ${result.comments.length} comment(s):
13103
+ ${lines.join(`
13104
+ `)}` }] };
13105
+ }
13106
+ const summary = [
13107
+ `Created ${result.tasks.length} task(s)`,
13108
+ result.skipped > 0 ? `Skipped ${result.skipped} duplicate(s)` : null,
13109
+ `Total comments found: ${result.comments.length}`
13110
+ ].filter(Boolean).join(`
13111
+ `);
13112
+ const taskLines = result.tasks.map((t) => formatTask(t));
13113
+ return { content: [{ type: "text", text: `${summary}
13114
+
13115
+ ${taskLines.join(`
13116
+ `)}` }] };
13117
+ } catch (e) {
13118
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13119
+ }
13120
+ });
13121
+ }
12295
13122
  server.resource("task-lists", "todos://task-lists", { description: "All task lists", mimeType: "application/json" }, async () => {
12296
13123
  const lists = listTaskLists();
12297
13124
  return { contents: [{ uri: "todos://task-lists", text: JSON.stringify(lists, null, 2), mimeType: "application/json" }] };