@hasna/todos 0.10.19 → 0.10.20

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
@@ -2346,6 +2346,7 @@ function ensureSchema(db) {
2346
2346
  ensureColumn("tasks", "spawned_from_session", "TEXT");
2347
2347
  ensureColumn("tasks", "assigned_by", "TEXT");
2348
2348
  ensureColumn("tasks", "assigned_from_project", "TEXT");
2349
+ ensureColumn("tasks", "started_at", "TEXT");
2349
2350
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
2350
2351
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
2351
2352
  ensureColumn("agents", "reports_to", "TEXT");
@@ -2463,6 +2464,16 @@ function resolvePartialId(db, table, partialId) {
2463
2464
  return shortIdRows[0].id;
2464
2465
  }
2465
2466
  }
2467
+ if (table === "task_lists") {
2468
+ const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
2469
+ if (slugRow)
2470
+ return slugRow.id;
2471
+ }
2472
+ if (table === "projects") {
2473
+ const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
2474
+ if (nameRow)
2475
+ return nameRow.id;
2476
+ }
2466
2477
  return null;
2467
2478
  }
2468
2479
  var LOCK_EXPIRY_MINUTES = 30, MIGRATIONS, _db = null;
@@ -2941,6 +2952,10 @@ var init_database = __esm(() => {
2941
2952
  CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
2942
2953
  CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
2943
2954
  INSERT OR IGNORE INTO _migrations (id) VALUES (33);
2955
+ `,
2956
+ `
2957
+ ALTER TABLE tasks ADD COLUMN started_at TEXT;
2958
+ INSERT OR IGNORE INTO _migrations (id) VALUES (34);
2944
2959
  `
2945
2960
  ];
2946
2961
  });
@@ -3413,7 +3428,8 @@ var exports_audit = {};
3413
3428
  __export(exports_audit, {
3414
3429
  logTaskChange: () => logTaskChange,
3415
3430
  getTaskHistory: () => getTaskHistory,
3416
- getRecentActivity: () => getRecentActivity
3431
+ getRecentActivity: () => getRecentActivity,
3432
+ getRecap: () => getRecap
3417
3433
  });
3418
3434
  function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
3419
3435
  const d = db || getDatabase();
@@ -3431,6 +3447,32 @@ function getRecentActivity(limit = 50, db) {
3431
3447
  const d = db || getDatabase();
3432
3448
  return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
3433
3449
  }
3450
+ function getRecap(hours = 8, projectId, db) {
3451
+ const d = db || getDatabase();
3452
+ const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
3453
+ const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
3454
+ const pf = projectId ? " AND project_id = ?" : "";
3455
+ const tpf = projectId ? " AND t.project_id = ?" : "";
3456
+ 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);
3457
+ 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);
3458
+ 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();
3459
+ 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();
3460
+ 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);
3461
+ 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);
3462
+ return {
3463
+ hours,
3464
+ since,
3465
+ completed: completed.map((r) => ({
3466
+ ...r,
3467
+ duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
3468
+ })),
3469
+ created,
3470
+ in_progress,
3471
+ blocked,
3472
+ stale,
3473
+ agents
3474
+ };
3475
+ }
3434
3476
  var init_audit = __esm(() => {
3435
3477
  init_database();
3436
3478
  });
@@ -4141,8 +4183,8 @@ function startTask(id, agentId, db) {
4141
4183
  }
4142
4184
  const cutoff = lockExpiryCutoff();
4143
4185
  const timestamp = now();
4144
- const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
4145
- WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, id, agentId, cutoff]);
4186
+ 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 = ?
4187
+ WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
4146
4188
  if (result.changes === 0) {
4147
4189
  if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
4148
4190
  throw new LockError(id, task.locked_by);
@@ -4150,7 +4192,7 @@ function startTask(id, agentId, db) {
4150
4192
  }
4151
4193
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
4152
4194
  dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
4153
- return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
4195
+ 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 };
4154
4196
  }
4155
4197
  function completeTask(id, agentId, db, options) {
4156
4198
  const d = db || getDatabase();
@@ -4801,6 +4843,7 @@ __export(exports_agents, {
4801
4843
  updateAgentActivity: () => updateAgentActivity,
4802
4844
  updateAgent: () => updateAgent,
4803
4845
  unarchiveAgent: () => unarchiveAgent,
4846
+ releaseAgent: () => releaseAgent,
4804
4847
  registerAgent: () => registerAgent,
4805
4848
  matchCapabilities: () => matchCapabilities,
4806
4849
  listAgents: () => listAgents,
@@ -4812,10 +4855,29 @@ __export(exports_agents, {
4812
4855
  getAgentByName: () => getAgentByName,
4813
4856
  getAgent: () => getAgent,
4814
4857
  deleteAgent: () => deleteAgent,
4858
+ autoReleaseStaleAgents: () => autoReleaseStaleAgents,
4815
4859
  archiveAgent: () => archiveAgent
4816
4860
  });
4861
+ function getActiveWindowMs() {
4862
+ const env = process.env["TODOS_AGENT_TIMEOUT_MS"];
4863
+ if (env) {
4864
+ const parsed = parseInt(env, 10);
4865
+ if (!isNaN(parsed) && parsed > 0)
4866
+ return parsed;
4867
+ }
4868
+ return 30 * 60 * 1000;
4869
+ }
4870
+ function autoReleaseStaleAgents(db) {
4871
+ if (process.env["TODOS_AGENT_AUTO_RELEASE"] !== "true")
4872
+ return 0;
4873
+ const d = db || getDatabase();
4874
+ const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
4875
+ const result = d.run("UPDATE agents SET session_id = NULL WHERE status = 'active' AND session_id IS NOT NULL AND last_seen_at < ?", [cutoff]);
4876
+ return result.changes;
4877
+ }
4817
4878
  function getAvailableNamesFromPool(pool, db) {
4818
- const cutoff = new Date(Date.now() - AGENT_ACTIVE_WINDOW_MS).toISOString();
4879
+ autoReleaseStaleAgents(db);
4880
+ const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
4819
4881
  const activeNames = new Set(db.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
4820
4882
  return pool.filter((name) => !activeNames.has(name.toLowerCase()));
4821
4883
  }
@@ -4837,22 +4899,19 @@ function registerAgent(input, db) {
4837
4899
  const existing = getAgentByName(normalizedName, d);
4838
4900
  if (existing) {
4839
4901
  const lastSeenMs = new Date(existing.last_seen_at).getTime();
4840
- const isActive = Date.now() - lastSeenMs < AGENT_ACTIVE_WINDOW_MS;
4902
+ const activeWindowMs = getActiveWindowMs();
4903
+ const isActive = Date.now() - lastSeenMs < activeWindowMs;
4841
4904
  const sameSession = input.session_id && existing.session_id && input.session_id === existing.session_id;
4842
4905
  const differentSession = input.session_id && existing.session_id && input.session_id !== existing.session_id;
4843
- if (isActive && differentSession) {
4844
- const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
4845
- const suggestions = input.pool ? getAvailableNamesFromPool(input.pool, d) : [];
4846
- return {
4847
- conflict: true,
4848
- existing_id: existing.id,
4849
- existing_name: existing.name,
4850
- last_seen_at: existing.last_seen_at,
4851
- session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
4852
- working_dir: existing.working_dir,
4853
- suggestions: suggestions.slice(0, 5),
4854
- 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(", ")}` : ""}`
4855
- };
4906
+ const callerHasNoSession = !input.session_id;
4907
+ const existingHasActiveSession = existing.session_id && isActive;
4908
+ if (!input.force) {
4909
+ if (isActive && differentSession) {
4910
+ return buildConflictError(existing, lastSeenMs, input.pool, d);
4911
+ }
4912
+ if (callerHasNoSession && existingHasActiveSession) {
4913
+ return buildConflictError(existing, lastSeenMs, input.pool, d);
4914
+ }
4856
4915
  }
4857
4916
  const updates = ["last_seen_at = ?", "status = 'active'"];
4858
4917
  const params = [now()];
@@ -4897,6 +4956,32 @@ function registerAgent(input, db) {
4897
4956
  function isAgentConflict(result) {
4898
4957
  return result.conflict === true;
4899
4958
  }
4959
+ function buildConflictError(existing, lastSeenMs, pool, d) {
4960
+ const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
4961
+ const suggestions = pool ? getAvailableNamesFromPool(pool, d) : [];
4962
+ return {
4963
+ conflict: true,
4964
+ existing_id: existing.id,
4965
+ existing_name: existing.name,
4966
+ last_seen_at: existing.last_seen_at,
4967
+ session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
4968
+ working_dir: existing.working_dir,
4969
+ suggestions: suggestions.slice(0, 5),
4970
+ 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(", ")}` : ""}`
4971
+ };
4972
+ }
4973
+ function releaseAgent(id, session_id, db) {
4974
+ const d = db || getDatabase();
4975
+ const agent = getAgent(id, d);
4976
+ if (!agent)
4977
+ return false;
4978
+ if (session_id && agent.session_id && agent.session_id !== session_id) {
4979
+ return false;
4980
+ }
4981
+ const epoch = new Date(0).toISOString();
4982
+ d.run("UPDATE agents SET session_id = NULL, last_seen_at = ? WHERE id = ?", [epoch, id]);
4983
+ return true;
4984
+ }
4900
4985
  function getAgent(id, db) {
4901
4986
  const d = db || getDatabase();
4902
4987
  const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
@@ -4917,6 +5002,7 @@ function listAgents(opts, db) {
4917
5002
  includeArchived = opts?.include_archived ?? false;
4918
5003
  d = db || getDatabase();
4919
5004
  }
5005
+ autoReleaseStaleAgents(d);
4920
5006
  if (includeArchived) {
4921
5007
  return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
4922
5008
  }
@@ -4934,8 +5020,19 @@ function updateAgent(id, input, db) {
4934
5020
  const sets = ["last_seen_at = ?"];
4935
5021
  const params = [now()];
4936
5022
  if (input.name !== undefined) {
5023
+ const newName = input.name.trim().toLowerCase();
5024
+ const holder = getAgentByName(newName, d);
5025
+ if (holder && holder.id !== id) {
5026
+ const lastSeenMs = new Date(holder.last_seen_at).getTime();
5027
+ const isActive = Date.now() - lastSeenMs < getActiveWindowMs();
5028
+ if (isActive && holder.status === "active") {
5029
+ throw new Error(`Cannot rename: name "${newName}" is held by active agent ${holder.id} (last seen ${Math.round((Date.now() - lastSeenMs) / 60000)}m ago)`);
5030
+ }
5031
+ const evictedName = `${holder.name}__evicted_${holder.id}`;
5032
+ d.run("UPDATE agents SET name = ? WHERE id = ?", [evictedName, holder.id]);
5033
+ }
4937
5034
  sets.push("name = ?");
4938
- params.push(input.name.trim().toLowerCase());
5035
+ params.push(newName);
4939
5036
  }
4940
5037
  if (input.description !== undefined) {
4941
5038
  sets.push("description = ?");
@@ -5032,10 +5129,8 @@ function getCapableAgents(capabilities, opts, db) {
5032
5129
  })).filter((entry) => entry.score >= minScore).sort((a, b) => b.score - a.score);
5033
5130
  return opts?.limit ? scored.slice(0, opts.limit) : scored;
5034
5131
  }
5035
- var AGENT_ACTIVE_WINDOW_MS;
5036
5132
  var init_agents = __esm(() => {
5037
5133
  init_database();
5038
- AGENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
5039
5134
  });
5040
5135
 
5041
5136
  // src/db/task-lists.ts
@@ -5867,139 +5962,555 @@ var init_sync = __esm(() => {
5867
5962
  init_config();
5868
5963
  });
5869
5964
 
5870
- // node_modules/zod/v3/helpers/util.js
5871
- var util, objectUtil, ZodParsedType, getParsedType = (data) => {
5872
- const t = typeof data;
5873
- switch (t) {
5874
- case "undefined":
5875
- return ZodParsedType.undefined;
5876
- case "string":
5877
- return ZodParsedType.string;
5878
- case "number":
5879
- return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number;
5880
- case "boolean":
5881
- return ZodParsedType.boolean;
5882
- case "function":
5883
- return ZodParsedType.function;
5884
- case "bigint":
5885
- return ZodParsedType.bigint;
5886
- case "symbol":
5887
- return ZodParsedType.symbol;
5888
- case "object":
5889
- if (Array.isArray(data)) {
5890
- return ZodParsedType.array;
5891
- }
5892
- if (data === null) {
5893
- return ZodParsedType.null;
5894
- }
5895
- if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") {
5896
- return ZodParsedType.promise;
5897
- }
5898
- if (typeof Map !== "undefined" && data instanceof Map) {
5899
- return ZodParsedType.map;
5900
- }
5901
- if (typeof Set !== "undefined" && data instanceof Set) {
5902
- return ZodParsedType.set;
5903
- }
5904
- if (typeof Date !== "undefined" && data instanceof Date) {
5905
- return ZodParsedType.date;
5906
- }
5907
- return ZodParsedType.object;
5908
- default:
5909
- return ZodParsedType.unknown;
5965
+ // src/db/task-files.ts
5966
+ var exports_task_files = {};
5967
+ __export(exports_task_files, {
5968
+ updateTaskFileStatus: () => updateTaskFileStatus,
5969
+ removeTaskFile: () => removeTaskFile,
5970
+ listTaskFiles: () => listTaskFiles,
5971
+ listActiveFiles: () => listActiveFiles,
5972
+ getTaskFile: () => getTaskFile,
5973
+ getFileHeatMap: () => getFileHeatMap,
5974
+ findTasksByFile: () => findTasksByFile,
5975
+ detectFileConflicts: () => detectFileConflicts,
5976
+ bulkFindTasksByFiles: () => bulkFindTasksByFiles,
5977
+ bulkAddTaskFiles: () => bulkAddTaskFiles,
5978
+ addTaskFile: () => addTaskFile
5979
+ });
5980
+ function addTaskFile(input, db) {
5981
+ const d = db || getDatabase();
5982
+ const id = uuid();
5983
+ const timestamp = now();
5984
+ const existing = d.query("SELECT id FROM task_files WHERE task_id = ? AND path = ?").get(input.task_id, input.path);
5985
+ if (existing) {
5986
+ d.run("UPDATE task_files SET status = ?, agent_id = ?, note = ?, updated_at = ? WHERE id = ?", [input.status || "active", input.agent_id || null, input.note || null, timestamp, existing.id]);
5987
+ return getTaskFile(existing.id, d);
5910
5988
  }
5911
- };
5912
- var init_util = __esm(() => {
5913
- (function(util2) {
5914
- util2.assertEqual = (_) => {};
5915
- function assertIs(_arg) {}
5916
- util2.assertIs = assertIs;
5917
- function assertNever(_x) {
5918
- throw new Error;
5919
- }
5920
- util2.assertNever = assertNever;
5921
- util2.arrayToEnum = (items) => {
5922
- const obj = {};
5923
- for (const item of items) {
5924
- obj[item] = item;
5925
- }
5926
- return obj;
5927
- };
5928
- util2.getValidEnumValues = (obj) => {
5929
- const validKeys = util2.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number");
5930
- const filtered = {};
5931
- for (const k of validKeys) {
5932
- filtered[k] = obj[k];
5933
- }
5934
- return util2.objectValues(filtered);
5935
- };
5936
- util2.objectValues = (obj) => {
5937
- return util2.objectKeys(obj).map(function(e) {
5938
- return obj[e];
5939
- });
5940
- };
5941
- util2.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object) => {
5942
- const keys = [];
5943
- for (const key in object) {
5944
- if (Object.prototype.hasOwnProperty.call(object, key)) {
5945
- keys.push(key);
5946
- }
5947
- }
5948
- return keys;
5949
- };
5950
- util2.find = (arr, checker) => {
5951
- for (const item of arr) {
5952
- if (checker(item))
5953
- return item;
5954
- }
5955
- return;
5956
- };
5957
- util2.isInteger = typeof Number.isInteger === "function" ? (val) => Number.isInteger(val) : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val;
5958
- function joinValues(array, separator = " | ") {
5959
- return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator);
5960
- }
5961
- util2.joinValues = joinValues;
5962
- util2.jsonStringifyReplacer = (_, value) => {
5963
- if (typeof value === "bigint") {
5964
- return value.toString();
5965
- }
5966
- return value;
5967
- };
5968
- })(util || (util = {}));
5969
- (function(objectUtil2) {
5970
- objectUtil2.mergeShapes = (first, second) => {
5971
- return {
5972
- ...first,
5973
- ...second
5974
- };
5989
+ d.run(`INSERT INTO task_files (id, task_id, path, status, agent_id, note, created_at, updated_at)
5990
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.path, input.status || "active", input.agent_id || null, input.note || null, timestamp, timestamp]);
5991
+ return getTaskFile(id, d);
5992
+ }
5993
+ function getTaskFile(id, db) {
5994
+ const d = db || getDatabase();
5995
+ return d.query("SELECT * FROM task_files WHERE id = ?").get(id);
5996
+ }
5997
+ function listTaskFiles(taskId, db) {
5998
+ const d = db || getDatabase();
5999
+ return d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY path").all(taskId);
6000
+ }
6001
+ function findTasksByFile(path, db) {
6002
+ const d = db || getDatabase();
6003
+ return d.query("SELECT * FROM task_files WHERE path = ? AND status != 'removed' ORDER BY updated_at DESC").all(path);
6004
+ }
6005
+ function updateTaskFileStatus(taskId, path, status, agentId, db) {
6006
+ const d = db || getDatabase();
6007
+ const timestamp = now();
6008
+ d.run("UPDATE task_files SET status = ?, agent_id = COALESCE(?, agent_id), updated_at = ? WHERE task_id = ? AND path = ?", [status, agentId || null, timestamp, taskId, path]);
6009
+ const row = d.query("SELECT * FROM task_files WHERE task_id = ? AND path = ?").get(taskId, path);
6010
+ return row;
6011
+ }
6012
+ function removeTaskFile(taskId, path, db) {
6013
+ const d = db || getDatabase();
6014
+ const result = d.run("DELETE FROM task_files WHERE task_id = ? AND path = ?", [taskId, path]);
6015
+ return result.changes > 0;
6016
+ }
6017
+ function detectFileConflicts(taskId, paths, db) {
6018
+ const d = db || getDatabase();
6019
+ if (paths.length === 0)
6020
+ return [];
6021
+ const placeholders = paths.map(() => "?").join(", ");
6022
+ const rows = d.query(`
6023
+ SELECT tf.path, tf.agent_id AS conflicting_agent_id, t.id AS conflicting_task_id,
6024
+ t.title AS conflicting_task_title, t.status AS conflicting_task_status
6025
+ FROM task_files tf
6026
+ JOIN tasks t ON tf.task_id = t.id
6027
+ WHERE tf.path IN (${placeholders})
6028
+ AND tf.task_id != ?
6029
+ AND tf.status != 'removed'
6030
+ AND t.status = 'in_progress'
6031
+ ORDER BY tf.updated_at DESC
6032
+ `).all(...paths, taskId);
6033
+ return rows;
6034
+ }
6035
+ function bulkFindTasksByFiles(paths, db) {
6036
+ const d = db || getDatabase();
6037
+ if (paths.length === 0)
6038
+ return [];
6039
+ const placeholders = paths.map(() => "?").join(", ");
6040
+ const rows = d.query(`SELECT tf.*, t.status AS task_status FROM task_files tf
6041
+ JOIN tasks t ON tf.task_id = t.id
6042
+ WHERE tf.path IN (${placeholders}) AND tf.status != 'removed'
6043
+ ORDER BY tf.updated_at DESC`).all(...paths);
6044
+ const byPath = new Map;
6045
+ for (const path of paths)
6046
+ byPath.set(path, []);
6047
+ for (const row of rows) {
6048
+ byPath.get(row.path)?.push(row);
6049
+ }
6050
+ return paths.map((path) => {
6051
+ const tasks = byPath.get(path) ?? [];
6052
+ const inProgressCount = tasks.filter((t) => t.task_status === "in_progress").length;
6053
+ return {
6054
+ path,
6055
+ tasks,
6056
+ has_conflict: inProgressCount > 1,
6057
+ in_progress_count: inProgressCount
5975
6058
  };
5976
- })(objectUtil || (objectUtil = {}));
5977
- ZodParsedType = util.arrayToEnum([
5978
- "string",
5979
- "nan",
5980
- "number",
5981
- "integer",
5982
- "float",
5983
- "boolean",
5984
- "date",
5985
- "bigint",
5986
- "symbol",
5987
- "function",
5988
- "undefined",
5989
- "null",
5990
- "array",
5991
- "object",
5992
- "unknown",
5993
- "promise",
5994
- "void",
5995
- "never",
5996
- "map",
5997
- "set"
5998
- ]);
5999
- });
6000
-
6001
- // node_modules/zod/v3/ZodError.js
6002
- var ZodIssueCode, quotelessJson = (obj) => {
6059
+ });
6060
+ }
6061
+ function listActiveFiles(db) {
6062
+ const d = db || getDatabase();
6063
+ return d.query(`
6064
+ SELECT
6065
+ tf.path,
6066
+ tf.status AS file_status,
6067
+ tf.agent_id AS file_agent_id,
6068
+ tf.note,
6069
+ tf.updated_at,
6070
+ t.id AS task_id,
6071
+ t.short_id AS task_short_id,
6072
+ t.title AS task_title,
6073
+ t.status AS task_status,
6074
+ t.locked_by AS task_locked_by,
6075
+ t.locked_at AS task_locked_at,
6076
+ a.id AS agent_id,
6077
+ a.name AS agent_name
6078
+ FROM task_files tf
6079
+ JOIN tasks t ON tf.task_id = t.id
6080
+ LEFT JOIN agents a ON (tf.agent_id = a.id OR (tf.agent_id IS NULL AND t.assigned_to = a.id))
6081
+ WHERE t.status = 'in_progress'
6082
+ AND tf.status != 'removed'
6083
+ ORDER BY tf.updated_at DESC
6084
+ `).all();
6085
+ }
6086
+ function getFileHeatMap(opts, db) {
6087
+ const d = db || getDatabase();
6088
+ const limit = opts?.limit ?? 20;
6089
+ const minEdits = opts?.min_edits ?? 1;
6090
+ const rows = d.query(`
6091
+ SELECT
6092
+ tf.path,
6093
+ COUNT(*) AS edit_count,
6094
+ COUNT(DISTINCT COALESCE(tf.agent_id, t.assigned_to)) AS unique_agents,
6095
+ GROUP_CONCAT(DISTINCT COALESCE(tf.agent_id, t.assigned_to)) AS agent_ids,
6096
+ MAX(tf.updated_at) AS last_edited_at,
6097
+ SUM(CASE WHEN t.status = 'in_progress' THEN 1 ELSE 0 END) AS active_task_count
6098
+ FROM task_files tf
6099
+ JOIN tasks t ON tf.task_id = t.id
6100
+ WHERE tf.status != 'removed'
6101
+ ${opts?.project_id ? `AND t.project_id = '${opts.project_id}'` : ""}
6102
+ GROUP BY tf.path
6103
+ HAVING edit_count >= ${minEdits}
6104
+ ORDER BY edit_count DESC, last_edited_at DESC
6105
+ LIMIT ${limit}
6106
+ `).all();
6107
+ return rows.map((r) => ({
6108
+ path: r.path,
6109
+ edit_count: r.edit_count,
6110
+ unique_agents: r.unique_agents,
6111
+ agent_ids: r.agent_ids ? r.agent_ids.split(",").filter(Boolean) : [],
6112
+ last_edited_at: r.last_edited_at,
6113
+ active_task_count: r.active_task_count
6114
+ }));
6115
+ }
6116
+ function bulkAddTaskFiles(taskId, paths, agentId, db) {
6117
+ const d = db || getDatabase();
6118
+ const results = [];
6119
+ const tx = d.transaction(() => {
6120
+ for (const path of paths) {
6121
+ results.push(addTaskFile({ task_id: taskId, path, agent_id: agentId }, d));
6122
+ }
6123
+ });
6124
+ tx();
6125
+ return results;
6126
+ }
6127
+ var init_task_files = __esm(() => {
6128
+ init_database();
6129
+ });
6130
+
6131
+ // src/db/task-commits.ts
6132
+ var exports_task_commits = {};
6133
+ __export(exports_task_commits, {
6134
+ unlinkTaskCommit: () => unlinkTaskCommit,
6135
+ linkTaskToCommit: () => linkTaskToCommit,
6136
+ getTaskCommits: () => getTaskCommits,
6137
+ findTaskByCommit: () => findTaskByCommit
6138
+ });
6139
+ function rowToCommit(row) {
6140
+ return {
6141
+ ...row,
6142
+ files_changed: row.files_changed ? JSON.parse(row.files_changed) : null
6143
+ };
6144
+ }
6145
+ function linkTaskToCommit(input, db) {
6146
+ const d = db || getDatabase();
6147
+ const existing = d.query("SELECT * FROM task_commits WHERE task_id = ? AND sha = ?").get(input.task_id, input.sha);
6148
+ if (existing) {
6149
+ d.run("UPDATE task_commits SET message = COALESCE(?, message), author = COALESCE(?, author), files_changed = COALESCE(?, files_changed), committed_at = COALESCE(?, committed_at) WHERE id = ?", [input.message ?? null, input.author ?? null, input.files_changed ? JSON.stringify(input.files_changed) : null, input.committed_at ?? null, existing.id]);
6150
+ return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(existing.id));
6151
+ }
6152
+ const id = uuid();
6153
+ d.run("INSERT INTO task_commits (id, task_id, sha, message, author, files_changed, committed_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, input.task_id, input.sha, input.message ?? null, input.author ?? null, input.files_changed ? JSON.stringify(input.files_changed) : null, input.committed_at ?? null, now()]);
6154
+ return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(id));
6155
+ }
6156
+ function getTaskCommits(taskId, db) {
6157
+ const d = db || getDatabase();
6158
+ return d.query("SELECT * FROM task_commits WHERE task_id = ? ORDER BY committed_at DESC, created_at DESC").all(taskId).map(rowToCommit);
6159
+ }
6160
+ function findTaskByCommit(sha, db) {
6161
+ const d = db || getDatabase();
6162
+ const row = d.query("SELECT * FROM task_commits WHERE sha = ? OR sha LIKE ? LIMIT 1").get(sha, `${sha}%`);
6163
+ if (!row)
6164
+ return null;
6165
+ return { task_id: row.task_id, commit: rowToCommit(row) };
6166
+ }
6167
+ function unlinkTaskCommit(taskId, sha, db) {
6168
+ const d = db || getDatabase();
6169
+ return d.run("DELETE FROM task_commits WHERE task_id = ? AND (sha = ? OR sha LIKE ?)", [taskId, sha, `${sha}%`]).changes > 0;
6170
+ }
6171
+ var init_task_commits = __esm(() => {
6172
+ init_database();
6173
+ });
6174
+
6175
+ // src/lib/extract.ts
6176
+ var exports_extract = {};
6177
+ __export(exports_extract, {
6178
+ tagToPriority: () => tagToPriority,
6179
+ extractTodos: () => extractTodos,
6180
+ extractFromSource: () => extractFromSource,
6181
+ EXTRACT_TAGS: () => EXTRACT_TAGS
6182
+ });
6183
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
6184
+ import { relative, resolve as resolve2, join as join6 } from "path";
6185
+ function tagToPriority(tag) {
6186
+ switch (tag) {
6187
+ case "BUG":
6188
+ case "FIXME":
6189
+ return "high";
6190
+ case "HACK":
6191
+ case "XXX":
6192
+ return "medium";
6193
+ case "TODO":
6194
+ return "medium";
6195
+ case "NOTE":
6196
+ return "low";
6197
+ }
6198
+ }
6199
+ function buildTagRegex(tags) {
6200
+ const tagPattern = tags.join("|");
6201
+ return new RegExp(`(?:^|\\s)(?:\\/\\/|\\/\\*|#|\\*|--|;;|%|<!--|\\{-)\\s*(?:@?)(${tagPattern})\\s*[:(]?\\s*(.*)$`, "i");
6202
+ }
6203
+ function extractFromSource(source, filePath, tags = [...EXTRACT_TAGS]) {
6204
+ const regex = buildTagRegex(tags);
6205
+ const results = [];
6206
+ const lines = source.split(`
6207
+ `);
6208
+ for (let i = 0;i < lines.length; i++) {
6209
+ const line = lines[i];
6210
+ const match = line.match(regex);
6211
+ if (match) {
6212
+ const tag = match[1].toUpperCase();
6213
+ let message = match[2].trim();
6214
+ message = message.replace(/\s*\*\/\s*$/, "").replace(/\s*-->\s*$/, "").replace(/\s*-\}\s*$/, "").trim();
6215
+ if (message) {
6216
+ results.push({
6217
+ tag,
6218
+ message,
6219
+ file: filePath,
6220
+ line: i + 1,
6221
+ raw: line
6222
+ });
6223
+ }
6224
+ }
6225
+ }
6226
+ return results;
6227
+ }
6228
+ function collectFiles(basePath, extensions) {
6229
+ const stat = statSync2(basePath);
6230
+ if (stat.isFile()) {
6231
+ return [basePath];
6232
+ }
6233
+ const glob = new Bun.Glob("**/*");
6234
+ const files = [];
6235
+ for (const entry of glob.scanSync({ cwd: basePath, onlyFiles: true, dot: false })) {
6236
+ const parts = entry.split("/");
6237
+ if (parts.some((p) => SKIP_DIRS.has(p)))
6238
+ continue;
6239
+ const dotIdx = entry.lastIndexOf(".");
6240
+ if (dotIdx === -1)
6241
+ continue;
6242
+ const ext = entry.slice(dotIdx);
6243
+ if (!extensions.has(ext))
6244
+ continue;
6245
+ files.push(entry);
6246
+ }
6247
+ return files.sort();
6248
+ }
6249
+ function extractTodos(options, db) {
6250
+ const basePath = resolve2(options.path);
6251
+ const tags = options.patterns || [...EXTRACT_TAGS];
6252
+ const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
6253
+ const files = collectFiles(basePath, extensions);
6254
+ const allComments = [];
6255
+ for (const file of files) {
6256
+ const fullPath = statSync2(basePath).isFile() ? basePath : join6(basePath, file);
6257
+ try {
6258
+ const source = readFileSync3(fullPath, "utf-8");
6259
+ const relPath = statSync2(basePath).isFile() ? relative(resolve2(basePath, ".."), fullPath) : file;
6260
+ const comments = extractFromSource(source, relPath, tags);
6261
+ allComments.push(...comments);
6262
+ } catch {}
6263
+ }
6264
+ if (options.dry_run) {
6265
+ return { comments: allComments, tasks: [], skipped: 0 };
6266
+ }
6267
+ const tasks = [];
6268
+ let skipped = 0;
6269
+ const existingTasks = options.project_id ? listTasks({ project_id: options.project_id, tags: ["extracted"] }, db) : listTasks({ tags: ["extracted"] }, db);
6270
+ const existingKeys = new Set;
6271
+ for (const t of existingTasks) {
6272
+ const meta = t.metadata;
6273
+ if (meta?.["source_file"] && meta?.["source_line"]) {
6274
+ existingKeys.add(`${meta["source_file"]}:${meta["source_line"]}`);
6275
+ }
6276
+ }
6277
+ for (const comment of allComments) {
6278
+ const dedupKey = `${comment.file}:${comment.line}`;
6279
+ if (existingKeys.has(dedupKey)) {
6280
+ skipped++;
6281
+ continue;
6282
+ }
6283
+ const taskTags = ["extracted", comment.tag.toLowerCase(), ...options.tags || []];
6284
+ const task = createTask({
6285
+ title: `[${comment.tag}] ${comment.message}`,
6286
+ description: `Extracted from code comment in \`${comment.file}\` at line ${comment.line}:
6287
+ \`\`\`
6288
+ ${comment.raw.trim()}
6289
+ \`\`\``,
6290
+ priority: tagToPriority(comment.tag),
6291
+ project_id: options.project_id,
6292
+ task_list_id: options.task_list_id,
6293
+ assigned_to: options.assigned_to,
6294
+ agent_id: options.agent_id,
6295
+ tags: taskTags,
6296
+ metadata: {
6297
+ source: "code_comment",
6298
+ comment_type: comment.tag,
6299
+ source_file: comment.file,
6300
+ source_line: comment.line
6301
+ }
6302
+ }, db);
6303
+ addTaskFile({
6304
+ task_id: task.id,
6305
+ path: comment.file,
6306
+ note: `Line ${comment.line}: ${comment.tag} comment`
6307
+ }, db);
6308
+ tasks.push(task);
6309
+ existingKeys.add(dedupKey);
6310
+ }
6311
+ return { comments: allComments, tasks, skipped };
6312
+ }
6313
+ var EXTRACT_TAGS, DEFAULT_EXTENSIONS, SKIP_DIRS;
6314
+ var init_extract = __esm(() => {
6315
+ init_tasks();
6316
+ init_task_files();
6317
+ EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
6318
+ DEFAULT_EXTENSIONS = new Set([
6319
+ ".ts",
6320
+ ".tsx",
6321
+ ".js",
6322
+ ".jsx",
6323
+ ".mjs",
6324
+ ".cjs",
6325
+ ".py",
6326
+ ".rb",
6327
+ ".go",
6328
+ ".rs",
6329
+ ".c",
6330
+ ".cpp",
6331
+ ".h",
6332
+ ".hpp",
6333
+ ".java",
6334
+ ".kt",
6335
+ ".swift",
6336
+ ".cs",
6337
+ ".php",
6338
+ ".sh",
6339
+ ".bash",
6340
+ ".zsh",
6341
+ ".lua",
6342
+ ".sql",
6343
+ ".r",
6344
+ ".R",
6345
+ ".yaml",
6346
+ ".yml",
6347
+ ".toml",
6348
+ ".css",
6349
+ ".scss",
6350
+ ".less",
6351
+ ".vue",
6352
+ ".svelte",
6353
+ ".ex",
6354
+ ".exs",
6355
+ ".erl",
6356
+ ".hs",
6357
+ ".ml",
6358
+ ".mli",
6359
+ ".clj",
6360
+ ".cljs"
6361
+ ]);
6362
+ SKIP_DIRS = new Set([
6363
+ "node_modules",
6364
+ ".git",
6365
+ "dist",
6366
+ "build",
6367
+ "out",
6368
+ ".next",
6369
+ ".turbo",
6370
+ "coverage",
6371
+ "__pycache__",
6372
+ ".venv",
6373
+ "venv",
6374
+ "vendor",
6375
+ "target",
6376
+ ".cache",
6377
+ ".parcel-cache"
6378
+ ]);
6379
+ });
6380
+
6381
+ // node_modules/zod/v3/helpers/util.js
6382
+ var util, objectUtil, ZodParsedType, getParsedType = (data) => {
6383
+ const t = typeof data;
6384
+ switch (t) {
6385
+ case "undefined":
6386
+ return ZodParsedType.undefined;
6387
+ case "string":
6388
+ return ZodParsedType.string;
6389
+ case "number":
6390
+ return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number;
6391
+ case "boolean":
6392
+ return ZodParsedType.boolean;
6393
+ case "function":
6394
+ return ZodParsedType.function;
6395
+ case "bigint":
6396
+ return ZodParsedType.bigint;
6397
+ case "symbol":
6398
+ return ZodParsedType.symbol;
6399
+ case "object":
6400
+ if (Array.isArray(data)) {
6401
+ return ZodParsedType.array;
6402
+ }
6403
+ if (data === null) {
6404
+ return ZodParsedType.null;
6405
+ }
6406
+ if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") {
6407
+ return ZodParsedType.promise;
6408
+ }
6409
+ if (typeof Map !== "undefined" && data instanceof Map) {
6410
+ return ZodParsedType.map;
6411
+ }
6412
+ if (typeof Set !== "undefined" && data instanceof Set) {
6413
+ return ZodParsedType.set;
6414
+ }
6415
+ if (typeof Date !== "undefined" && data instanceof Date) {
6416
+ return ZodParsedType.date;
6417
+ }
6418
+ return ZodParsedType.object;
6419
+ default:
6420
+ return ZodParsedType.unknown;
6421
+ }
6422
+ };
6423
+ var init_util = __esm(() => {
6424
+ (function(util2) {
6425
+ util2.assertEqual = (_) => {};
6426
+ function assertIs(_arg) {}
6427
+ util2.assertIs = assertIs;
6428
+ function assertNever(_x) {
6429
+ throw new Error;
6430
+ }
6431
+ util2.assertNever = assertNever;
6432
+ util2.arrayToEnum = (items) => {
6433
+ const obj = {};
6434
+ for (const item of items) {
6435
+ obj[item] = item;
6436
+ }
6437
+ return obj;
6438
+ };
6439
+ util2.getValidEnumValues = (obj) => {
6440
+ const validKeys = util2.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number");
6441
+ const filtered = {};
6442
+ for (const k of validKeys) {
6443
+ filtered[k] = obj[k];
6444
+ }
6445
+ return util2.objectValues(filtered);
6446
+ };
6447
+ util2.objectValues = (obj) => {
6448
+ return util2.objectKeys(obj).map(function(e) {
6449
+ return obj[e];
6450
+ });
6451
+ };
6452
+ util2.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object) => {
6453
+ const keys = [];
6454
+ for (const key in object) {
6455
+ if (Object.prototype.hasOwnProperty.call(object, key)) {
6456
+ keys.push(key);
6457
+ }
6458
+ }
6459
+ return keys;
6460
+ };
6461
+ util2.find = (arr, checker) => {
6462
+ for (const item of arr) {
6463
+ if (checker(item))
6464
+ return item;
6465
+ }
6466
+ return;
6467
+ };
6468
+ util2.isInteger = typeof Number.isInteger === "function" ? (val) => Number.isInteger(val) : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val;
6469
+ function joinValues(array, separator = " | ") {
6470
+ return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator);
6471
+ }
6472
+ util2.joinValues = joinValues;
6473
+ util2.jsonStringifyReplacer = (_, value) => {
6474
+ if (typeof value === "bigint") {
6475
+ return value.toString();
6476
+ }
6477
+ return value;
6478
+ };
6479
+ })(util || (util = {}));
6480
+ (function(objectUtil2) {
6481
+ objectUtil2.mergeShapes = (first, second) => {
6482
+ return {
6483
+ ...first,
6484
+ ...second
6485
+ };
6486
+ };
6487
+ })(objectUtil || (objectUtil = {}));
6488
+ ZodParsedType = util.arrayToEnum([
6489
+ "string",
6490
+ "nan",
6491
+ "number",
6492
+ "integer",
6493
+ "float",
6494
+ "boolean",
6495
+ "date",
6496
+ "bigint",
6497
+ "symbol",
6498
+ "function",
6499
+ "undefined",
6500
+ "null",
6501
+ "array",
6502
+ "object",
6503
+ "unknown",
6504
+ "promise",
6505
+ "void",
6506
+ "never",
6507
+ "map",
6508
+ "set"
6509
+ ]);
6510
+ });
6511
+
6512
+ // node_modules/zod/v3/ZodError.js
6513
+ var ZodIssueCode, quotelessJson = (obj) => {
6003
6514
  const json = JSON.stringify(obj, null, 2);
6004
6515
  return json.replace(/"([^"]+)":/g, "$1:");
6005
6516
  }, ZodError;
@@ -9833,47 +10344,148 @@ var init_zod = __esm(() => {
9833
10344
  init_external();
9834
10345
  });
9835
10346
 
9836
- // src/db/task-commits.ts
9837
- var exports_task_commits = {};
9838
- __export(exports_task_commits, {
9839
- unlinkTaskCommit: () => unlinkTaskCommit,
9840
- linkTaskToCommit: () => linkTaskToCommit,
9841
- getTaskCommits: () => getTaskCommits,
9842
- findTaskByCommit: () => findTaskByCommit
10347
+ // src/lib/github.ts
10348
+ var exports_github = {};
10349
+ __export(exports_github, {
10350
+ parseGitHubUrl: () => parseGitHubUrl,
10351
+ issueToTask: () => issueToTask,
10352
+ fetchGitHubIssue: () => fetchGitHubIssue
9843
10353
  });
9844
- function rowToCommit(row) {
10354
+ import { execSync } from "child_process";
10355
+ function parseGitHubUrl(url) {
10356
+ const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
10357
+ if (!match)
10358
+ return null;
10359
+ return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
10360
+ }
10361
+ function fetchGitHubIssue(owner, repo, number) {
10362
+ const json = execSync(`gh api repos/${owner}/${repo}/issues/${number}`, { encoding: "utf-8", timeout: 15000 });
10363
+ const data = JSON.parse(json);
9845
10364
  return {
9846
- ...row,
9847
- files_changed: row.files_changed ? JSON.parse(row.files_changed) : null
10365
+ number: data.number,
10366
+ title: data.title,
10367
+ body: data.body,
10368
+ labels: (data.labels || []).map((l) => l.name),
10369
+ state: data.state,
10370
+ assignee: data.assignee?.login || null,
10371
+ url: data.html_url
9848
10372
  };
9849
10373
  }
9850
- function linkTaskToCommit(input, db) {
9851
- const d = db || getDatabase();
9852
- const existing = d.query("SELECT * FROM task_commits WHERE task_id = ? AND sha = ?").get(input.task_id, input.sha);
9853
- if (existing) {
9854
- d.run("UPDATE task_commits SET message = COALESCE(?, message), author = COALESCE(?, author), files_changed = COALESCE(?, files_changed), committed_at = COALESCE(?, committed_at) WHERE id = ?", [input.message ?? null, input.author ?? null, input.files_changed ? JSON.stringify(input.files_changed) : null, input.committed_at ?? null, existing.id]);
9855
- return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(existing.id));
10374
+ function issueToTask(issue, opts) {
10375
+ const labelToPriority = {
10376
+ critical: "critical",
10377
+ "priority:critical": "critical",
10378
+ high: "high",
10379
+ "priority:high": "high",
10380
+ urgent: "high",
10381
+ low: "low",
10382
+ "priority:low": "low"
10383
+ };
10384
+ let priority = "medium";
10385
+ for (const label of issue.labels) {
10386
+ const mapped = labelToPriority[label.toLowerCase()];
10387
+ if (mapped) {
10388
+ priority = mapped;
10389
+ break;
10390
+ }
9856
10391
  }
9857
- const id = uuid();
9858
- d.run("INSERT INTO task_commits (id, task_id, sha, message, author, files_changed, committed_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, input.task_id, input.sha, input.message ?? null, input.author ?? null, input.files_changed ? JSON.stringify(input.files_changed) : null, input.committed_at ?? null, now()]);
9859
- return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(id));
9860
- }
9861
- function getTaskCommits(taskId, db) {
9862
- const d = db || getDatabase();
9863
- return d.query("SELECT * FROM task_commits WHERE task_id = ? ORDER BY committed_at DESC, created_at DESC").all(taskId).map(rowToCommit);
10392
+ return {
10393
+ title: `[GH#${issue.number}] ${issue.title}`,
10394
+ description: issue.body ? issue.body.slice(0, 4000) : undefined,
10395
+ tags: issue.labels.slice(0, 10),
10396
+ priority,
10397
+ metadata: { github_url: issue.url, github_number: issue.number, github_state: issue.state },
10398
+ project_id: opts?.project_id,
10399
+ task_list_id: opts?.task_list_id,
10400
+ agent_id: opts?.agent_id
10401
+ };
9864
10402
  }
9865
- function findTaskByCommit(sha, db) {
10403
+ var init_github = () => {};
10404
+
10405
+ // src/lib/burndown.ts
10406
+ var exports_burndown = {};
10407
+ __export(exports_burndown, {
10408
+ getBurndown: () => getBurndown
10409
+ });
10410
+ function getBurndown(opts, db) {
9866
10411
  const d = db || getDatabase();
9867
- const row = d.query("SELECT * FROM task_commits WHERE sha = ? OR sha LIKE ? LIMIT 1").get(sha, `${sha}%`);
9868
- if (!row)
9869
- return null;
9870
- return { task_id: row.task_id, commit: rowToCommit(row) };
10412
+ const conditions = [];
10413
+ const params = [];
10414
+ if (opts.plan_id) {
10415
+ conditions.push("plan_id = ?");
10416
+ params.push(opts.plan_id);
10417
+ }
10418
+ if (opts.project_id) {
10419
+ conditions.push("project_id = ?");
10420
+ params.push(opts.project_id);
10421
+ }
10422
+ if (opts.task_list_id) {
10423
+ conditions.push("task_list_id = ?");
10424
+ params.push(opts.task_list_id);
10425
+ }
10426
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
10427
+ const total = d.query(`SELECT COUNT(*) as c FROM tasks ${where}`).get(...params).c;
10428
+ const completed = d.query(`SELECT COUNT(*) as c FROM tasks ${where}${where ? " AND" : " WHERE"} status = 'completed'`).get(...params).c;
10429
+ 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);
10430
+ const firstTask = d.query(`SELECT MIN(created_at) as min_date FROM tasks ${where}`).get(...params);
10431
+ const startDate = firstTask?.min_date ? new Date(firstTask.min_date) : new Date;
10432
+ const endDate = new Date;
10433
+ const days = [];
10434
+ const totalDays = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)));
10435
+ let cumulative = 0;
10436
+ const completionMap = new Map(completions.map((c) => [c.date, c.count]));
10437
+ const current = new Date(startDate);
10438
+ for (let i = 0;i <= totalDays; i++) {
10439
+ const dateStr = current.toISOString().slice(0, 10);
10440
+ cumulative += completionMap.get(dateStr) || 0;
10441
+ days.push({
10442
+ date: dateStr,
10443
+ completed_cumulative: cumulative,
10444
+ ideal: Math.round(total / totalDays * i)
10445
+ });
10446
+ current.setDate(current.getDate() + 1);
10447
+ }
10448
+ const chart = renderBurndownChart(total, days);
10449
+ return { total, completed, remaining: total - completed, days, chart };
9871
10450
  }
9872
- function unlinkTaskCommit(taskId, sha, db) {
9873
- const d = db || getDatabase();
9874
- return d.run("DELETE FROM task_commits WHERE task_id = ? AND (sha = ? OR sha LIKE ?)", [taskId, sha, `${sha}%`]).changes > 0;
10451
+ function renderBurndownChart(total, days) {
10452
+ const height = 12;
10453
+ const width = Math.min(60, days.length);
10454
+ const step = Math.max(1, Math.floor(days.length / width));
10455
+ const sampled = days.filter((_, i) => i % step === 0 || i === days.length - 1).slice(0, width);
10456
+ const lines = [];
10457
+ lines.push(` ${total} \u2524`);
10458
+ for (let row = height - 1;row >= 0; row--) {
10459
+ const threshold = Math.round(total / height * row);
10460
+ let line = "";
10461
+ for (const day of sampled) {
10462
+ const remaining = total - day.completed_cumulative;
10463
+ const idealRemaining = total - day.ideal;
10464
+ if (remaining >= threshold && remaining > threshold - Math.round(total / height)) {
10465
+ line += "\u2588";
10466
+ } else if (idealRemaining >= threshold && idealRemaining > threshold - Math.round(total / height)) {
10467
+ line += "\xB7";
10468
+ } else {
10469
+ line += " ";
10470
+ }
10471
+ }
10472
+ const label = String(threshold).padStart(4);
10473
+ lines.push(`${label} \u2524${line}`);
10474
+ }
10475
+ lines.push(` 0 \u2524${"\u2500".repeat(sampled.length)}`);
10476
+ lines.push(` \u2514${"\u2500".repeat(sampled.length)}`);
10477
+ if (sampled.length > 0) {
10478
+ const first = sampled[0].date.slice(5);
10479
+ const last = sampled[sampled.length - 1].date.slice(5);
10480
+ const pad = sampled.length - first.length - last.length;
10481
+ lines.push(` ${first}${" ".repeat(Math.max(1, pad))}${last}`);
10482
+ }
10483
+ lines.push("");
10484
+ lines.push(` \u2588 actual remaining \xB7 ideal burndown`);
10485
+ return lines.join(`
10486
+ `);
9875
10487
  }
9876
- var init_task_commits = __esm(() => {
10488
+ var init_burndown = __esm(() => {
9877
10489
  init_database();
9878
10490
  });
9879
10491
 
@@ -9919,283 +10531,117 @@ function buildPrompt(task, agents) {
9919
10531
  return `You are a task routing assistant. Given a task and available agents, choose the SINGLE best agent.
9920
10532
 
9921
10533
  TASK:
9922
- Title: ${task.title}
9923
- Priority: ${task.priority}
9924
- Tags: ${task.tags.join(", ") || "none"}
9925
- Description: ${task.description?.slice(0, 300) || "none"}
9926
-
9927
- AVAILABLE AGENTS:
9928
- ${agentList}
9929
-
9930
- Rules:
9931
- - Match task tags/content to agent capabilities
9932
- - Prefer agents with fewer active tasks
9933
- - Prefer agents whose role fits the task (lead for critical, developer for features, qa for testing)
9934
- - If no clear match, pick the agent with fewest active tasks
9935
-
9936
- Respond with ONLY a JSON object: {"agent_name": "<name>", "reason": "<one sentence>"}`;
9937
- }
9938
- async function callCerebras(prompt, apiKey) {
9939
- try {
9940
- const resp = await fetch(CEREBRAS_API_URL, {
9941
- method: "POST",
9942
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
9943
- body: JSON.stringify({
9944
- model: CEREBRAS_MODEL,
9945
- messages: [{ role: "user", content: prompt }],
9946
- max_tokens: 150,
9947
- temperature: 0
9948
- }),
9949
- signal: AbortSignal.timeout(1e4)
9950
- });
9951
- if (!resp.ok)
9952
- return null;
9953
- const data = await resp.json();
9954
- const content = data?.choices?.[0]?.message?.content?.trim();
9955
- if (!content)
9956
- return null;
9957
- const match = content.match(/\{[^}]+\}/s);
9958
- if (!match)
9959
- return null;
9960
- return JSON.parse(match[0]);
9961
- } catch {
9962
- return null;
9963
- }
9964
- }
9965
- async function autoAssignTask(taskId, db) {
9966
- const d = db || getDatabase();
9967
- const task = getTask(taskId, d);
9968
- if (!task)
9969
- throw new Error(`Task ${taskId} not found`);
9970
- const agents = listAgents(d).filter((a) => a.status === "active");
9971
- if (agents.length === 0) {
9972
- return { task_id: taskId, assigned_to: null, agent_name: null, method: "no_agents" };
9973
- }
9974
- const workloads = getAgentWorkloads(d);
9975
- const apiKey = process.env["CEREBRAS_API_KEY"];
9976
- let selectedAgent = null;
9977
- let method = "capability_match";
9978
- let reason;
9979
- if (apiKey) {
9980
- const agentData = agents.map((a) => ({
9981
- id: a.id,
9982
- name: a.name,
9983
- role: a.role || "agent",
9984
- capabilities: a.capabilities || [],
9985
- in_progress_tasks: workloads.get(a.id) ?? 0
9986
- }));
9987
- const result = await callCerebras(buildPrompt({
9988
- title: task.title,
9989
- description: task.description,
9990
- priority: task.priority,
9991
- tags: task.tags || []
9992
- }, agentData), apiKey);
9993
- if (result?.agent_name) {
9994
- selectedAgent = agents.find((a) => a.name === result.agent_name) ?? null;
9995
- if (selectedAgent) {
9996
- method = "cerebras";
9997
- reason = result.reason;
9998
- }
9999
- }
10000
- }
10001
- if (!selectedAgent) {
10002
- const taskTags = task.tags || [];
10003
- const capable = getCapableAgents(taskTags, { min_score: 0, limit: 10 }, d);
10004
- if (capable.length > 0) {
10005
- const sorted = capable.sort((a, b) => {
10006
- if (b.score !== a.score)
10007
- return b.score - a.score;
10008
- return (workloads.get(a.agent.id) ?? 0) - (workloads.get(b.agent.id) ?? 0);
10009
- });
10010
- selectedAgent = sorted[0].agent;
10011
- reason = `Capability match (score: ${sorted[0].score.toFixed(2)})`;
10012
- } else {
10013
- selectedAgent = agents.slice().sort((a, b) => (workloads.get(a.id) ?? 0) - (workloads.get(b.id) ?? 0))[0];
10014
- reason = `Least busy agent (${workloads.get(selectedAgent.id) ?? 0} active tasks)`;
10015
- }
10016
- }
10017
- if (selectedAgent) {
10018
- updateTask(taskId, { assigned_to: selectedAgent.id, version: task.version }, d);
10019
- }
10020
- return {
10021
- task_id: taskId,
10022
- assigned_to: selectedAgent?.id ?? null,
10023
- agent_name: selectedAgent?.name ?? null,
10024
- method,
10025
- reason
10026
- };
10027
- }
10028
- var CEREBRAS_API_URL = "https://api.cerebras.ai/v1/chat/completions", CEREBRAS_MODEL = "llama-3.3-70b";
10029
- var init_auto_assign = __esm(() => {
10030
- init_database();
10031
- init_tasks();
10032
- init_agents();
10033
- });
10034
-
10035
- // src/db/task-files.ts
10036
- var exports_task_files = {};
10037
- __export(exports_task_files, {
10038
- updateTaskFileStatus: () => updateTaskFileStatus,
10039
- removeTaskFile: () => removeTaskFile,
10040
- listTaskFiles: () => listTaskFiles,
10041
- listActiveFiles: () => listActiveFiles,
10042
- getTaskFile: () => getTaskFile,
10043
- getFileHeatMap: () => getFileHeatMap,
10044
- findTasksByFile: () => findTasksByFile,
10045
- detectFileConflicts: () => detectFileConflicts,
10046
- bulkFindTasksByFiles: () => bulkFindTasksByFiles,
10047
- bulkAddTaskFiles: () => bulkAddTaskFiles,
10048
- addTaskFile: () => addTaskFile
10049
- });
10050
- function addTaskFile(input, db) {
10051
- const d = db || getDatabase();
10052
- const id = uuid();
10053
- const timestamp = now();
10054
- const existing = d.query("SELECT id FROM task_files WHERE task_id = ? AND path = ?").get(input.task_id, input.path);
10055
- if (existing) {
10056
- d.run("UPDATE task_files SET status = ?, agent_id = ?, note = ?, updated_at = ? WHERE id = ?", [input.status || "active", input.agent_id || null, input.note || null, timestamp, existing.id]);
10057
- return getTaskFile(existing.id, d);
10058
- }
10059
- d.run(`INSERT INTO task_files (id, task_id, path, status, agent_id, note, created_at, updated_at)
10060
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.path, input.status || "active", input.agent_id || null, input.note || null, timestamp, timestamp]);
10061
- return getTaskFile(id, d);
10062
- }
10063
- function getTaskFile(id, db) {
10064
- const d = db || getDatabase();
10065
- return d.query("SELECT * FROM task_files WHERE id = ?").get(id);
10066
- }
10067
- function listTaskFiles(taskId, db) {
10068
- const d = db || getDatabase();
10069
- return d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY path").all(taskId);
10070
- }
10071
- function findTasksByFile(path, db) {
10072
- const d = db || getDatabase();
10073
- return d.query("SELECT * FROM task_files WHERE path = ? AND status != 'removed' ORDER BY updated_at DESC").all(path);
10074
- }
10075
- function updateTaskFileStatus(taskId, path, status, agentId, db) {
10076
- const d = db || getDatabase();
10077
- const timestamp = now();
10078
- d.run("UPDATE task_files SET status = ?, agent_id = COALESCE(?, agent_id), updated_at = ? WHERE task_id = ? AND path = ?", [status, agentId || null, timestamp, taskId, path]);
10079
- const row = d.query("SELECT * FROM task_files WHERE task_id = ? AND path = ?").get(taskId, path);
10080
- return row;
10081
- }
10082
- function removeTaskFile(taskId, path, db) {
10083
- const d = db || getDatabase();
10084
- const result = d.run("DELETE FROM task_files WHERE task_id = ? AND path = ?", [taskId, path]);
10085
- return result.changes > 0;
10086
- }
10087
- function detectFileConflicts(taskId, paths, db) {
10088
- const d = db || getDatabase();
10089
- if (paths.length === 0)
10090
- return [];
10091
- const placeholders = paths.map(() => "?").join(", ");
10092
- const rows = d.query(`
10093
- SELECT tf.path, tf.agent_id AS conflicting_agent_id, t.id AS conflicting_task_id,
10094
- t.title AS conflicting_task_title, t.status AS conflicting_task_status
10095
- FROM task_files tf
10096
- JOIN tasks t ON tf.task_id = t.id
10097
- WHERE tf.path IN (${placeholders})
10098
- AND tf.task_id != ?
10099
- AND tf.status != 'removed'
10100
- AND t.status = 'in_progress'
10101
- ORDER BY tf.updated_at DESC
10102
- `).all(...paths, taskId);
10103
- return rows;
10104
- }
10105
- function bulkFindTasksByFiles(paths, db) {
10106
- const d = db || getDatabase();
10107
- if (paths.length === 0)
10108
- return [];
10109
- const placeholders = paths.map(() => "?").join(", ");
10110
- const rows = d.query(`SELECT tf.*, t.status AS task_status FROM task_files tf
10111
- JOIN tasks t ON tf.task_id = t.id
10112
- WHERE tf.path IN (${placeholders}) AND tf.status != 'removed'
10113
- ORDER BY tf.updated_at DESC`).all(...paths);
10114
- const byPath = new Map;
10115
- for (const path of paths)
10116
- byPath.set(path, []);
10117
- for (const row of rows) {
10118
- byPath.get(row.path)?.push(row);
10119
- }
10120
- return paths.map((path) => {
10121
- const tasks = byPath.get(path) ?? [];
10122
- const inProgressCount = tasks.filter((t) => t.task_status === "in_progress").length;
10123
- return {
10124
- path,
10125
- tasks,
10126
- has_conflict: inProgressCount > 1,
10127
- in_progress_count: inProgressCount
10128
- };
10129
- });
10130
- }
10131
- function listActiveFiles(db) {
10132
- const d = db || getDatabase();
10133
- return d.query(`
10134
- SELECT
10135
- tf.path,
10136
- tf.status AS file_status,
10137
- tf.agent_id AS file_agent_id,
10138
- tf.note,
10139
- tf.updated_at,
10140
- t.id AS task_id,
10141
- t.short_id AS task_short_id,
10142
- t.title AS task_title,
10143
- t.status AS task_status,
10144
- t.locked_by AS task_locked_by,
10145
- t.locked_at AS task_locked_at,
10146
- a.id AS agent_id,
10147
- a.name AS agent_name
10148
- FROM task_files tf
10149
- JOIN tasks t ON tf.task_id = t.id
10150
- LEFT JOIN agents a ON (tf.agent_id = a.id OR (tf.agent_id IS NULL AND t.assigned_to = a.id))
10151
- WHERE t.status = 'in_progress'
10152
- AND tf.status != 'removed'
10153
- ORDER BY tf.updated_at DESC
10154
- `).all();
10534
+ Title: ${task.title}
10535
+ Priority: ${task.priority}
10536
+ Tags: ${task.tags.join(", ") || "none"}
10537
+ Description: ${task.description?.slice(0, 300) || "none"}
10538
+
10539
+ AVAILABLE AGENTS:
10540
+ ${agentList}
10541
+
10542
+ Rules:
10543
+ - Match task tags/content to agent capabilities
10544
+ - Prefer agents with fewer active tasks
10545
+ - Prefer agents whose role fits the task (lead for critical, developer for features, qa for testing)
10546
+ - If no clear match, pick the agent with fewest active tasks
10547
+
10548
+ Respond with ONLY a JSON object: {"agent_name": "<name>", "reason": "<one sentence>"}`;
10155
10549
  }
10156
- function getFileHeatMap(opts, db) {
10157
- const d = db || getDatabase();
10158
- const limit = opts?.limit ?? 20;
10159
- const minEdits = opts?.min_edits ?? 1;
10160
- const rows = d.query(`
10161
- SELECT
10162
- tf.path,
10163
- COUNT(*) AS edit_count,
10164
- COUNT(DISTINCT COALESCE(tf.agent_id, t.assigned_to)) AS unique_agents,
10165
- GROUP_CONCAT(DISTINCT COALESCE(tf.agent_id, t.assigned_to)) AS agent_ids,
10166
- MAX(tf.updated_at) AS last_edited_at,
10167
- SUM(CASE WHEN t.status = 'in_progress' THEN 1 ELSE 0 END) AS active_task_count
10168
- FROM task_files tf
10169
- JOIN tasks t ON tf.task_id = t.id
10170
- WHERE tf.status != 'removed'
10171
- ${opts?.project_id ? `AND t.project_id = '${opts.project_id}'` : ""}
10172
- GROUP BY tf.path
10173
- HAVING edit_count >= ${minEdits}
10174
- ORDER BY edit_count DESC, last_edited_at DESC
10175
- LIMIT ${limit}
10176
- `).all();
10177
- return rows.map((r) => ({
10178
- path: r.path,
10179
- edit_count: r.edit_count,
10180
- unique_agents: r.unique_agents,
10181
- agent_ids: r.agent_ids ? r.agent_ids.split(",").filter(Boolean) : [],
10182
- last_edited_at: r.last_edited_at,
10183
- active_task_count: r.active_task_count
10184
- }));
10550
+ async function callCerebras(prompt, apiKey) {
10551
+ try {
10552
+ const resp = await fetch(CEREBRAS_API_URL, {
10553
+ method: "POST",
10554
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
10555
+ body: JSON.stringify({
10556
+ model: CEREBRAS_MODEL,
10557
+ messages: [{ role: "user", content: prompt }],
10558
+ max_tokens: 150,
10559
+ temperature: 0
10560
+ }),
10561
+ signal: AbortSignal.timeout(1e4)
10562
+ });
10563
+ if (!resp.ok)
10564
+ return null;
10565
+ const data = await resp.json();
10566
+ const content = data?.choices?.[0]?.message?.content?.trim();
10567
+ if (!content)
10568
+ return null;
10569
+ const match = content.match(/\{[^}]+\}/s);
10570
+ if (!match)
10571
+ return null;
10572
+ return JSON.parse(match[0]);
10573
+ } catch {
10574
+ return null;
10575
+ }
10185
10576
  }
10186
- function bulkAddTaskFiles(taskId, paths, agentId, db) {
10577
+ async function autoAssignTask(taskId, db) {
10187
10578
  const d = db || getDatabase();
10188
- const results = [];
10189
- const tx = d.transaction(() => {
10190
- for (const path of paths) {
10191
- results.push(addTaskFile({ task_id: taskId, path, agent_id: agentId }, d));
10579
+ const task = getTask(taskId, d);
10580
+ if (!task)
10581
+ throw new Error(`Task ${taskId} not found`);
10582
+ const agents = listAgents(d).filter((a) => a.status === "active");
10583
+ if (agents.length === 0) {
10584
+ return { task_id: taskId, assigned_to: null, agent_name: null, method: "no_agents" };
10585
+ }
10586
+ const workloads = getAgentWorkloads(d);
10587
+ const apiKey = process.env["CEREBRAS_API_KEY"];
10588
+ let selectedAgent = null;
10589
+ let method = "capability_match";
10590
+ let reason;
10591
+ if (apiKey) {
10592
+ const agentData = agents.map((a) => ({
10593
+ id: a.id,
10594
+ name: a.name,
10595
+ role: a.role || "agent",
10596
+ capabilities: a.capabilities || [],
10597
+ in_progress_tasks: workloads.get(a.id) ?? 0
10598
+ }));
10599
+ const result = await callCerebras(buildPrompt({
10600
+ title: task.title,
10601
+ description: task.description,
10602
+ priority: task.priority,
10603
+ tags: task.tags || []
10604
+ }, agentData), apiKey);
10605
+ if (result?.agent_name) {
10606
+ selectedAgent = agents.find((a) => a.name === result.agent_name) ?? null;
10607
+ if (selectedAgent) {
10608
+ method = "cerebras";
10609
+ reason = result.reason;
10610
+ }
10192
10611
  }
10193
- });
10194
- tx();
10195
- return results;
10612
+ }
10613
+ if (!selectedAgent) {
10614
+ const taskTags = task.tags || [];
10615
+ const capable = getCapableAgents(taskTags, { min_score: 0, limit: 10 }, d);
10616
+ if (capable.length > 0) {
10617
+ const sorted = capable.sort((a, b) => {
10618
+ if (b.score !== a.score)
10619
+ return b.score - a.score;
10620
+ return (workloads.get(a.agent.id) ?? 0) - (workloads.get(b.agent.id) ?? 0);
10621
+ });
10622
+ selectedAgent = sorted[0].agent;
10623
+ reason = `Capability match (score: ${sorted[0].score.toFixed(2)})`;
10624
+ } else {
10625
+ selectedAgent = agents.slice().sort((a, b) => (workloads.get(a.id) ?? 0) - (workloads.get(b.id) ?? 0))[0];
10626
+ reason = `Least busy agent (${workloads.get(selectedAgent.id) ?? 0} active tasks)`;
10627
+ }
10628
+ }
10629
+ if (selectedAgent) {
10630
+ updateTask(taskId, { assigned_to: selectedAgent.id, version: task.version }, d);
10631
+ }
10632
+ return {
10633
+ task_id: taskId,
10634
+ assigned_to: selectedAgent?.id ?? null,
10635
+ agent_name: selectedAgent?.name ?? null,
10636
+ method,
10637
+ reason
10638
+ };
10196
10639
  }
10197
- var init_task_files = __esm(() => {
10640
+ var CEREBRAS_API_URL = "https://api.cerebras.ai/v1/chat/completions", CEREBRAS_MODEL = "llama-3.3-70b";
10641
+ var init_auto_assign = __esm(() => {
10198
10642
  init_database();
10643
+ init_tasks();
10644
+ init_agents();
10199
10645
  });
10200
10646
 
10201
10647
  // src/db/file-locks.ts
@@ -10997,14 +11443,14 @@ __export(exports_mcp, {
10997
11443
  });
10998
11444
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10999
11445
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11000
- import { readFileSync as readFileSync3 } from "fs";
11001
- import { join as join6, dirname as dirname2 } from "path";
11446
+ import { readFileSync as readFileSync4 } from "fs";
11447
+ import { join as join7, dirname as dirname2 } from "path";
11002
11448
  import { fileURLToPath } from "url";
11003
11449
  function getMcpVersion() {
11004
11450
  try {
11005
11451
  const __dir = dirname2(fileURLToPath(import.meta.url));
11006
- const pkgPath = join6(__dir, "..", "package.json");
11007
- return JSON.parse(readFileSync3(pkgPath, "utf-8")).version || "0.0.0";
11452
+ const pkgPath = join7(__dir, "..", "package.json");
11453
+ return JSON.parse(readFileSync4(pkgPath, "utf-8")).version || "0.0.0";
11008
11454
  } catch {
11009
11455
  return "0.0.0";
11010
11456
  }
@@ -11168,7 +11614,8 @@ var init_mcp = __esm(() => {
11168
11614
  "get_next_task",
11169
11615
  "bootstrap",
11170
11616
  "get_tasks_changed_since",
11171
- "heartbeat"
11617
+ "heartbeat",
11618
+ "release_agent"
11172
11619
  ]);
11173
11620
  STANDARD_EXCLUDED = new Set([
11174
11621
  "rename_agent",
@@ -11390,7 +11837,12 @@ Checklist (${done}/${task.checklist.length}):`);
11390
11837
  }, async ({ id, ...rest }) => {
11391
11838
  try {
11392
11839
  const resolvedId = resolveId(id);
11393
- const task = updateTask(resolvedId, rest);
11840
+ const resolved = { ...rest };
11841
+ if (resolved.task_list_id)
11842
+ resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
11843
+ if (resolved.plan_id)
11844
+ resolved.plan_id = resolveId(resolved.plan_id, "plans");
11845
+ const task = updateTask(resolvedId, resolved);
11394
11846
  return { content: [{ type: "text", text: `updated: ${formatTask(task)}` }] };
11395
11847
  } catch (e) {
11396
11848
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -12030,11 +12482,12 @@ ${text}` }] };
12030
12482
  description: exports_external.string().optional(),
12031
12483
  capabilities: exports_external.array(exports_external.string()).optional().describe("Agent capabilities/skills for task routing (e.g. ['typescript', 'testing', 'devops'])"),
12032
12484
  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."),
12033
- 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")
12034
- }, async ({ name, description, capabilities, session_id, working_dir }) => {
12485
+ 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"),
12486
+ 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.")
12487
+ }, async ({ name, description, capabilities, session_id, working_dir, force }) => {
12035
12488
  try {
12036
12489
  const pool = getAgentPoolForProject(working_dir);
12037
- const result = registerAgent({ name, description, capabilities, session_id, working_dir, pool: pool || undefined });
12490
+ const result = registerAgent({ name, description, capabilities, session_id, working_dir, force, pool: pool || undefined });
12038
12491
  if (isAgentConflict(result)) {
12039
12492
  const suggestLine = result.suggestions && result.suggestions.length > 0 ? `
12040
12493
  Available names: ${result.suggestions.join(", ")}` : "";
@@ -12253,6 +12706,31 @@ ID: ${updated.id}`
12253
12706
  }
12254
12707
  });
12255
12708
  }
12709
+ if (shouldRegisterTool("release_agent")) {
12710
+ 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.", {
12711
+ agent_id: exports_external.string().describe("Your agent ID or name."),
12712
+ 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).")
12713
+ }, async ({ agent_id, session_id }) => {
12714
+ try {
12715
+ const agent = getAgent(agent_id) || getAgentByName(agent_id);
12716
+ if (!agent) {
12717
+ return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
12718
+ }
12719
+ const released = releaseAgent(agent.id, session_id);
12720
+ if (!released) {
12721
+ return { content: [{ type: "text", text: `Release denied: session_id does not match agent's current session.` }], isError: true };
12722
+ }
12723
+ return {
12724
+ content: [{
12725
+ type: "text",
12726
+ text: `Agent released: ${agent.name} (${agent.id}) \u2014 session cleared, name is now available.`
12727
+ }]
12728
+ };
12729
+ } catch (e) {
12730
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12731
+ }
12732
+ });
12733
+ }
12256
12734
  if (shouldRegisterTool("create_task_list")) {
12257
12735
  server.tool("create_task_list", "Create a task list container for organizing tasks.", {
12258
12736
  name: exports_external.string(),
@@ -12356,56 +12834,228 @@ Slug: ${list.slug}`
12356
12834
  }
12357
12835
  });
12358
12836
  }
12359
- if (shouldRegisterTool("delete_task_list")) {
12360
- server.tool("delete_task_list", "Delete a task list. Tasks are orphaned, not deleted.", {
12361
- id: exports_external.string()
12362
- }, async ({ id }) => {
12837
+ if (shouldRegisterTool("delete_task_list")) {
12838
+ server.tool("delete_task_list", "Delete a task list. Tasks are orphaned, not deleted.", {
12839
+ id: exports_external.string()
12840
+ }, async ({ id }) => {
12841
+ try {
12842
+ const resolvedId = resolveId(id, "task_lists");
12843
+ const deleted = deleteTaskList(resolvedId);
12844
+ return {
12845
+ content: [{
12846
+ type: "text",
12847
+ text: deleted ? `Task list ${id} deleted.` : `Task list ${id} not found.`
12848
+ }]
12849
+ };
12850
+ } catch (e) {
12851
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12852
+ }
12853
+ });
12854
+ }
12855
+ if (shouldRegisterTool("get_task_history")) {
12856
+ server.tool("get_task_history", "Get audit log \u2014 field changes with timestamps and actors.", {
12857
+ task_id: exports_external.string()
12858
+ }, async ({ task_id }) => {
12859
+ try {
12860
+ const resolvedId = resolveId(task_id);
12861
+ const { getTaskHistory: getTaskHistory2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
12862
+ const history = getTaskHistory2(resolvedId);
12863
+ if (history.length === 0)
12864
+ return { content: [{ type: "text", text: "No history for this task." }] };
12865
+ const text = history.map((h) => `${h.created_at} | ${h.action}${h.field ? ` ${h.field}` : ""}${h.old_value ? ` from "${h.old_value}"` : ""}${h.new_value ? ` to "${h.new_value}"` : ""}${h.agent_id ? ` by ${h.agent_id}` : ""}`).join(`
12866
+ `);
12867
+ return { content: [{ type: "text", text: `${history.length} change(s):
12868
+ ${text}` }] };
12869
+ } catch (e) {
12870
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12871
+ }
12872
+ });
12873
+ }
12874
+ if (shouldRegisterTool("get_recent_activity")) {
12875
+ server.tool("get_recent_activity", "Get recent task changes \u2014 global activity feed.", {
12876
+ limit: exports_external.number().optional()
12877
+ }, async ({ limit }) => {
12878
+ try {
12879
+ const { getRecentActivity: getRecentActivity2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
12880
+ const activity = getRecentActivity2(limit || 50);
12881
+ if (activity.length === 0)
12882
+ return { content: [{ type: "text", text: "No recent activity." }] };
12883
+ const text = activity.map((h) => `${h.created_at} | ${h.task_id.slice(0, 8)} | ${h.action}${h.field ? ` ${h.field}` : ""}${h.agent_id ? ` by ${h.agent_id}` : ""}`).join(`
12884
+ `);
12885
+ return { content: [{ type: "text", text: `${activity.length} recent change(s):
12886
+ ${text}` }] };
12887
+ } catch (e) {
12888
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12889
+ }
12890
+ });
12891
+ }
12892
+ if (shouldRegisterTool("recap")) {
12893
+ 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.", {
12894
+ hours: exports_external.number().optional().describe("Look back N hours (default: 8)"),
12895
+ project_id: exports_external.string().optional().describe("Filter to a specific project")
12896
+ }, async ({ hours, project_id }) => {
12897
+ try {
12898
+ const { getRecap: getRecap2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
12899
+ const recap = getRecap2(hours || 8, project_id);
12900
+ const lines = [`Recap \u2014 last ${recap.hours}h (since ${recap.since})`];
12901
+ if (recap.completed.length > 0) {
12902
+ lines.push(`
12903
+ Completed (${recap.completed.length}):`);
12904
+ for (const t of recap.completed) {
12905
+ const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
12906
+ lines.push(` \u2713 ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
12907
+ }
12908
+ }
12909
+ if (recap.in_progress.length > 0) {
12910
+ lines.push(`
12911
+ In Progress (${recap.in_progress.length}):`);
12912
+ for (const t of recap.in_progress)
12913
+ lines.push(` \u2192 ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
12914
+ }
12915
+ if (recap.blocked.length > 0) {
12916
+ lines.push(`
12917
+ Blocked (${recap.blocked.length}):`);
12918
+ for (const t of recap.blocked)
12919
+ lines.push(` \u2717 ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
12920
+ }
12921
+ if (recap.stale.length > 0) {
12922
+ lines.push(`
12923
+ Stale (${recap.stale.length}):`);
12924
+ for (const t of recap.stale)
12925
+ lines.push(` ! ${t.short_id || t.id.slice(0, 8)} ${t.title} \u2014 updated ${t.updated_at}`);
12926
+ }
12927
+ if (recap.agents.length > 0) {
12928
+ lines.push(`
12929
+ Agents:`);
12930
+ for (const a of recap.agents)
12931
+ lines.push(` ${a.name}: ${a.completed_count} done, ${a.in_progress_count} active (seen ${a.last_seen_at})`);
12932
+ }
12933
+ lines.push(`
12934
+ Created: ${recap.created.length} new tasks`);
12935
+ return { content: [{ type: "text", text: lines.join(`
12936
+ `) }] };
12937
+ } catch (e) {
12938
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12939
+ }
12940
+ });
12941
+ }
12942
+ if (shouldRegisterTool("standup")) {
12943
+ server.tool("standup", "Generate standup notes \u2014 completed tasks since yesterday grouped by agent, in-progress work, and blockers. Copy-paste ready.", {
12944
+ hours: exports_external.number().optional().describe("Look back N hours (default: 24)"),
12945
+ project_id: exports_external.string().optional()
12946
+ }, async ({ hours, project_id }) => {
12947
+ try {
12948
+ const { getRecap: getRecap2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
12949
+ const recap = getRecap2(hours || 24, project_id);
12950
+ const lines = [`Standup \u2014 last ${recap.hours}h`];
12951
+ const byAgent = new Map;
12952
+ for (const t of recap.completed) {
12953
+ const agent = t.assigned_to || "unassigned";
12954
+ if (!byAgent.has(agent))
12955
+ byAgent.set(agent, []);
12956
+ byAgent.get(agent).push(t);
12957
+ }
12958
+ if (byAgent.size > 0) {
12959
+ lines.push(`
12960
+ Done:`);
12961
+ for (const [agent, tasks] of byAgent) {
12962
+ lines.push(` ${agent}:`);
12963
+ for (const t of tasks) {
12964
+ const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
12965
+ lines.push(` \u2713 ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}`);
12966
+ }
12967
+ }
12968
+ } else {
12969
+ lines.push(`
12970
+ Nothing completed.`);
12971
+ }
12972
+ if (recap.in_progress.length > 0) {
12973
+ lines.push(`
12974
+ In Progress:`);
12975
+ for (const t of recap.in_progress)
12976
+ lines.push(` \u2192 ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
12977
+ }
12978
+ if (recap.blocked.length > 0) {
12979
+ lines.push(`
12980
+ Blocked:`);
12981
+ for (const t of recap.blocked)
12982
+ lines.push(` \u2717 ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
12983
+ }
12984
+ return { content: [{ type: "text", text: lines.join(`
12985
+ `) }] };
12986
+ } catch (e) {
12987
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12988
+ }
12989
+ });
12990
+ }
12991
+ if (shouldRegisterTool("import_github_issue")) {
12992
+ server.tool("import_github_issue", "Import a GitHub issue as a task. Requires gh CLI installed and authenticated.", {
12993
+ url: exports_external.string().describe("GitHub issue URL (e.g. https://github.com/owner/repo/issues/42)"),
12994
+ project_id: exports_external.string().optional(),
12995
+ task_list_id: exports_external.string().optional()
12996
+ }, async ({ url, project_id, task_list_id }) => {
12363
12997
  try {
12364
- const resolvedId = resolveId(id, "task_lists");
12365
- const deleted = deleteTaskList(resolvedId);
12366
- return {
12367
- content: [{
12368
- type: "text",
12369
- text: deleted ? `Task list ${id} deleted.` : `Task list ${id} not found.`
12370
- }]
12371
- };
12998
+ const { parseGitHubUrl: parseGitHubUrl2, fetchGitHubIssue: fetchGitHubIssue2, issueToTask: issueToTask2 } = await Promise.resolve().then(() => (init_github(), exports_github));
12999
+ const parsed = parseGitHubUrl2(url);
13000
+ if (!parsed)
13001
+ return { content: [{ type: "text", text: "Invalid GitHub issue URL." }], isError: true };
13002
+ const issue = fetchGitHubIssue2(parsed.owner, parsed.repo, parsed.number);
13003
+ const input = issueToTask2(issue, { project_id, task_list_id });
13004
+ const task = createTask(input);
13005
+ return { content: [{ type: "text", text: `Imported GH#${issue.number}: ${issue.title}
13006
+ Task: ${task.short_id || task.id} [${task.priority}]
13007
+ Labels: ${issue.labels.join(", ") || "none"}` }] };
12372
13008
  } catch (e) {
12373
13009
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
12374
13010
  }
12375
13011
  });
12376
13012
  }
12377
- if (shouldRegisterTool("get_task_history")) {
12378
- server.tool("get_task_history", "Get audit log \u2014 field changes with timestamps and actors.", {
12379
- task_id: exports_external.string()
12380
- }, async ({ task_id }) => {
13013
+ if (shouldRegisterTool("blame")) {
13014
+ server.tool("blame", "Show which tasks and agents touched a file \u2014 combines task_files and task_commits data.", {
13015
+ path: exports_external.string().describe("File path to look up")
13016
+ }, async ({ path }) => {
12381
13017
  try {
12382
- const resolvedId = resolveId(task_id);
12383
- const { getTaskHistory: getTaskHistory2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
12384
- const history = getTaskHistory2(resolvedId);
12385
- if (history.length === 0)
12386
- return { content: [{ type: "text", text: "No history for this task." }] };
12387
- const text = history.map((h) => `${h.created_at} | ${h.action}${h.field ? ` ${h.field}` : ""}${h.old_value ? ` from "${h.old_value}"` : ""}${h.new_value ? ` to "${h.new_value}"` : ""}${h.agent_id ? ` by ${h.agent_id}` : ""}`).join(`
12388
- `);
12389
- return { content: [{ type: "text", text: `${history.length} change(s):
12390
- ${text}` }] };
13018
+ const { findTasksByFile: findTasksByFile2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
13019
+ const db = getDatabase();
13020
+ const taskFiles = findTasksByFile2(path, db);
13021
+ 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}%`);
13022
+ const lines = [`Blame: ${path}`];
13023
+ if (taskFiles.length > 0) {
13024
+ lines.push(`
13025
+ Task File Links (${taskFiles.length}):`);
13026
+ for (const tf of taskFiles) {
13027
+ const task = getTask(tf.task_id, db);
13028
+ lines.push(` ${task?.short_id || tf.task_id.slice(0, 8)} ${task?.title || "?"} \u2014 ${tf.role || "file"}`);
13029
+ }
13030
+ }
13031
+ if (commitRows.length > 0) {
13032
+ lines.push(`
13033
+ Commit Links (${commitRows.length}):`);
13034
+ for (const c of commitRows)
13035
+ lines.push(` ${c.sha?.slice(0, 7)} ${c.short_id || c.task_id.slice(0, 8)} ${c.title || ""} \u2014 ${c.author || ""}`);
13036
+ }
13037
+ if (taskFiles.length === 0 && commitRows.length === 0) {
13038
+ lines.push("No task or commit links found.");
13039
+ }
13040
+ return { content: [{ type: "text", text: lines.join(`
13041
+ `) }] };
12391
13042
  } catch (e) {
12392
13043
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
12393
13044
  }
12394
13045
  });
12395
13046
  }
12396
- if (shouldRegisterTool("get_recent_activity")) {
12397
- server.tool("get_recent_activity", "Get recent task changes \u2014 global activity feed.", {
12398
- limit: exports_external.number().optional()
12399
- }, async ({ limit }) => {
13047
+ if (shouldRegisterTool("burndown")) {
13048
+ server.tool("burndown", "ASCII burndown chart showing actual vs ideal progress for a plan, project, or task list.", {
13049
+ plan_id: exports_external.string().optional(),
13050
+ project_id: exports_external.string().optional(),
13051
+ task_list_id: exports_external.string().optional()
13052
+ }, async ({ plan_id, project_id, task_list_id }) => {
12400
13053
  try {
12401
- const { getRecentActivity: getRecentActivity2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
12402
- const activity = getRecentActivity2(limit || 50);
12403
- if (activity.length === 0)
12404
- return { content: [{ type: "text", text: "No recent activity." }] };
12405
- const text = activity.map((h) => `${h.created_at} | ${h.task_id.slice(0, 8)} | ${h.action}${h.field ? ` ${h.field}` : ""}${h.agent_id ? ` by ${h.agent_id}` : ""}`).join(`
12406
- `);
12407
- return { content: [{ type: "text", text: `${activity.length} recent change(s):
12408
- ${text}` }] };
13054
+ const { getBurndown: getBurndown2 } = await Promise.resolve().then(() => (init_burndown(), exports_burndown));
13055
+ const data = getBurndown2({ plan_id, project_id, task_list_id });
13056
+ return { content: [{ type: "text", text: `Burndown: ${data.completed}/${data.total} done, ${data.remaining} remaining
13057
+
13058
+ ${data.chart}` }] };
12409
13059
  } catch (e) {
12410
13060
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
12411
13061
  }
@@ -13216,6 +13866,91 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
13216
13866
  }
13217
13867
  });
13218
13868
  }
13869
+ if (shouldRegisterTool("task_context")) {
13870
+ 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.", {
13871
+ id: exports_external.string().describe("Task ID, short_id, or partial ID")
13872
+ }, async ({ id }) => {
13873
+ try {
13874
+ const resolvedId = resolveId(id, "tasks");
13875
+ const task = getTaskWithRelations(resolvedId);
13876
+ if (!task)
13877
+ return { content: [{ type: "text", text: `Task not found: ${id}` }], isError: true };
13878
+ const lines = [];
13879
+ const sid = task.short_id || task.id.slice(0, 8);
13880
+ lines.push(`${sid} [${task.status}] [${task.priority}] ${task.title}`);
13881
+ if (task.description)
13882
+ lines.push(`
13883
+ Description:
13884
+ ${task.description}`);
13885
+ if (task.assigned_to)
13886
+ lines.push(`Assigned: ${task.assigned_to}`);
13887
+ if (task.started_at)
13888
+ lines.push(`Started: ${task.started_at}`);
13889
+ if (task.completed_at) {
13890
+ lines.push(`Completed: ${task.completed_at}`);
13891
+ if (task.started_at) {
13892
+ const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
13893
+ lines.push(`Duration: ${dur}m`);
13894
+ }
13895
+ }
13896
+ if (task.tags.length > 0)
13897
+ lines.push(`Tags: ${task.tags.join(", ")}`);
13898
+ if (task.dependencies.length > 0) {
13899
+ lines.push(`
13900
+ Depends on (${task.dependencies.length}):`);
13901
+ for (const dep of task.dependencies) {
13902
+ const blocked = dep.status !== "completed" && dep.status !== "cancelled";
13903
+ lines.push(` ${blocked ? "\u2717" : "\u2713"} ${dep.short_id || dep.id.slice(0, 8)} [${dep.status}] ${dep.title}`);
13904
+ }
13905
+ const unfinished = task.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
13906
+ if (unfinished.length > 0)
13907
+ lines.push(`\u26A0 BLOCKED by ${unfinished.length} unfinished dep(s)`);
13908
+ }
13909
+ if (task.blocked_by.length > 0) {
13910
+ lines.push(`
13911
+ Blocks (${task.blocked_by.length}):`);
13912
+ for (const b of task.blocked_by)
13913
+ lines.push(` ${b.short_id || b.id.slice(0, 8)} [${b.status}] ${b.title}`);
13914
+ }
13915
+ if (task.subtasks.length > 0) {
13916
+ lines.push(`
13917
+ Subtasks (${task.subtasks.length}):`);
13918
+ for (const st of task.subtasks)
13919
+ lines.push(` ${st.short_id || st.id.slice(0, 8)} [${st.status}] ${st.title}`);
13920
+ }
13921
+ try {
13922
+ const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
13923
+ const files = listTaskFiles2(task.id);
13924
+ if (files.length > 0) {
13925
+ lines.push(`
13926
+ Files (${files.length}):`);
13927
+ for (const f of files)
13928
+ lines.push(` ${f.role || "file"}: ${f.path}`);
13929
+ }
13930
+ } catch {}
13931
+ try {
13932
+ const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
13933
+ const commits = getTaskCommits2(task.id);
13934
+ if (commits.length > 0) {
13935
+ lines.push(`
13936
+ Commits (${commits.length}):`);
13937
+ for (const c of commits)
13938
+ lines.push(` ${c.commit_hash?.slice(0, 7)} ${c.message || ""}`);
13939
+ }
13940
+ } catch {}
13941
+ if (task.comments.length > 0) {
13942
+ lines.push(`
13943
+ Comments (${task.comments.length}):`);
13944
+ for (const c of task.comments)
13945
+ lines.push(` [${c.agent_id || "?"}] ${c.created_at}: ${c.content}`);
13946
+ }
13947
+ return { content: [{ type: "text", text: lines.join(`
13948
+ `) }] };
13949
+ } catch (e) {
13950
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13951
+ }
13952
+ });
13953
+ }
13219
13954
  if (shouldRegisterTool("get_context")) {
13220
13955
  server.tool("get_context", "Get a compact task summary for agent prompt injection. Returns formatted text.", {
13221
13956
  agent_id: exports_external.string().optional(),
@@ -13478,6 +14213,7 @@ ${stack_trace.slice(0, 1500)}
13478
14213
  "delete_agent",
13479
14214
  "unarchive_agent",
13480
14215
  "heartbeat",
14216
+ "release_agent",
13481
14217
  "get_my_tasks",
13482
14218
  "get_org_chart",
13483
14219
  "set_reports_to",
@@ -13494,6 +14230,12 @@ ${stack_trace.slice(0, 1500)}
13494
14230
  "claim_next_task",
13495
14231
  "get_task_history",
13496
14232
  "get_recent_activity",
14233
+ "recap",
14234
+ "task_context",
14235
+ "standup",
14236
+ "burndown",
14237
+ "blame",
14238
+ "import_github_issue",
13497
14239
  "create_webhook",
13498
14240
  "list_webhooks",
13499
14241
  "delete_webhook",
@@ -13618,8 +14360,8 @@ ${stack_trace.slice(0, 1500)}
13618
14360
  suggest_agent_name: `Check available agent names before registering. Shows active agents and, if a pool is configured, which pool names are free.
13619
14361
  Params: working_dir(string \u2014 your working directory, used to look up project pool from config)
13620
14362
  Example: {working_dir: '/workspace/platform'}`,
13621
- register_agent: `Register an agent. Any name is allowed \u2014 pool is advisory. Returns CONFLICT if name is held by a recently-active agent.
13622
- Params: name(string, req), description(string), capabilities(string[]), session_id(string \u2014 unique per session), working_dir(string \u2014 used to determine project pool)
14363
+ 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.
14364
+ 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)
13623
14365
  Example: {name: 'my-agent', session_id: 'abc123-1741952000', working_dir: '/workspace/platform'}`,
13624
14366
  list_agents: `List all registered agents (active by default). Set include_archived: true to see archived agents.
13625
14367
  Params: include_archived(boolean, optional)
@@ -13639,6 +14381,9 @@ ${stack_trace.slice(0, 1500)}
13639
14381
  heartbeat: `Update last_seen_at timestamp to signal you're still active. Call periodically during long tasks.
13640
14382
  Params: agent_id(string, req \u2014 your agent ID or name)
13641
14383
  Example: {agent_id: 'maximus'}`,
14384
+ release_agent: `Explicitly release/logout an agent \u2014 clears session binding and makes name immediately available. Call when session ends.
14385
+ Params: agent_id(string, req), session_id(string \u2014 only releases if matching)
14386
+ Example: {agent_id: 'maximus', session_id: 'my-session-123'}`,
13642
14387
  get_my_tasks: `Get all tasks assigned to/created by an agent, with stats (pending/active/done/rate).
13643
14388
  Params: agent_name(string, req)
13644
14389
  Example: {agent_name: 'maximus'}`,
@@ -13699,6 +14444,24 @@ ${stack_trace.slice(0, 1500)}
13699
14444
  get_recent_activity: `Get recent task changes across all tasks \u2014 global activity feed.
13700
14445
  Params: limit(number, default:50)
13701
14446
  Example: {limit: 20}`,
14447
+ recap: `Summary of what happened in the last N hours \u2014 completed tasks with durations, new tasks, in-progress, blocked, stale, agent activity.
14448
+ Params: hours(number, default:8), project_id(string)
14449
+ Example: {hours: 4}`,
14450
+ task_context: `Full orientation for a specific task \u2014 description, dependencies with blocked status, files, commits, comments, checklist, duration. Use before starting work.
14451
+ Params: id(string, req)
14452
+ Example: {id: 'OPE-00042'}`,
14453
+ standup: `Generate standup notes \u2014 completed tasks grouped by agent, in-progress, blocked. Copy-paste ready.
14454
+ Params: hours(number, default:24), project_id(string)
14455
+ Example: {hours: 24}`,
14456
+ import_github_issue: `Import a GitHub issue as a task. Requires gh CLI.
14457
+ Params: url(string, req), project_id(string), task_list_id(string)
14458
+ Example: {url: 'https://github.com/owner/repo/issues/42'}`,
14459
+ blame: `Show which tasks/agents touched a file \u2014 combines task_files and task_commits.
14460
+ Params: path(string, req)
14461
+ Example: {path: 'src/db/agents.ts'}`,
14462
+ burndown: `ASCII burndown chart \u2014 actual vs ideal progress for a plan, project, or task list.
14463
+ Params: plan_id(string), project_id(string), task_list_id(string)
14464
+ Example: {plan_id: 'abc123'}`,
13702
14465
  create_webhook: `Register a webhook for task change events.
13703
14466
  Params: url(string, req), events(string[] \u2014 empty=all), secret(string \u2014 HMAC signing)
13704
14467
  Example: {url: 'https://example.com/hook', events: ['task.created', 'task.completed']}`,
@@ -14474,6 +15237,48 @@ ${lines.join(`
14474
15237
  const lines = entries.map((e) => `#${e.rank} ${e.agent_name.padEnd(15)} score:${(e.composite_score * 100).toFixed(0).padStart(3)}% done:${String(e.tasks_completed).padStart(3)} rate:${(e.completion_rate * 100).toFixed(0)}%`);
14475
15238
  return { content: [{ type: "text", text: `Leaderboard:
14476
15239
  ${lines.join(`
15240
+ `)}` }] };
15241
+ } catch (e) {
15242
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
15243
+ }
15244
+ });
15245
+ }
15246
+ if (shouldRegisterTool("extract_todos")) {
15247
+ server.tool("extract_todos", "Scan source files for TODO/FIXME/HACK/BUG/XXX/NOTE comments and create tasks from them. Deduplicates on re-runs.", {
15248
+ path: exports_external.string().describe("Directory or file path to scan"),
15249
+ project_id: exports_external.string().optional().describe("Project to assign tasks to"),
15250
+ task_list_id: exports_external.string().optional().describe("Task list to add tasks to"),
15251
+ patterns: exports_external.array(exports_external.enum(["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"])).optional().describe("Tags to search for (default: all)"),
15252
+ tags: exports_external.array(exports_external.string()).optional().describe("Extra tags to add to created tasks"),
15253
+ assigned_to: exports_external.string().optional().describe("Agent to assign tasks to"),
15254
+ agent_id: exports_external.string().optional().describe("Agent performing the extraction"),
15255
+ dry_run: exports_external.boolean().optional().describe("If true, return found comments without creating tasks"),
15256
+ extensions: exports_external.array(exports_external.string()).optional().describe("File extensions to scan (e.g. ['ts', 'py'])")
15257
+ }, async (params) => {
15258
+ try {
15259
+ const { extractTodos: extractTodos2 } = (init_extract(), __toCommonJS(exports_extract));
15260
+ const resolved = { ...params };
15261
+ if (resolved["project_id"])
15262
+ resolved["project_id"] = resolveId(resolved["project_id"], "projects");
15263
+ if (resolved["task_list_id"])
15264
+ resolved["task_list_id"] = resolveId(resolved["task_list_id"], "task_lists");
15265
+ const result = extractTodos2(resolved);
15266
+ if (params.dry_run) {
15267
+ const lines = result.comments.map((c) => `[${c.tag}] ${c.message} \u2014 ${c.file}:${c.line}`);
15268
+ return { content: [{ type: "text", text: `Found ${result.comments.length} comment(s):
15269
+ ${lines.join(`
15270
+ `)}` }] };
15271
+ }
15272
+ const summary = [
15273
+ `Created ${result.tasks.length} task(s)`,
15274
+ result.skipped > 0 ? `Skipped ${result.skipped} duplicate(s)` : null,
15275
+ `Total comments found: ${result.comments.length}`
15276
+ ].filter(Boolean).join(`
15277
+ `);
15278
+ const taskLines = result.tasks.map((t) => formatTask(t));
15279
+ return { content: [{ type: "text", text: `${summary}
15280
+
15281
+ ${taskLines.join(`
14477
15282
  `)}` }] };
14478
15283
  } catch (e) {
14479
15284
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -14561,26 +15366,26 @@ __export(exports_serve, {
14561
15366
  startServer: () => startServer
14562
15367
  });
14563
15368
  import { existsSync as existsSync6 } from "fs";
14564
- import { join as join7, dirname as dirname3, extname, resolve as resolve2, sep } from "path";
15369
+ import { join as join8, dirname as dirname3, extname, resolve as resolve3, sep } from "path";
14565
15370
  import { fileURLToPath as fileURLToPath2 } from "url";
14566
15371
  function resolveDashboardDir() {
14567
15372
  const candidates = [];
14568
15373
  try {
14569
15374
  const scriptDir = dirname3(fileURLToPath2(import.meta.url));
14570
- candidates.push(join7(scriptDir, "..", "dashboard", "dist"));
14571
- candidates.push(join7(scriptDir, "..", "..", "dashboard", "dist"));
15375
+ candidates.push(join8(scriptDir, "..", "dashboard", "dist"));
15376
+ candidates.push(join8(scriptDir, "..", "..", "dashboard", "dist"));
14572
15377
  } catch {}
14573
15378
  if (process.argv[1]) {
14574
15379
  const mainDir = dirname3(process.argv[1]);
14575
- candidates.push(join7(mainDir, "..", "dashboard", "dist"));
14576
- candidates.push(join7(mainDir, "..", "..", "dashboard", "dist"));
15380
+ candidates.push(join8(mainDir, "..", "dashboard", "dist"));
15381
+ candidates.push(join8(mainDir, "..", "..", "dashboard", "dist"));
14577
15382
  }
14578
- candidates.push(join7(process.cwd(), "dashboard", "dist"));
15383
+ candidates.push(join8(process.cwd(), "dashboard", "dist"));
14579
15384
  for (const candidate of candidates) {
14580
15385
  if (existsSync6(candidate))
14581
15386
  return candidate;
14582
15387
  }
14583
- return join7(process.cwd(), "dashboard", "dist");
15388
+ return join8(process.cwd(), "dashboard", "dist");
14584
15389
  }
14585
15390
  function json(data, status = 200, port) {
14586
15391
  return new Response(JSON.stringify(data), {
@@ -15384,9 +16189,9 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
15384
16189
  }
15385
16190
  if (dashboardExists && (method === "GET" || method === "HEAD")) {
15386
16191
  if (path !== "/") {
15387
- const filePath = join7(dashboardDir, path);
15388
- const resolvedFile = resolve2(filePath);
15389
- const resolvedBase = resolve2(dashboardDir);
16192
+ const filePath = join8(dashboardDir, path);
16193
+ const resolvedFile = resolve3(filePath);
16194
+ const resolvedBase = resolve3(dashboardDir);
15390
16195
  if (!resolvedFile.startsWith(resolvedBase + sep) && resolvedFile !== resolvedBase) {
15391
16196
  return json({ error: "Forbidden" }, 403, port);
15392
16197
  }
@@ -15394,7 +16199,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
15394
16199
  if (res2)
15395
16200
  return res2;
15396
16201
  }
15397
- const indexPath = join7(dashboardDir, "index.html");
16202
+ const indexPath = join8(dashboardDir, "index.html");
15398
16203
  const res = serveStaticFile(indexPath);
15399
16204
  if (res)
15400
16205
  return res;
@@ -16483,21 +17288,307 @@ function App({ projectId }) {
16483
17288
  ]
16484
17289
  }, undefined, true, undefined, this);
16485
17290
  }
16486
- function renderApp(projectId) {
16487
- render(/* @__PURE__ */ jsxDEV7(App, {
16488
- projectId
16489
- }, undefined, false, undefined, this));
16490
- }
16491
- var init_App = __esm(() => {
16492
- init_Header();
16493
- init_TaskList();
16494
- init_TaskDetail();
16495
- init_TaskForm();
16496
- init_ProjectList();
16497
- init_SearchView();
17291
+ function renderApp(projectId) {
17292
+ render(/* @__PURE__ */ jsxDEV7(App, {
17293
+ projectId
17294
+ }, undefined, false, undefined, this));
17295
+ }
17296
+ var init_App = __esm(() => {
17297
+ init_Header();
17298
+ init_TaskList();
17299
+ init_TaskDetail();
17300
+ init_TaskForm();
17301
+ init_ProjectList();
17302
+ init_SearchView();
17303
+ init_tasks();
17304
+ init_projects();
17305
+ init_search();
17306
+ });
17307
+
17308
+ // src/cli/components/Dashboard.tsx
17309
+ var exports_Dashboard = {};
17310
+ __export(exports_Dashboard, {
17311
+ Dashboard: () => Dashboard
17312
+ });
17313
+ import { useState as useState4, useEffect as useEffect2 } from "react";
17314
+ import { Box as Box8, Text as Text7, useApp as useApp2, useInput as useInput4 } from "ink";
17315
+ import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
17316
+ function AgentStatus({ name, lastSeen, sessionId }) {
17317
+ const ago = Math.round((Date.now() - new Date(lastSeen).getTime()) / 60000);
17318
+ const color = ago < 5 ? "green" : ago < 15 ? "yellow" : "red";
17319
+ const symbol = ago < 5 ? "\u25CF" : ago < 15 ? "\u25D0" : "\u25CB";
17320
+ return /* @__PURE__ */ jsxDEV8(Box8, {
17321
+ children: [
17322
+ /* @__PURE__ */ jsxDEV8(Text7, {
17323
+ color,
17324
+ children: [
17325
+ symbol,
17326
+ " "
17327
+ ]
17328
+ }, undefined, true, undefined, this),
17329
+ /* @__PURE__ */ jsxDEV8(Text7, {
17330
+ bold: true,
17331
+ children: name
17332
+ }, undefined, false, undefined, this),
17333
+ /* @__PURE__ */ jsxDEV8(Text7, {
17334
+ dimColor: true,
17335
+ children: [
17336
+ " ",
17337
+ ago,
17338
+ "m ago"
17339
+ ]
17340
+ }, undefined, true, undefined, this),
17341
+ sessionId && /* @__PURE__ */ jsxDEV8(Text7, {
17342
+ dimColor: true,
17343
+ children: [
17344
+ " [",
17345
+ sessionId.slice(0, 8),
17346
+ "]"
17347
+ ]
17348
+ }, undefined, true, undefined, this)
17349
+ ]
17350
+ }, undefined, true, undefined, this);
17351
+ }
17352
+ function Dashboard({ projectId, refreshMs = 2000 }) {
17353
+ const { exit } = useApp2();
17354
+ const [tick, setTick] = useState4(0);
17355
+ const [recap, setRecap] = useState4(null);
17356
+ const [counts, setCounts] = useState4({ pending: 0, in_progress: 0, completed: 0, failed: 0, total: 0 });
17357
+ const [agents, setAgents] = useState4([]);
17358
+ useInput4((input) => {
17359
+ if (input === "q" || input === "Q")
17360
+ exit();
17361
+ });
17362
+ useEffect2(() => {
17363
+ const timer = setInterval(() => setTick((t) => t + 1), refreshMs);
17364
+ return () => clearInterval(timer);
17365
+ }, [refreshMs]);
17366
+ useEffect2(() => {
17367
+ try {
17368
+ const db = getDatabase();
17369
+ const filters = projectId ? { project_id: projectId } : {};
17370
+ const pending = countTasks({ ...filters, status: "pending" }, db);
17371
+ const in_progress = countTasks({ ...filters, status: "in_progress" }, db);
17372
+ const completed = countTasks({ ...filters, status: "completed" }, db);
17373
+ const failed = countTasks({ ...filters, status: "failed" }, db);
17374
+ setCounts({ pending, in_progress, completed, failed, total: pending + in_progress + completed + failed });
17375
+ setAgents(listAgents());
17376
+ setRecap(getRecap(1, projectId, db));
17377
+ } catch {}
17378
+ }, [tick, projectId]);
17379
+ return /* @__PURE__ */ jsxDEV8(Box8, {
17380
+ flexDirection: "column",
17381
+ padding: 1,
17382
+ children: [
17383
+ /* @__PURE__ */ jsxDEV8(Box8, {
17384
+ marginBottom: 1,
17385
+ children: [
17386
+ /* @__PURE__ */ jsxDEV8(Text7, {
17387
+ bold: true,
17388
+ color: "cyan",
17389
+ children: " todos dashboard "
17390
+ }, undefined, false, undefined, this),
17391
+ /* @__PURE__ */ jsxDEV8(Text7, {
17392
+ dimColor: true,
17393
+ children: [
17394
+ "| refreshing every ",
17395
+ refreshMs / 1000,
17396
+ "s | press q to quit"
17397
+ ]
17398
+ }, undefined, true, undefined, this)
17399
+ ]
17400
+ }, undefined, true, undefined, this),
17401
+ /* @__PURE__ */ jsxDEV8(Box8, {
17402
+ marginBottom: 1,
17403
+ children: [
17404
+ /* @__PURE__ */ jsxDEV8(Text7, {
17405
+ color: "yellow",
17406
+ children: [
17407
+ counts.pending,
17408
+ " pending"
17409
+ ]
17410
+ }, undefined, true, undefined, this),
17411
+ /* @__PURE__ */ jsxDEV8(Text7, {
17412
+ dimColor: true,
17413
+ children: " | "
17414
+ }, undefined, false, undefined, this),
17415
+ /* @__PURE__ */ jsxDEV8(Text7, {
17416
+ color: "blue",
17417
+ children: [
17418
+ counts.in_progress,
17419
+ " active"
17420
+ ]
17421
+ }, undefined, true, undefined, this),
17422
+ /* @__PURE__ */ jsxDEV8(Text7, {
17423
+ dimColor: true,
17424
+ children: " | "
17425
+ }, undefined, false, undefined, this),
17426
+ /* @__PURE__ */ jsxDEV8(Text7, {
17427
+ color: "green",
17428
+ children: [
17429
+ counts.completed,
17430
+ " done"
17431
+ ]
17432
+ }, undefined, true, undefined, this),
17433
+ /* @__PURE__ */ jsxDEV8(Text7, {
17434
+ dimColor: true,
17435
+ children: " | "
17436
+ }, undefined, false, undefined, this),
17437
+ /* @__PURE__ */ jsxDEV8(Text7, {
17438
+ color: "red",
17439
+ children: [
17440
+ counts.failed,
17441
+ " failed"
17442
+ ]
17443
+ }, undefined, true, undefined, this),
17444
+ /* @__PURE__ */ jsxDEV8(Text7, {
17445
+ dimColor: true,
17446
+ children: " | "
17447
+ }, undefined, false, undefined, this),
17448
+ /* @__PURE__ */ jsxDEV8(Text7, {
17449
+ children: [
17450
+ counts.total,
17451
+ " total"
17452
+ ]
17453
+ }, undefined, true, undefined, this)
17454
+ ]
17455
+ }, undefined, true, undefined, this),
17456
+ agents.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
17457
+ flexDirection: "column",
17458
+ marginBottom: 1,
17459
+ children: [
17460
+ /* @__PURE__ */ jsxDEV8(Text7, {
17461
+ bold: true,
17462
+ children: [
17463
+ "Agents (",
17464
+ agents.length,
17465
+ "):"
17466
+ ]
17467
+ }, undefined, true, undefined, this),
17468
+ agents.map((a) => /* @__PURE__ */ jsxDEV8(AgentStatus, {
17469
+ name: a.name,
17470
+ lastSeen: a.last_seen_at,
17471
+ sessionId: a.session_id
17472
+ }, a.id, false, undefined, this))
17473
+ ]
17474
+ }, undefined, true, undefined, this),
17475
+ recap && recap.in_progress.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
17476
+ flexDirection: "column",
17477
+ marginBottom: 1,
17478
+ children: [
17479
+ /* @__PURE__ */ jsxDEV8(Text7, {
17480
+ bold: true,
17481
+ color: "blue",
17482
+ children: [
17483
+ "In Progress (",
17484
+ recap.in_progress.length,
17485
+ "):"
17486
+ ]
17487
+ }, undefined, true, undefined, this),
17488
+ recap.in_progress.slice(0, 8).map((t) => /* @__PURE__ */ jsxDEV8(Box8, {
17489
+ children: [
17490
+ /* @__PURE__ */ jsxDEV8(Text7, {
17491
+ color: "cyan",
17492
+ children: [
17493
+ t.short_id || t.id.slice(0, 8),
17494
+ " "
17495
+ ]
17496
+ }, undefined, true, undefined, this),
17497
+ /* @__PURE__ */ jsxDEV8(Text7, {
17498
+ children: t.title
17499
+ }, undefined, false, undefined, this),
17500
+ t.assigned_to && /* @__PURE__ */ jsxDEV8(Text7, {
17501
+ dimColor: true,
17502
+ children: [
17503
+ " \u2014 ",
17504
+ t.assigned_to
17505
+ ]
17506
+ }, undefined, true, undefined, this)
17507
+ ]
17508
+ }, t.id, true, undefined, this))
17509
+ ]
17510
+ }, undefined, true, undefined, this),
17511
+ recap && recap.completed.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
17512
+ flexDirection: "column",
17513
+ marginBottom: 1,
17514
+ children: [
17515
+ /* @__PURE__ */ jsxDEV8(Text7, {
17516
+ bold: true,
17517
+ color: "green",
17518
+ children: [
17519
+ "Completed (last 1h: ",
17520
+ recap.completed.length,
17521
+ "):"
17522
+ ]
17523
+ }, undefined, true, undefined, this),
17524
+ recap.completed.slice(0, 5).map((t) => /* @__PURE__ */ jsxDEV8(Box8, {
17525
+ children: [
17526
+ /* @__PURE__ */ jsxDEV8(Text7, {
17527
+ color: "green",
17528
+ children: "\u2713 "
17529
+ }, undefined, false, undefined, this),
17530
+ /* @__PURE__ */ jsxDEV8(Text7, {
17531
+ color: "cyan",
17532
+ children: [
17533
+ t.short_id || t.id.slice(0, 8),
17534
+ " "
17535
+ ]
17536
+ }, undefined, true, undefined, this),
17537
+ /* @__PURE__ */ jsxDEV8(Text7, {
17538
+ children: t.title
17539
+ }, undefined, false, undefined, this),
17540
+ t.duration_minutes != null && /* @__PURE__ */ jsxDEV8(Text7, {
17541
+ dimColor: true,
17542
+ children: [
17543
+ " (",
17544
+ t.duration_minutes,
17545
+ "m)"
17546
+ ]
17547
+ }, undefined, true, undefined, this)
17548
+ ]
17549
+ }, t.id, true, undefined, this))
17550
+ ]
17551
+ }, undefined, true, undefined, this),
17552
+ recap && recap.stale.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
17553
+ flexDirection: "column",
17554
+ children: [
17555
+ /* @__PURE__ */ jsxDEV8(Text7, {
17556
+ bold: true,
17557
+ color: "red",
17558
+ children: [
17559
+ "Stale (",
17560
+ recap.stale.length,
17561
+ "):"
17562
+ ]
17563
+ }, undefined, true, undefined, this),
17564
+ recap.stale.slice(0, 3).map((t) => /* @__PURE__ */ jsxDEV8(Box8, {
17565
+ children: [
17566
+ /* @__PURE__ */ jsxDEV8(Text7, {
17567
+ color: "red",
17568
+ children: "! "
17569
+ }, undefined, false, undefined, this),
17570
+ /* @__PURE__ */ jsxDEV8(Text7, {
17571
+ color: "cyan",
17572
+ children: [
17573
+ t.short_id || t.id.slice(0, 8),
17574
+ " "
17575
+ ]
17576
+ }, undefined, true, undefined, this),
17577
+ /* @__PURE__ */ jsxDEV8(Text7, {
17578
+ children: t.title
17579
+ }, undefined, false, undefined, this)
17580
+ ]
17581
+ }, t.id, true, undefined, this))
17582
+ ]
17583
+ }, undefined, true, undefined, this)
17584
+ ]
17585
+ }, undefined, true, undefined, this);
17586
+ }
17587
+ var init_Dashboard = __esm(() => {
17588
+ init_database();
17589
+ init_agents();
16498
17590
  init_tasks();
16499
- init_projects();
16500
- init_search();
17591
+ init_audit();
16501
17592
  });
16502
17593
 
16503
17594
  // node_modules/commander/esm.mjs
@@ -16528,14 +17619,14 @@ init_search();
16528
17619
  init_sync();
16529
17620
  init_config();
16530
17621
  import chalk from "chalk";
16531
- import { execSync } from "child_process";
16532
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
16533
- import { basename, dirname as dirname4, join as join8, resolve as resolve3 } from "path";
17622
+ import { execSync as execSync2 } from "child_process";
17623
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
17624
+ import { basename, dirname as dirname4, join as join9, resolve as resolve4 } from "path";
16534
17625
  import { fileURLToPath as fileURLToPath3 } from "url";
16535
17626
  function getPackageVersion() {
16536
17627
  try {
16537
- const pkgPath = join8(dirname4(fileURLToPath3(import.meta.url)), "..", "..", "package.json");
16538
- return JSON.parse(readFileSync4(pkgPath, "utf-8")).version || "0.0.0";
17628
+ const pkgPath = join9(dirname4(fileURLToPath3(import.meta.url)), "..", "..", "package.json");
17629
+ return JSON.parse(readFileSync5(pkgPath, "utf-8")).version || "0.0.0";
16539
17630
  } catch {
16540
17631
  return "0.0.0";
16541
17632
  }
@@ -16567,14 +17658,14 @@ function resolveTaskId(partialId) {
16567
17658
  }
16568
17659
  function detectGitRoot() {
16569
17660
  try {
16570
- return execSync("git rev-parse --show-toplevel", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
17661
+ return execSync2("git rev-parse --show-toplevel", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
16571
17662
  } catch {
16572
17663
  return null;
16573
17664
  }
16574
17665
  }
16575
17666
  function autoDetectProject(opts) {
16576
17667
  if (opts.project) {
16577
- return getProjectByPath(resolve3(opts.project)) ?? undefined;
17668
+ return getProjectByPath(resolve4(opts.project)) ?? undefined;
16578
17669
  }
16579
17670
  if (process.env["TODOS_AUTO_PROJECT"] === "false")
16580
17671
  return;
@@ -16842,8 +17933,15 @@ program2.command("show <id>").description("Show full task details").action((id)
16842
17933
  console.log(` ${chalk.dim("Tags:")} ${task.tags.join(", ")}`);
16843
17934
  console.log(` ${chalk.dim("Version:")} ${task.version}`);
16844
17935
  console.log(` ${chalk.dim("Created:")} ${task.created_at}`);
16845
- if (task.completed_at)
17936
+ if (task.started_at)
17937
+ console.log(` ${chalk.dim("Started:")} ${task.started_at}`);
17938
+ if (task.completed_at) {
16846
17939
  console.log(` ${chalk.dim("Done:")} ${task.completed_at}`);
17940
+ if (task.started_at) {
17941
+ const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
17942
+ console.log(` ${chalk.dim("Duration:")} ${dur}m`);
17943
+ }
17944
+ }
16847
17945
  if (task.subtasks.length > 0) {
16848
17946
  console.log(chalk.bold(`
16849
17947
  Subtasks (${task.subtasks.length}):`));
@@ -16874,6 +17972,137 @@ program2.command("show <id>").description("Show full task details").action((id)
16874
17972
  }
16875
17973
  }
16876
17974
  });
17975
+ program2.command("inspect [id]").description("Full orientation for a task \u2014 details, description, dependencies, blockers, files, commits, comments. If no ID given, shows current in-progress task for --agent.").action((id) => {
17976
+ const globalOpts = program2.opts();
17977
+ let resolvedId = id ? resolveTaskId(id) : null;
17978
+ if (!resolvedId && globalOpts.agent) {
17979
+ const { listTasks: listTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
17980
+ const active = listTasks2({ status: "in_progress", assigned_to: globalOpts.agent });
17981
+ if (active.length > 0)
17982
+ resolvedId = active[0].id;
17983
+ }
17984
+ if (!resolvedId) {
17985
+ console.error(chalk.red("No task ID given and no active task found. Pass an ID or use --agent."));
17986
+ process.exit(1);
17987
+ }
17988
+ const task = getTaskWithRelations(resolvedId);
17989
+ if (!task) {
17990
+ console.error(chalk.red(`Task not found: ${id || resolvedId}`));
17991
+ process.exit(1);
17992
+ }
17993
+ if (globalOpts.json) {
17994
+ const { listTaskFiles: listTaskFiles2 } = (init_task_files(), __toCommonJS(exports_task_files));
17995
+ const { getTaskCommits: getTaskCommits2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
17996
+ try {
17997
+ task.files = listTaskFiles2(task.id);
17998
+ } catch {
17999
+ task.files = [];
18000
+ }
18001
+ try {
18002
+ task.commits = getTaskCommits2(task.id);
18003
+ } catch {
18004
+ task.commits = [];
18005
+ }
18006
+ output(task, true);
18007
+ return;
18008
+ }
18009
+ const sid = task.short_id || task.id.slice(0, 8);
18010
+ const statusColor = statusColors4[task.status] || chalk.white;
18011
+ const prioColor = priorityColors2[task.priority] || chalk.white;
18012
+ console.log(chalk.bold(`
18013
+ ${chalk.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${task.title}
18014
+ `));
18015
+ if (task.description) {
18016
+ console.log(chalk.dim("Description:"));
18017
+ console.log(` ${task.description}
18018
+ `);
18019
+ }
18020
+ if (task.assigned_to)
18021
+ console.log(` ${chalk.dim("Assigned:")} ${task.assigned_to}`);
18022
+ if (task.locked_by)
18023
+ console.log(` ${chalk.dim("Locked by:")} ${task.locked_by}`);
18024
+ if (task.project_id)
18025
+ console.log(` ${chalk.dim("Project:")} ${task.project_id}`);
18026
+ if (task.plan_id)
18027
+ console.log(` ${chalk.dim("Plan:")} ${task.plan_id}`);
18028
+ if (task.started_at)
18029
+ console.log(` ${chalk.dim("Started:")} ${task.started_at}`);
18030
+ if (task.completed_at) {
18031
+ console.log(` ${chalk.dim("Completed:")} ${task.completed_at}`);
18032
+ if (task.started_at) {
18033
+ const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
18034
+ console.log(` ${chalk.dim("Duration:")} ${dur}m`);
18035
+ }
18036
+ }
18037
+ if (task.estimated_minutes)
18038
+ console.log(` ${chalk.dim("Estimate:")} ${task.estimated_minutes}m`);
18039
+ if (task.tags.length > 0)
18040
+ console.log(` ${chalk.dim("Tags:")} ${task.tags.join(", ")}`);
18041
+ const unfinishedDeps = task.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
18042
+ if (task.dependencies.length > 0) {
18043
+ console.log(chalk.bold(`
18044
+ Depends on (${task.dependencies.length}):`));
18045
+ for (const dep of task.dependencies) {
18046
+ const blocked = dep.status !== "completed" && dep.status !== "cancelled";
18047
+ const icon = blocked ? chalk.red("\u2717") : chalk.green("\u2713");
18048
+ console.log(` ${icon} ${formatTaskLine(dep)}`);
18049
+ }
18050
+ }
18051
+ if (unfinishedDeps.length > 0) {
18052
+ console.log(chalk.red(`
18053
+ BLOCKED by ${unfinishedDeps.length} unfinished dep(s)`));
18054
+ }
18055
+ if (task.blocked_by.length > 0) {
18056
+ console.log(chalk.bold(`
18057
+ Blocks (${task.blocked_by.length}):`));
18058
+ for (const b of task.blocked_by)
18059
+ console.log(` ${formatTaskLine(b)}`);
18060
+ }
18061
+ if (task.subtasks.length > 0) {
18062
+ console.log(chalk.bold(`
18063
+ Subtasks (${task.subtasks.length}):`));
18064
+ for (const st of task.subtasks)
18065
+ console.log(` ${formatTaskLine(st)}`);
18066
+ }
18067
+ try {
18068
+ const { listTaskFiles: listTaskFiles2 } = (init_task_files(), __toCommonJS(exports_task_files));
18069
+ const files = listTaskFiles2(task.id);
18070
+ if (files.length > 0) {
18071
+ console.log(chalk.bold(`
18072
+ Files (${files.length}):`));
18073
+ for (const f of files)
18074
+ console.log(` ${chalk.dim(f.role || "file")} ${f.path}`);
18075
+ }
18076
+ } catch {}
18077
+ try {
18078
+ const { getTaskCommits: getTaskCommits2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
18079
+ const commits = getTaskCommits2(task.id);
18080
+ if (commits.length > 0) {
18081
+ console.log(chalk.bold(`
18082
+ Commits (${commits.length}):`));
18083
+ for (const c of commits)
18084
+ console.log(` ${chalk.yellow(c.commit_hash.slice(0, 7))} ${c.message || ""}`);
18085
+ }
18086
+ } catch {}
18087
+ if (task.comments.length > 0) {
18088
+ console.log(chalk.bold(`
18089
+ Comments (${task.comments.length}):`));
18090
+ for (const c of task.comments) {
18091
+ const agent = c.agent_id ? chalk.cyan(`[${c.agent_id}] `) : "";
18092
+ console.log(` ${agent}${chalk.dim(c.created_at)}: ${c.content}`);
18093
+ }
18094
+ }
18095
+ if (task.checklist && task.checklist.length > 0) {
18096
+ const done = task.checklist.filter((c) => c.checked).length;
18097
+ console.log(chalk.bold(`
18098
+ Checklist (${done}/${task.checklist.length}):`));
18099
+ for (const item of task.checklist) {
18100
+ const icon = item.checked ? chalk.green("\u2611") : chalk.dim("\u2610");
18101
+ console.log(` ${icon} ${item.text || item.title}`);
18102
+ }
18103
+ }
18104
+ console.log();
18105
+ });
16877
18106
  program2.command("history <id>").description("Show change history for a task (audit log)").action((id) => {
16878
18107
  const globalOpts = program2.opts();
16879
18108
  const resolvedId = resolveTaskId(id);
@@ -17377,7 +18606,7 @@ program2.command("deps <id>").description("Manage task dependencies").option("--
17377
18606
  program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--name <name>", "Project name (with --add)").option("--task-list-id <id>", "Custom task list ID (with --add)").action((opts) => {
17378
18607
  const globalOpts = program2.opts();
17379
18608
  if (opts.add) {
17380
- const projectPath = resolve3(opts.add);
18609
+ const projectPath = resolve4(opts.add);
17381
18610
  const name = opts.name || basename(projectPath);
17382
18611
  const existing = getProjectByPath(projectPath);
17383
18612
  let project;
@@ -17414,6 +18643,55 @@ program2.command("projects").description("List and manage projects").option("--a
17414
18643
  console.log(`${chalk.dim(p.id.slice(0, 8))} ${chalk.bold(p.name)} ${chalk.dim(p.path)}${taskList}${p.description ? ` - ${p.description}` : ""}`);
17415
18644
  }
17416
18645
  });
18646
+ program2.command("extract <path>").description("Extract TODO/FIXME/HACK/BUG/XXX/NOTE comments from source files and create tasks").option("--dry-run", "Show extracted comments without creating tasks").option("--pattern <tags>", "Comma-separated tags to look for (default: TODO,FIXME,HACK,XXX,BUG,NOTE)").option("-t, --tags <tags>", "Extra comma-separated tags to add to created tasks").option("--assign <agent>", "Assign extracted tasks to an agent").option("--list <id>", "Task list ID").option("--ext <extensions>", "Comma-separated file extensions to scan (e.g. ts,py,go)").action((scanPath, opts) => {
18647
+ try {
18648
+ const globalOpts = program2.opts();
18649
+ const projectId = autoProject(globalOpts);
18650
+ const { extractTodos: extractTodos2, EXTRACT_TAGS: EXTRACT_TAGS2 } = (init_extract(), __toCommonJS(exports_extract));
18651
+ const patterns = opts.pattern ? opts.pattern.split(",").map((t) => t.trim().toUpperCase()) : undefined;
18652
+ const taskListId = opts.list ? (() => {
18653
+ const db = getDatabase();
18654
+ const id = resolvePartialId(db, "task_lists", opts.list);
18655
+ if (!id) {
18656
+ console.error(chalk.red(`Could not resolve task list ID: ${opts.list}`));
18657
+ process.exit(1);
18658
+ }
18659
+ return id;
18660
+ })() : undefined;
18661
+ const result = extractTodos2({
18662
+ path: resolve4(scanPath),
18663
+ patterns,
18664
+ project_id: projectId,
18665
+ task_list_id: taskListId,
18666
+ tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
18667
+ assigned_to: opts.assign,
18668
+ agent_id: globalOpts.agent,
18669
+ dry_run: opts.dryRun,
18670
+ extensions: opts.ext ? opts.ext.split(",").map((e) => e.trim()) : undefined
18671
+ });
18672
+ if (globalOpts.json) {
18673
+ console.log(JSON.stringify(opts.dryRun ? { comments: result.comments } : { tasks_created: result.tasks.length, skipped: result.skipped, comments: result.comments.length }, null, 2));
18674
+ } else if (opts.dryRun) {
18675
+ console.log(chalk.cyan(`Found ${result.comments.length} comment(s):
18676
+ `));
18677
+ for (const c of result.comments) {
18678
+ console.log(` ${chalk.yellow(`[${c.tag}]`)} ${c.message}`);
18679
+ console.log(` ${chalk.gray(`${c.file}:${c.line}`)}`);
18680
+ }
18681
+ } else {
18682
+ console.log(chalk.green(`Created ${result.tasks.length} task(s)`));
18683
+ if (result.skipped > 0) {
18684
+ console.log(chalk.gray(`Skipped ${result.skipped} duplicate(s)`));
18685
+ }
18686
+ console.log(chalk.gray(`Total comments found: ${result.comments.length}`));
18687
+ for (const t of result.tasks) {
18688
+ console.log(formatTaskLine(t));
18689
+ }
18690
+ }
18691
+ } catch (e) {
18692
+ handleError(e);
18693
+ }
18694
+ });
17417
18695
  program2.command("export").description("Export tasks").option("-f, --format <format>", "Format: json or md", "json").action((opts) => {
17418
18696
  const globalOpts = program2.opts();
17419
18697
  const projectId = autoProject(globalOpts);
@@ -17479,11 +18757,11 @@ var hooks = program2.command("hooks").description("Manage Claude Code hook integ
17479
18757
  hooks.command("install").description("Install Claude Code hooks for auto-sync").action(() => {
17480
18758
  let todosBin = "todos";
17481
18759
  try {
17482
- const p = execSync("which todos", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
18760
+ const p = execSync2("which todos", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
17483
18761
  if (p)
17484
18762
  todosBin = p;
17485
18763
  } catch {}
17486
- const hooksDir = join8(process.cwd(), ".claude", "hooks");
18764
+ const hooksDir = join9(process.cwd(), ".claude", "hooks");
17487
18765
  if (!existsSync7(hooksDir))
17488
18766
  mkdirSync3(hooksDir, { recursive: true });
17489
18767
  const hookScript = `#!/usr/bin/env bash
@@ -17509,11 +18787,11 @@ esac
17509
18787
 
17510
18788
  exit 0
17511
18789
  `;
17512
- const hookPath = join8(hooksDir, "todos-sync.sh");
18790
+ const hookPath = join9(hooksDir, "todos-sync.sh");
17513
18791
  writeFileSync3(hookPath, hookScript);
17514
- execSync(`chmod +x "${hookPath}"`);
18792
+ execSync2(`chmod +x "${hookPath}"`);
17515
18793
  console.log(chalk.green(`Hook script created: ${hookPath}`));
17516
- const settingsPath = join8(process.cwd(), ".claude", "settings.json");
18794
+ const settingsPath = join9(process.cwd(), ".claude", "settings.json");
17517
18795
  const settings = readJsonFile2(settingsPath);
17518
18796
  if (!settings["hooks"]) {
17519
18797
  settings["hooks"] = {};
@@ -17556,11 +18834,11 @@ program2.command("mcp").description("Start MCP server (stdio)").option("--regist
17556
18834
  var HOME2 = process.env["HOME"] || process.env["USERPROFILE"] || "~";
17557
18835
  function getMcpBinaryPath() {
17558
18836
  try {
17559
- const p = execSync("which todos-mcp", { encoding: "utf-8" }).trim();
18837
+ const p = execSync2("which todos-mcp", { encoding: "utf-8" }).trim();
17560
18838
  if (p)
17561
18839
  return p;
17562
18840
  } catch {}
17563
- const bunBin = join8(HOME2, ".bun", "bin", "todos-mcp");
18841
+ const bunBin = join9(HOME2, ".bun", "bin", "todos-mcp");
17564
18842
  if (existsSync7(bunBin))
17565
18843
  return bunBin;
17566
18844
  return "todos-mcp";
@@ -17569,7 +18847,7 @@ function readJsonFile2(path) {
17569
18847
  if (!existsSync7(path))
17570
18848
  return {};
17571
18849
  try {
17572
- return JSON.parse(readFileSync4(path, "utf-8"));
18850
+ return JSON.parse(readFileSync5(path, "utf-8"));
17573
18851
  } catch {
17574
18852
  return {};
17575
18853
  }
@@ -17584,7 +18862,7 @@ function writeJsonFile2(path, data) {
17584
18862
  function readTomlFile(path) {
17585
18863
  if (!existsSync7(path))
17586
18864
  return "";
17587
- return readFileSync4(path, "utf-8");
18865
+ return readFileSync5(path, "utf-8");
17588
18866
  }
17589
18867
  function writeTomlFile(path, content) {
17590
18868
  const dir = dirname4(path);
@@ -17596,8 +18874,8 @@ function registerClaude(binPath, global) {
17596
18874
  const scope = global ? "user" : "project";
17597
18875
  const cmd = `claude mcp add --transport stdio --scope ${scope} todos -- ${binPath}`;
17598
18876
  try {
17599
- const { execSync: execSync2 } = __require("child_process");
17600
- execSync2(cmd, { stdio: "pipe" });
18877
+ const { execSync: execSync3 } = __require("child_process");
18878
+ execSync3(cmd, { stdio: "pipe" });
17601
18879
  console.log(chalk.green(`Claude Code (${scope}): registered via 'claude mcp add'`));
17602
18880
  } catch {
17603
18881
  console.log(chalk.yellow(`Claude Code: could not auto-register. Run this command manually:`));
@@ -17606,8 +18884,8 @@ function registerClaude(binPath, global) {
17606
18884
  }
17607
18885
  function unregisterClaude(_global) {
17608
18886
  try {
17609
- const { execSync: execSync2 } = __require("child_process");
17610
- execSync2("claude mcp remove todos", { stdio: "pipe" });
18887
+ const { execSync: execSync3 } = __require("child_process");
18888
+ execSync3("claude mcp remove todos", { stdio: "pipe" });
17611
18889
  console.log(chalk.green(`Claude Code: removed todos MCP server`));
17612
18890
  } catch {
17613
18891
  console.log(chalk.yellow(`Claude Code: could not auto-remove. Run manually:`));
@@ -17615,7 +18893,7 @@ function unregisterClaude(_global) {
17615
18893
  }
17616
18894
  }
17617
18895
  function registerCodex(binPath) {
17618
- const configPath = join8(HOME2, ".codex", "config.toml");
18896
+ const configPath = join9(HOME2, ".codex", "config.toml");
17619
18897
  let content = readTomlFile(configPath);
17620
18898
  content = removeTomlBlock(content, "mcp_servers.todos");
17621
18899
  const block = `
@@ -17629,7 +18907,7 @@ args = []
17629
18907
  console.log(chalk.green(`Codex CLI: registered in ${configPath}`));
17630
18908
  }
17631
18909
  function unregisterCodex() {
17632
- const configPath = join8(HOME2, ".codex", "config.toml");
18910
+ const configPath = join9(HOME2, ".codex", "config.toml");
17633
18911
  let content = readTomlFile(configPath);
17634
18912
  if (!content.includes("[mcp_servers.todos]")) {
17635
18913
  console.log(chalk.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -17662,7 +18940,7 @@ function removeTomlBlock(content, blockName) {
17662
18940
  `);
17663
18941
  }
17664
18942
  function registerGemini(binPath) {
17665
- const configPath = join8(HOME2, ".gemini", "settings.json");
18943
+ const configPath = join9(HOME2, ".gemini", "settings.json");
17666
18944
  const config = readJsonFile2(configPath);
17667
18945
  if (!config["mcpServers"]) {
17668
18946
  config["mcpServers"] = {};
@@ -17676,7 +18954,7 @@ function registerGemini(binPath) {
17676
18954
  console.log(chalk.green(`Gemini CLI: registered in ${configPath}`));
17677
18955
  }
17678
18956
  function unregisterGemini() {
17679
- const configPath = join8(HOME2, ".gemini", "settings.json");
18957
+ const configPath = join9(HOME2, ".gemini", "settings.json");
17680
18958
  const config = readJsonFile2(configPath);
17681
18959
  const servers = config["mcpServers"];
17682
18960
  if (!servers || !("todos" in servers)) {
@@ -17724,6 +19002,115 @@ function unregisterMcp(agent, global) {
17724
19002
  }
17725
19003
  }
17726
19004
  }
19005
+ program2.command("import <url>").description("Import a GitHub issue as a task").option("--project <id>", "Project ID").option("--list <id>", "Task list ID").action((url, opts) => {
19006
+ const globalOpts = program2.opts();
19007
+ const { parseGitHubUrl: parseGitHubUrl2, fetchGitHubIssue: fetchGitHubIssue2, issueToTask: issueToTask2 } = (init_github(), __toCommonJS(exports_github));
19008
+ const parsed = parseGitHubUrl2(url);
19009
+ if (!parsed) {
19010
+ console.error(chalk.red("Invalid GitHub issue URL. Expected: https://github.com/owner/repo/issues/123"));
19011
+ process.exit(1);
19012
+ }
19013
+ try {
19014
+ const issue = fetchGitHubIssue2(parsed.owner, parsed.repo, parsed.number);
19015
+ const projectId = opts.project || autoProject(globalOpts) || undefined;
19016
+ const input = issueToTask2(issue, { project_id: projectId, task_list_id: opts.list });
19017
+ const task = createTask(input);
19018
+ if (globalOpts.json) {
19019
+ output(task, true);
19020
+ return;
19021
+ }
19022
+ console.log(chalk.green(`Imported GH#${issue.number}: ${issue.title}`));
19023
+ console.log(` ${chalk.dim("Task ID:")} ${task.short_id || task.id}`);
19024
+ console.log(` ${chalk.dim("Labels:")} ${issue.labels.join(", ") || "none"}`);
19025
+ console.log(` ${chalk.dim("Priority:")} ${task.priority}`);
19026
+ } catch (e) {
19027
+ if (e.message?.includes("gh")) {
19028
+ console.error(chalk.red("GitHub CLI (gh) not found or not authenticated. Install: https://cli.github.com"));
19029
+ } else {
19030
+ console.error(chalk.red(`Import failed: ${e.message}`));
19031
+ }
19032
+ process.exit(1);
19033
+ }
19034
+ });
19035
+ program2.command("link-commit <task-id> <sha>").description("Link a git commit to a task").option("--message <text>", "Commit message").option("--author <name>", "Commit author").option("--files <list>", "Comma-separated list of changed files").action((taskId, sha, opts) => {
19036
+ const globalOpts = program2.opts();
19037
+ const resolvedId = resolveTaskId(taskId);
19038
+ const { linkTaskToCommit: linkTaskToCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
19039
+ const commit = linkTaskToCommit2({
19040
+ task_id: resolvedId,
19041
+ sha,
19042
+ message: opts.message,
19043
+ author: opts.author,
19044
+ files_changed: opts.files ? opts.files.split(",").filter(Boolean) : undefined
19045
+ });
19046
+ if (globalOpts.json) {
19047
+ output(commit, true);
19048
+ return;
19049
+ }
19050
+ console.log(chalk.green(`Linked commit ${sha.slice(0, 7)} to task ${taskId}`));
19051
+ });
19052
+ var hookCmd = program2.command("hook").description("Manage git hooks for auto-linking commits to tasks");
19053
+ hookCmd.command("install").description("Install post-commit hook that auto-links commits to tasks").action(() => {
19054
+ const { execSync: execSync3 } = __require("child_process");
19055
+ try {
19056
+ const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
19057
+ const hookPath = `${gitDir}/hooks/post-commit`;
19058
+ const { existsSync: existsSync8, readFileSync: readFileSync6, writeFileSync: writeFileSync4, chmodSync } = __require("fs");
19059
+ const marker = "# todos-auto-link";
19060
+ if (existsSync8(hookPath)) {
19061
+ const existing = readFileSync6(hookPath, "utf-8");
19062
+ if (existing.includes(marker)) {
19063
+ console.log(chalk.yellow("Hook already installed."));
19064
+ return;
19065
+ }
19066
+ writeFileSync4(hookPath, existing + `
19067
+ ${marker}
19068
+ $(dirname "$0")/../../scripts/post-commit-hook.sh
19069
+ `);
19070
+ } else {
19071
+ writeFileSync4(hookPath, `#!/usr/bin/env bash
19072
+ ${marker}
19073
+ $(dirname "$0")/../../scripts/post-commit-hook.sh
19074
+ `);
19075
+ chmodSync(hookPath, 493);
19076
+ }
19077
+ console.log(chalk.green("Post-commit hook installed. Commits with task IDs (e.g. OPE-00042) will auto-link."));
19078
+ } catch (e) {
19079
+ console.error(chalk.red("Not in a git repository or hook install failed."));
19080
+ process.exit(1);
19081
+ }
19082
+ });
19083
+ hookCmd.command("uninstall").description("Remove the todos post-commit hook").action(() => {
19084
+ const { execSync: execSync3 } = __require("child_process");
19085
+ try {
19086
+ const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
19087
+ const hookPath = `${gitDir}/hooks/post-commit`;
19088
+ const { existsSync: existsSync8, readFileSync: readFileSync6, writeFileSync: writeFileSync4 } = __require("fs");
19089
+ const marker = "# todos-auto-link";
19090
+ if (!existsSync8(hookPath)) {
19091
+ console.log(chalk.dim("No post-commit hook found."));
19092
+ return;
19093
+ }
19094
+ const content = readFileSync6(hookPath, "utf-8");
19095
+ if (!content.includes(marker)) {
19096
+ console.log(chalk.dim("Hook not managed by todos."));
19097
+ return;
19098
+ }
19099
+ const cleaned = content.split(`
19100
+ `).filter((l) => !l.includes(marker) && !l.includes("post-commit-hook.sh")).join(`
19101
+ `).trim();
19102
+ if (cleaned === "#!/usr/bin/env bash" || cleaned === "") {
19103
+ __require("fs").unlinkSync(hookPath);
19104
+ } else {
19105
+ writeFileSync4(hookPath, cleaned + `
19106
+ `);
19107
+ }
19108
+ console.log(chalk.green("Post-commit hook removed."));
19109
+ } catch (e) {
19110
+ console.error(chalk.red("Not in a git repository or hook removal failed."));
19111
+ process.exit(1);
19112
+ }
19113
+ });
17727
19114
  program2.command("init <name>").description("Register an agent and get a short UUID").option("-d, --description <text>", "Agent description").action((name, opts) => {
17728
19115
  const globalOpts = program2.opts();
17729
19116
  try {
@@ -17765,6 +19152,30 @@ program2.command("heartbeat [agent]").description("Update last_seen_at to signal
17765
19152
  console.log(chalk.green(`\u2665 ${a.name} (${a.id.slice(0, 8)}) \u2014 heartbeat sent`));
17766
19153
  }
17767
19154
  });
19155
+ program2.command("release [agent]").description("Release/logout an agent \u2014 clears session binding so the name is immediately available").option("--session-id <id>", "Only release if session ID matches").action((agent, opts) => {
19156
+ const globalOpts = program2.opts();
19157
+ const agentId = agent || globalOpts.agent;
19158
+ if (!agentId) {
19159
+ console.error(chalk.red("Agent ID or name required. Use --agent or pass as argument."));
19160
+ process.exit(1);
19161
+ }
19162
+ const { getAgent: getAgent2, getAgentByName: getAgentByName2 } = (init_agents(), __toCommonJS(exports_agents));
19163
+ const a = getAgent2(agentId) || getAgentByName2(agentId);
19164
+ if (!a) {
19165
+ console.error(chalk.red(`Agent not found: ${agentId}`));
19166
+ process.exit(1);
19167
+ }
19168
+ const released = releaseAgent(a.id, opts?.sessionId);
19169
+ if (!released) {
19170
+ console.error(chalk.red("Release denied: session_id does not match agent's current session."));
19171
+ process.exit(1);
19172
+ }
19173
+ if (globalOpts.json) {
19174
+ console.log(JSON.stringify({ agent_id: a.id, name: a.name, released: true }));
19175
+ } else {
19176
+ console.log(chalk.green(`\u2713 ${a.name} (${a.id}) released \u2014 name is now available.`));
19177
+ }
19178
+ });
17768
19179
  program2.command("focus [project]").description("Focus on a project (or clear focus if no project given)").action((project) => {
17769
19180
  const globalOpts = program2.opts();
17770
19181
  const agentId = globalOpts.agent;
@@ -17990,13 +19401,13 @@ Update available: ${currentVersion} \u2192 ${latestVersion}`));
17990
19401
  }
17991
19402
  let useBun = false;
17992
19403
  try {
17993
- execSync("which bun", { stdio: "ignore" });
19404
+ execSync2("which bun", { stdio: "ignore" });
17994
19405
  useBun = true;
17995
19406
  } catch {}
17996
19407
  const cmd = useBun ? "bun add -g @hasna/todos@latest" : "npm install -g @hasna/todos@latest";
17997
19408
  console.log(chalk.dim(`
17998
19409
  Running: ${cmd}`));
17999
- execSync(cmd, { stdio: "inherit" });
19410
+ execSync2(cmd, { stdio: "inherit" });
18000
19411
  console.log(chalk.green(`
18001
19412
  Updated to ${latestVersion}!`));
18002
19413
  } catch (e) {
@@ -18005,7 +19416,7 @@ Updated to ${latestVersion}!`));
18005
19416
  });
18006
19417
  program2.command("config").description("View or update configuration").option("--get <key>", "Get a config value").option("--set <key=value>", "Set a config value (e.g. completion_guard.enabled=true)").action((opts) => {
18007
19418
  const globalOpts = program2.opts();
18008
- const configPath = join8(process.env["HOME"] || "~", ".todos", "config.json");
19419
+ const configPath = join9(process.env["HOME"] || "~", ".todos", "config.json");
18009
19420
  if (opts.get) {
18010
19421
  const config2 = loadConfig();
18011
19422
  const keys = opts.get.split(".");
@@ -18031,7 +19442,7 @@ program2.command("config").description("View or update configuration").option("-
18031
19442
  }
18032
19443
  let config2 = {};
18033
19444
  try {
18034
- config2 = JSON.parse(readFileSync4(configPath, "utf-8"));
19445
+ config2 = JSON.parse(readFileSync5(configPath, "utf-8"));
18035
19446
  } catch {}
18036
19447
  const keys = key.split(".");
18037
19448
  let obj = config2;
@@ -18188,6 +19599,51 @@ program2.command("interactive").description("Launch interactive TUI").action(asy
18188
19599
  const projectId = autoProject(globalOpts);
18189
19600
  renderApp2(projectId);
18190
19601
  });
19602
+ program2.command("blame <file>").description("Show which tasks/agents touched a file and why \u2014 combines task_files + task_commits").action((filePath) => {
19603
+ const globalOpts = program2.opts();
19604
+ const { findTasksByFile: findTasksByFile2 } = (init_task_files(), __toCommonJS(exports_task_files));
19605
+ const { getTask: getTask2 } = (init_tasks(), __toCommonJS(exports_tasks));
19606
+ const db = getDatabase();
19607
+ const taskFiles = findTasksByFile2(filePath, db);
19608
+ 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(`%${filePath}%`);
19609
+ if (globalOpts.json) {
19610
+ output({ file: filePath, task_files: taskFiles, commits: commitRows }, true);
19611
+ return;
19612
+ }
19613
+ console.log(chalk.bold(`
19614
+ Blame: ${filePath}
19615
+ `));
19616
+ if (taskFiles.length > 0) {
19617
+ console.log(chalk.bold("Task File Links:"));
19618
+ for (const tf of taskFiles) {
19619
+ const task = getTask2(tf.task_id, db);
19620
+ const title = task ? task.title : "unknown";
19621
+ const sid = task?.short_id || tf.task_id.slice(0, 8);
19622
+ console.log(` ${chalk.cyan(sid)} ${title} \u2014 ${chalk.dim(tf.role || "file")} ${chalk.dim(tf.updated_at)}`);
19623
+ }
19624
+ }
19625
+ if (commitRows.length > 0) {
19626
+ console.log(chalk.bold(`
19627
+ Commit Links (${commitRows.length}):`));
19628
+ for (const c of commitRows) {
19629
+ const sid = c.short_id || c.task_id.slice(0, 8);
19630
+ console.log(` ${chalk.yellow(c.sha?.slice(0, 7) || "?")} ${chalk.cyan(sid)} ${c.title || ""} \u2014 ${chalk.dim(c.author || "")} ${chalk.dim(c.committed_at || "")}`);
19631
+ }
19632
+ }
19633
+ if (taskFiles.length === 0 && commitRows.length === 0) {
19634
+ console.log(chalk.dim("No task or commit links found for this file."));
19635
+ console.log(chalk.dim("Use 'todos hook install' to auto-link future commits."));
19636
+ }
19637
+ console.log();
19638
+ });
19639
+ program2.command("dashboard").description("Live-updating dashboard showing project health, agents, task flow").option("--project <id>", "Filter to project").option("--refresh <ms>", "Refresh interval in ms (default: 2000)", "2000").action(async (opts) => {
19640
+ const { render: render2 } = await import("ink");
19641
+ const React = await import("react");
19642
+ const { Dashboard: Dashboard2 } = await Promise.resolve().then(() => (init_Dashboard(), exports_Dashboard));
19643
+ const globalOpts = program2.opts();
19644
+ const projectId = opts.project || autoProject(globalOpts) || undefined;
19645
+ render2(React.createElement(Dashboard2, { projectId, refreshMs: parseInt(opts.refresh, 10) }));
19646
+ });
18191
19647
  program2.command("next").description("Show the best pending task to work on next").option("--agent <id>", "Prefer tasks assigned to this agent").option("--project <id>", "Filter to project").option("--json", "Output as JSON").action(async (opts) => {
18192
19648
  const db = getDatabase();
18193
19649
  const filters = {};
@@ -18253,6 +19709,120 @@ Next up:`));
18253
19709
  console.log(` ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${chalk.yellow(t.priority)} ${t.title}`);
18254
19710
  }
18255
19711
  });
19712
+ program2.command("recap").description("Show what happened in the last N hours \u2014 completed tasks, new tasks, agent activity, blockers").option("--hours <n>", "Look back N hours (default: 8)", "8").option("--project <id>", "Filter to project").action((opts) => {
19713
+ const globalOpts = program2.opts();
19714
+ const { getRecap: getRecap2 } = (init_audit(), __toCommonJS(exports_audit));
19715
+ const recap = getRecap2(parseInt(opts.hours, 10), opts.project);
19716
+ if (globalOpts.json) {
19717
+ output(recap, true);
19718
+ return;
19719
+ }
19720
+ console.log(chalk.bold(`
19721
+ Recap \u2014 last ${recap.hours} hours (since ${new Date(recap.since).toLocaleString()})
19722
+ `));
19723
+ if (recap.completed.length > 0) {
19724
+ console.log(chalk.green.bold(`Completed (${recap.completed.length}):`));
19725
+ for (const t of recap.completed) {
19726
+ const id = t.short_id || t.id.slice(0, 8);
19727
+ const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
19728
+ console.log(` ${chalk.green("\u2713")} ${chalk.cyan(id)} ${t.title}${dur}${t.assigned_to ? ` \u2014 ${chalk.dim(t.assigned_to)}` : ""}`);
19729
+ }
19730
+ } else {
19731
+ console.log(chalk.dim("No tasks completed in this period."));
19732
+ }
19733
+ if (recap.in_progress.length > 0) {
19734
+ console.log(chalk.blue.bold(`
19735
+ In Progress (${recap.in_progress.length}):`));
19736
+ for (const t of recap.in_progress) {
19737
+ const id = t.short_id || t.id.slice(0, 8);
19738
+ console.log(` ${chalk.blue("\u2192")} ${chalk.cyan(id)} ${t.title}${t.assigned_to ? ` \u2014 ${chalk.dim(t.assigned_to)}` : ""}`);
19739
+ }
19740
+ }
19741
+ if (recap.blocked.length > 0) {
19742
+ console.log(chalk.red.bold(`
19743
+ Blocked (${recap.blocked.length}):`));
19744
+ for (const t of recap.blocked) {
19745
+ const id = t.short_id || t.id.slice(0, 8);
19746
+ console.log(` ${chalk.red("\u2717")} ${chalk.cyan(id)} ${t.title}`);
19747
+ }
19748
+ }
19749
+ if (recap.stale.length > 0) {
19750
+ console.log(chalk.yellow.bold(`
19751
+ Stale (${recap.stale.length}):`));
19752
+ for (const t of recap.stale) {
19753
+ const id = t.short_id || t.id.slice(0, 8);
19754
+ const ago = Math.round((Date.now() - new Date(t.updated_at).getTime()) / 60000);
19755
+ console.log(` ${chalk.yellow("!")} ${chalk.cyan(id)} ${t.title} \u2014 last update ${ago}m ago`);
19756
+ }
19757
+ }
19758
+ if (recap.created.length > 0) {
19759
+ console.log(chalk.dim.bold(`
19760
+ Created (${recap.created.length}):`));
19761
+ for (const t of recap.created.slice(0, 10)) {
19762
+ const id = t.short_id || t.id.slice(0, 8);
19763
+ console.log(` ${chalk.dim("+")} ${chalk.cyan(id)} ${t.title}`);
19764
+ }
19765
+ if (recap.created.length > 10)
19766
+ console.log(chalk.dim(` ... and ${recap.created.length - 10} more`));
19767
+ }
19768
+ if (recap.agents.length > 0) {
19769
+ console.log(chalk.bold(`
19770
+ Agents:`));
19771
+ for (const a of recap.agents) {
19772
+ const seen = Math.round((Date.now() - new Date(a.last_seen_at).getTime()) / 60000);
19773
+ console.log(` ${a.name}: ${chalk.green(a.completed_count + " done")} | ${chalk.blue(a.in_progress_count + " active")} | last seen ${seen}m ago`);
19774
+ }
19775
+ }
19776
+ console.log();
19777
+ });
19778
+ program2.command("standup").description("Generate standup notes \u2014 completed since yesterday, in progress, blocked. Grouped by agent.").option("--since <date>", "ISO date or 'yesterday' (default: yesterday)").option("--project <id>", "Filter to project").action((opts) => {
19779
+ const globalOpts = program2.opts();
19780
+ const sinceDate = opts.since === "yesterday" || !opts.since ? new Date(Date.now() - 24 * 60 * 60 * 1000) : new Date(opts.since);
19781
+ const hours = Math.max(1, Math.round((Date.now() - sinceDate.getTime()) / (60 * 60 * 1000)));
19782
+ const { getRecap: getRecap2 } = (init_audit(), __toCommonJS(exports_audit));
19783
+ const recap = getRecap2(hours, opts.project);
19784
+ if (globalOpts.json) {
19785
+ output(recap, true);
19786
+ return;
19787
+ }
19788
+ console.log(chalk.bold(`
19789
+ Standup \u2014 since ${sinceDate.toLocaleDateString()}
19790
+ `));
19791
+ const byAgent = new Map;
19792
+ for (const t of recap.completed) {
19793
+ const agent = t.assigned_to || "unassigned";
19794
+ if (!byAgent.has(agent))
19795
+ byAgent.set(agent, []);
19796
+ byAgent.get(agent).push(t);
19797
+ }
19798
+ if (byAgent.size > 0) {
19799
+ console.log(chalk.green.bold("Done:"));
19800
+ for (const [agent, tasks] of byAgent) {
19801
+ console.log(` ${chalk.cyan(agent)}:`);
19802
+ for (const t of tasks) {
19803
+ const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
19804
+ console.log(` ${chalk.green("\u2713")} ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}`);
19805
+ }
19806
+ }
19807
+ } else {
19808
+ console.log(chalk.dim("Nothing completed."));
19809
+ }
19810
+ if (recap.in_progress.length > 0) {
19811
+ console.log(chalk.blue.bold(`
19812
+ In Progress:`));
19813
+ for (const t of recap.in_progress) {
19814
+ console.log(` ${chalk.blue("\u2192")} ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${chalk.dim(t.assigned_to)}` : ""}`);
19815
+ }
19816
+ }
19817
+ if (recap.blocked.length > 0) {
19818
+ console.log(chalk.red.bold(`
19819
+ Blocked:`));
19820
+ for (const t of recap.blocked) {
19821
+ console.log(` ${chalk.red("\u2717")} ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
19822
+ }
19823
+ }
19824
+ console.log();
19825
+ });
18256
19826
  program2.command("fail <id>").description("Mark a task as failed with optional reason and retry").option("--reason <text>", "Why it failed").option("--agent <id>", "Agent reporting the failure").option("--retry", "Auto-create a retry copy").option("--json", "Output as JSON").action(async (id, opts) => {
18257
19827
  const db = getDatabase();
18258
19828
  const resolvedId = resolvePartialId(db, "tasks", id);
@@ -18541,11 +20111,11 @@ program2.command("health").description("Check todos system health \u2014 databas
18541
20111
  try {
18542
20112
  const db = getDatabase();
18543
20113
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
18544
- const { statSync: statSync2 } = __require("fs");
20114
+ const { statSync: statSync3 } = __require("fs");
18545
20115
  const dbPath = process.env["TODOS_DB_PATH"] || __require("path").join(process.env["HOME"] || "~", ".todos", "todos.db");
18546
20116
  let size = "unknown";
18547
20117
  try {
18548
- size = `${(statSync2(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
20118
+ size = `${(statSync3(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
18549
20119
  } catch {}
18550
20120
  checks.push({ name: "Database", ok: true, message: `${row.count} tasks \xB7 ${size} \xB7 ${chalk.dim(dbPath)}` });
18551
20121
  } catch (e) {