@hasna/todos 0.10.19 → 0.10.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,15 @@ 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);
2959
+ `,
2960
+ `
2961
+ ALTER TABLE tasks ADD COLUMN task_type TEXT;
2962
+ CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type);
2963
+ INSERT OR IGNORE INTO _migrations (id) VALUES (35);
2944
2964
  `
2945
2965
  ];
2946
2966
  });
@@ -3413,7 +3433,8 @@ var exports_audit = {};
3413
3433
  __export(exports_audit, {
3414
3434
  logTaskChange: () => logTaskChange,
3415
3435
  getTaskHistory: () => getTaskHistory,
3416
- getRecentActivity: () => getRecentActivity
3436
+ getRecentActivity: () => getRecentActivity,
3437
+ getRecap: () => getRecap
3417
3438
  });
3418
3439
  function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
3419
3440
  const d = db || getDatabase();
@@ -3431,6 +3452,32 @@ function getRecentActivity(limit = 50, db) {
3431
3452
  const d = db || getDatabase();
3432
3453
  return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
3433
3454
  }
3455
+ function getRecap(hours = 8, projectId, db) {
3456
+ const d = db || getDatabase();
3457
+ const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
3458
+ const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
3459
+ const pf = projectId ? " AND project_id = ?" : "";
3460
+ const tpf = projectId ? " AND t.project_id = ?" : "";
3461
+ 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);
3462
+ 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);
3463
+ 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();
3464
+ 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();
3465
+ 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);
3466
+ 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);
3467
+ return {
3468
+ hours,
3469
+ since,
3470
+ completed: completed.map((r) => ({
3471
+ ...r,
3472
+ duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
3473
+ })),
3474
+ created,
3475
+ in_progress,
3476
+ blocked,
3477
+ stale,
3478
+ agents
3479
+ };
3480
+ }
3434
3481
  var init_audit = __esm(() => {
3435
3482
  init_database();
3436
3483
  });
@@ -3773,8 +3820,8 @@ function createTask(input, db) {
3773
3820
  const title = shortId ? `${shortId}: ${input.title}` : input.title;
3774
3821
  const assignedBy = input.assigned_by || input.agent_id;
3775
3822
  const assignedFromProject = input.assigned_from_project || null;
3776
- d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project)
3777
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3823
+ d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type)
3824
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3778
3825
  id,
3779
3826
  shortId,
3780
3827
  input.project_id || null,
@@ -3804,7 +3851,8 @@ function createTask(input, db) {
3804
3851
  input.reason || null,
3805
3852
  input.spawned_from_session || null,
3806
3853
  assignedBy || null,
3807
- assignedFromProject || null
3854
+ assignedFromProject || null,
3855
+ input.task_type || null
3808
3856
  ]);
3809
3857
  if (tags.length > 0) {
3810
3858
  insertTaskTags(id, tags, d);
@@ -3913,6 +3961,15 @@ function listTasks(filter = {}, db) {
3913
3961
  } else if (filter.has_recurrence === false) {
3914
3962
  conditions.push("recurrence_rule IS NULL");
3915
3963
  }
3964
+ if (filter.task_type) {
3965
+ if (Array.isArray(filter.task_type)) {
3966
+ conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
3967
+ params.push(...filter.task_type);
3968
+ } else {
3969
+ conditions.push("task_type = ?");
3970
+ params.push(filter.task_type);
3971
+ }
3972
+ }
3916
3973
  const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
3917
3974
  if (filter.cursor) {
3918
3975
  try {
@@ -4072,6 +4129,10 @@ function updateTask(id, input, db) {
4072
4129
  sets.push("recurrence_rule = ?");
4073
4130
  params.push(input.recurrence_rule);
4074
4131
  }
4132
+ if (input.task_type !== undefined) {
4133
+ sets.push("task_type = ?");
4134
+ params.push(input.task_type ?? null);
4135
+ }
4075
4136
  params.push(id, input.version);
4076
4137
  const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
4077
4138
  if (result.changes === 0) {
@@ -4141,8 +4202,8 @@ function startTask(id, agentId, db) {
4141
4202
  }
4142
4203
  const cutoff = lockExpiryCutoff();
4143
4204
  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]);
4205
+ 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 = ?
4206
+ WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
4146
4207
  if (result.changes === 0) {
4147
4208
  if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
4148
4209
  throw new LockError(id, task.locked_by);
@@ -4150,7 +4211,7 @@ function startTask(id, agentId, db) {
4150
4211
  }
4151
4212
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
4152
4213
  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 };
4214
+ 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
4215
  }
4155
4216
  function completeTask(id, agentId, db, options) {
4156
4217
  const d = db || getDatabase();
@@ -4801,6 +4862,7 @@ __export(exports_agents, {
4801
4862
  updateAgentActivity: () => updateAgentActivity,
4802
4863
  updateAgent: () => updateAgent,
4803
4864
  unarchiveAgent: () => unarchiveAgent,
4865
+ releaseAgent: () => releaseAgent,
4804
4866
  registerAgent: () => registerAgent,
4805
4867
  matchCapabilities: () => matchCapabilities,
4806
4868
  listAgents: () => listAgents,
@@ -4812,10 +4874,29 @@ __export(exports_agents, {
4812
4874
  getAgentByName: () => getAgentByName,
4813
4875
  getAgent: () => getAgent,
4814
4876
  deleteAgent: () => deleteAgent,
4877
+ autoReleaseStaleAgents: () => autoReleaseStaleAgents,
4815
4878
  archiveAgent: () => archiveAgent
4816
4879
  });
4880
+ function getActiveWindowMs() {
4881
+ const env = process.env["TODOS_AGENT_TIMEOUT_MS"];
4882
+ if (env) {
4883
+ const parsed = parseInt(env, 10);
4884
+ if (!isNaN(parsed) && parsed > 0)
4885
+ return parsed;
4886
+ }
4887
+ return 30 * 60 * 1000;
4888
+ }
4889
+ function autoReleaseStaleAgents(db) {
4890
+ if (process.env["TODOS_AGENT_AUTO_RELEASE"] !== "true")
4891
+ return 0;
4892
+ const d = db || getDatabase();
4893
+ const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
4894
+ const result = d.run("UPDATE agents SET session_id = NULL WHERE status = 'active' AND session_id IS NOT NULL AND last_seen_at < ?", [cutoff]);
4895
+ return result.changes;
4896
+ }
4817
4897
  function getAvailableNamesFromPool(pool, db) {
4818
- const cutoff = new Date(Date.now() - AGENT_ACTIVE_WINDOW_MS).toISOString();
4898
+ autoReleaseStaleAgents(db);
4899
+ const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
4819
4900
  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
4901
  return pool.filter((name) => !activeNames.has(name.toLowerCase()));
4821
4902
  }
@@ -4837,22 +4918,19 @@ function registerAgent(input, db) {
4837
4918
  const existing = getAgentByName(normalizedName, d);
4838
4919
  if (existing) {
4839
4920
  const lastSeenMs = new Date(existing.last_seen_at).getTime();
4840
- const isActive = Date.now() - lastSeenMs < AGENT_ACTIVE_WINDOW_MS;
4921
+ const activeWindowMs = getActiveWindowMs();
4922
+ const isActive = Date.now() - lastSeenMs < activeWindowMs;
4841
4923
  const sameSession = input.session_id && existing.session_id && input.session_id === existing.session_id;
4842
4924
  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
- };
4925
+ const callerHasNoSession = !input.session_id;
4926
+ const existingHasActiveSession = existing.session_id && isActive;
4927
+ if (!input.force) {
4928
+ if (isActive && differentSession) {
4929
+ return buildConflictError(existing, lastSeenMs, input.pool, d);
4930
+ }
4931
+ if (callerHasNoSession && existingHasActiveSession) {
4932
+ return buildConflictError(existing, lastSeenMs, input.pool, d);
4933
+ }
4856
4934
  }
4857
4935
  const updates = ["last_seen_at = ?", "status = 'active'"];
4858
4936
  const params = [now()];
@@ -4897,6 +4975,32 @@ function registerAgent(input, db) {
4897
4975
  function isAgentConflict(result) {
4898
4976
  return result.conflict === true;
4899
4977
  }
4978
+ function buildConflictError(existing, lastSeenMs, pool, d) {
4979
+ const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
4980
+ const suggestions = pool ? getAvailableNamesFromPool(pool, d) : [];
4981
+ return {
4982
+ conflict: true,
4983
+ existing_id: existing.id,
4984
+ existing_name: existing.name,
4985
+ last_seen_at: existing.last_seen_at,
4986
+ session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
4987
+ working_dir: existing.working_dir,
4988
+ suggestions: suggestions.slice(0, 5),
4989
+ 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(", ")}` : ""}`
4990
+ };
4991
+ }
4992
+ function releaseAgent(id, session_id, db) {
4993
+ const d = db || getDatabase();
4994
+ const agent = getAgent(id, d);
4995
+ if (!agent)
4996
+ return false;
4997
+ if (session_id && agent.session_id && agent.session_id !== session_id) {
4998
+ return false;
4999
+ }
5000
+ const epoch = new Date(0).toISOString();
5001
+ d.run("UPDATE agents SET session_id = NULL, last_seen_at = ? WHERE id = ?", [epoch, id]);
5002
+ return true;
5003
+ }
4900
5004
  function getAgent(id, db) {
4901
5005
  const d = db || getDatabase();
4902
5006
  const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
@@ -4917,6 +5021,7 @@ function listAgents(opts, db) {
4917
5021
  includeArchived = opts?.include_archived ?? false;
4918
5022
  d = db || getDatabase();
4919
5023
  }
5024
+ autoReleaseStaleAgents(d);
4920
5025
  if (includeArchived) {
4921
5026
  return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
4922
5027
  }
@@ -4934,8 +5039,19 @@ function updateAgent(id, input, db) {
4934
5039
  const sets = ["last_seen_at = ?"];
4935
5040
  const params = [now()];
4936
5041
  if (input.name !== undefined) {
5042
+ const newName = input.name.trim().toLowerCase();
5043
+ const holder = getAgentByName(newName, d);
5044
+ if (holder && holder.id !== id) {
5045
+ const lastSeenMs = new Date(holder.last_seen_at).getTime();
5046
+ const isActive = Date.now() - lastSeenMs < getActiveWindowMs();
5047
+ if (isActive && holder.status === "active") {
5048
+ throw new Error(`Cannot rename: name "${newName}" is held by active agent ${holder.id} (last seen ${Math.round((Date.now() - lastSeenMs) / 60000)}m ago)`);
5049
+ }
5050
+ const evictedName = `${holder.name}__evicted_${holder.id}`;
5051
+ d.run("UPDATE agents SET name = ? WHERE id = ?", [evictedName, holder.id]);
5052
+ }
4937
5053
  sets.push("name = ?");
4938
- params.push(input.name.trim().toLowerCase());
5054
+ params.push(newName);
4939
5055
  }
4940
5056
  if (input.description !== undefined) {
4941
5057
  sets.push("description = ?");
@@ -5032,10 +5148,8 @@ function getCapableAgents(capabilities, opts, db) {
5032
5148
  })).filter((entry) => entry.score >= minScore).sort((a, b) => b.score - a.score);
5033
5149
  return opts?.limit ? scored.slice(0, opts.limit) : scored;
5034
5150
  }
5035
- var AGENT_ACTIVE_WINDOW_MS;
5036
5151
  var init_agents = __esm(() => {
5037
5152
  init_database();
5038
- AGENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
5039
5153
  });
5040
5154
 
5041
5155
  // src/db/task-lists.ts
@@ -5867,133 +5981,549 @@ var init_sync = __esm(() => {
5867
5981
  init_config();
5868
5982
  });
5869
5983
 
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;
5984
+ // src/db/task-files.ts
5985
+ var exports_task_files = {};
5986
+ __export(exports_task_files, {
5987
+ updateTaskFileStatus: () => updateTaskFileStatus,
5988
+ removeTaskFile: () => removeTaskFile,
5989
+ listTaskFiles: () => listTaskFiles,
5990
+ listActiveFiles: () => listActiveFiles,
5991
+ getTaskFile: () => getTaskFile,
5992
+ getFileHeatMap: () => getFileHeatMap,
5993
+ findTasksByFile: () => findTasksByFile,
5994
+ detectFileConflicts: () => detectFileConflicts,
5995
+ bulkFindTasksByFiles: () => bulkFindTasksByFiles,
5996
+ bulkAddTaskFiles: () => bulkAddTaskFiles,
5997
+ addTaskFile: () => addTaskFile
5998
+ });
5999
+ function addTaskFile(input, db) {
6000
+ const d = db || getDatabase();
6001
+ const id = uuid();
6002
+ const timestamp = now();
6003
+ const existing = d.query("SELECT id FROM task_files WHERE task_id = ? AND path = ?").get(input.task_id, input.path);
6004
+ if (existing) {
6005
+ 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]);
6006
+ return getTaskFile(existing.id, d);
5910
6007
  }
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
- };
6008
+ d.run(`INSERT INTO task_files (id, task_id, path, status, agent_id, note, created_at, updated_at)
6009
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.path, input.status || "active", input.agent_id || null, input.note || null, timestamp, timestamp]);
6010
+ return getTaskFile(id, d);
6011
+ }
6012
+ function getTaskFile(id, db) {
6013
+ const d = db || getDatabase();
6014
+ return d.query("SELECT * FROM task_files WHERE id = ?").get(id);
6015
+ }
6016
+ function listTaskFiles(taskId, db) {
6017
+ const d = db || getDatabase();
6018
+ return d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY path").all(taskId);
6019
+ }
6020
+ function findTasksByFile(path, db) {
6021
+ const d = db || getDatabase();
6022
+ return d.query("SELECT * FROM task_files WHERE path = ? AND status != 'removed' ORDER BY updated_at DESC").all(path);
6023
+ }
6024
+ function updateTaskFileStatus(taskId, path, status, agentId, db) {
6025
+ const d = db || getDatabase();
6026
+ const timestamp = now();
6027
+ 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]);
6028
+ const row = d.query("SELECT * FROM task_files WHERE task_id = ? AND path = ?").get(taskId, path);
6029
+ return row;
6030
+ }
6031
+ function removeTaskFile(taskId, path, db) {
6032
+ const d = db || getDatabase();
6033
+ const result = d.run("DELETE FROM task_files WHERE task_id = ? AND path = ?", [taskId, path]);
6034
+ return result.changes > 0;
6035
+ }
6036
+ function detectFileConflicts(taskId, paths, db) {
6037
+ const d = db || getDatabase();
6038
+ if (paths.length === 0)
6039
+ return [];
6040
+ const placeholders = paths.map(() => "?").join(", ");
6041
+ const rows = d.query(`
6042
+ SELECT tf.path, tf.agent_id AS conflicting_agent_id, t.id AS conflicting_task_id,
6043
+ t.title AS conflicting_task_title, t.status AS conflicting_task_status
6044
+ FROM task_files tf
6045
+ JOIN tasks t ON tf.task_id = t.id
6046
+ WHERE tf.path IN (${placeholders})
6047
+ AND tf.task_id != ?
6048
+ AND tf.status != 'removed'
6049
+ AND t.status = 'in_progress'
6050
+ ORDER BY tf.updated_at DESC
6051
+ `).all(...paths, taskId);
6052
+ return rows;
6053
+ }
6054
+ function bulkFindTasksByFiles(paths, db) {
6055
+ const d = db || getDatabase();
6056
+ if (paths.length === 0)
6057
+ return [];
6058
+ const placeholders = paths.map(() => "?").join(", ");
6059
+ const rows = d.query(`SELECT tf.*, t.status AS task_status FROM task_files tf
6060
+ JOIN tasks t ON tf.task_id = t.id
6061
+ WHERE tf.path IN (${placeholders}) AND tf.status != 'removed'
6062
+ ORDER BY tf.updated_at DESC`).all(...paths);
6063
+ const byPath = new Map;
6064
+ for (const path of paths)
6065
+ byPath.set(path, []);
6066
+ for (const row of rows) {
6067
+ byPath.get(row.path)?.push(row);
6068
+ }
6069
+ return paths.map((path) => {
6070
+ const tasks = byPath.get(path) ?? [];
6071
+ const inProgressCount = tasks.filter((t) => t.task_status === "in_progress").length;
6072
+ return {
6073
+ path,
6074
+ tasks,
6075
+ has_conflict: inProgressCount > 1,
6076
+ in_progress_count: inProgressCount
5975
6077
  };
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",
6078
+ });
6079
+ }
6080
+ function listActiveFiles(db) {
6081
+ const d = db || getDatabase();
6082
+ return d.query(`
6083
+ SELECT
6084
+ tf.path,
6085
+ tf.status AS file_status,
6086
+ tf.agent_id AS file_agent_id,
6087
+ tf.note,
6088
+ tf.updated_at,
6089
+ t.id AS task_id,
6090
+ t.short_id AS task_short_id,
6091
+ t.title AS task_title,
6092
+ t.status AS task_status,
6093
+ t.locked_by AS task_locked_by,
6094
+ t.locked_at AS task_locked_at,
6095
+ a.id AS agent_id,
6096
+ a.name AS agent_name
6097
+ FROM task_files tf
6098
+ JOIN tasks t ON tf.task_id = t.id
6099
+ LEFT JOIN agents a ON (tf.agent_id = a.id OR (tf.agent_id IS NULL AND t.assigned_to = a.id))
6100
+ WHERE t.status = 'in_progress'
6101
+ AND tf.status != 'removed'
6102
+ ORDER BY tf.updated_at DESC
6103
+ `).all();
6104
+ }
6105
+ function getFileHeatMap(opts, db) {
6106
+ const d = db || getDatabase();
6107
+ const limit = opts?.limit ?? 20;
6108
+ const minEdits = opts?.min_edits ?? 1;
6109
+ const rows = d.query(`
6110
+ SELECT
6111
+ tf.path,
6112
+ COUNT(*) AS edit_count,
6113
+ COUNT(DISTINCT COALESCE(tf.agent_id, t.assigned_to)) AS unique_agents,
6114
+ GROUP_CONCAT(DISTINCT COALESCE(tf.agent_id, t.assigned_to)) AS agent_ids,
6115
+ MAX(tf.updated_at) AS last_edited_at,
6116
+ SUM(CASE WHEN t.status = 'in_progress' THEN 1 ELSE 0 END) AS active_task_count
6117
+ FROM task_files tf
6118
+ JOIN tasks t ON tf.task_id = t.id
6119
+ WHERE tf.status != 'removed'
6120
+ ${opts?.project_id ? `AND t.project_id = '${opts.project_id}'` : ""}
6121
+ GROUP BY tf.path
6122
+ HAVING edit_count >= ${minEdits}
6123
+ ORDER BY edit_count DESC, last_edited_at DESC
6124
+ LIMIT ${limit}
6125
+ `).all();
6126
+ return rows.map((r) => ({
6127
+ path: r.path,
6128
+ edit_count: r.edit_count,
6129
+ unique_agents: r.unique_agents,
6130
+ agent_ids: r.agent_ids ? r.agent_ids.split(",").filter(Boolean) : [],
6131
+ last_edited_at: r.last_edited_at,
6132
+ active_task_count: r.active_task_count
6133
+ }));
6134
+ }
6135
+ function bulkAddTaskFiles(taskId, paths, agentId, db) {
6136
+ const d = db || getDatabase();
6137
+ const results = [];
6138
+ const tx = d.transaction(() => {
6139
+ for (const path of paths) {
6140
+ results.push(addTaskFile({ task_id: taskId, path, agent_id: agentId }, d));
6141
+ }
6142
+ });
6143
+ tx();
6144
+ return results;
6145
+ }
6146
+ var init_task_files = __esm(() => {
6147
+ init_database();
6148
+ });
6149
+
6150
+ // src/db/task-commits.ts
6151
+ var exports_task_commits = {};
6152
+ __export(exports_task_commits, {
6153
+ unlinkTaskCommit: () => unlinkTaskCommit,
6154
+ linkTaskToCommit: () => linkTaskToCommit,
6155
+ getTaskCommits: () => getTaskCommits,
6156
+ findTaskByCommit: () => findTaskByCommit
6157
+ });
6158
+ function rowToCommit(row) {
6159
+ return {
6160
+ ...row,
6161
+ files_changed: row.files_changed ? JSON.parse(row.files_changed) : null
6162
+ };
6163
+ }
6164
+ function linkTaskToCommit(input, db) {
6165
+ const d = db || getDatabase();
6166
+ const existing = d.query("SELECT * FROM task_commits WHERE task_id = ? AND sha = ?").get(input.task_id, input.sha);
6167
+ if (existing) {
6168
+ 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]);
6169
+ return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(existing.id));
6170
+ }
6171
+ const id = uuid();
6172
+ 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()]);
6173
+ return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(id));
6174
+ }
6175
+ function getTaskCommits(taskId, db) {
6176
+ const d = db || getDatabase();
6177
+ return d.query("SELECT * FROM task_commits WHERE task_id = ? ORDER BY committed_at DESC, created_at DESC").all(taskId).map(rowToCommit);
6178
+ }
6179
+ function findTaskByCommit(sha, db) {
6180
+ const d = db || getDatabase();
6181
+ const row = d.query("SELECT * FROM task_commits WHERE sha = ? OR sha LIKE ? LIMIT 1").get(sha, `${sha}%`);
6182
+ if (!row)
6183
+ return null;
6184
+ return { task_id: row.task_id, commit: rowToCommit(row) };
6185
+ }
6186
+ function unlinkTaskCommit(taskId, sha, db) {
6187
+ const d = db || getDatabase();
6188
+ return d.run("DELETE FROM task_commits WHERE task_id = ? AND (sha = ? OR sha LIKE ?)", [taskId, sha, `${sha}%`]).changes > 0;
6189
+ }
6190
+ var init_task_commits = __esm(() => {
6191
+ init_database();
6192
+ });
6193
+
6194
+ // src/lib/extract.ts
6195
+ var exports_extract = {};
6196
+ __export(exports_extract, {
6197
+ tagToPriority: () => tagToPriority,
6198
+ extractTodos: () => extractTodos,
6199
+ extractFromSource: () => extractFromSource,
6200
+ EXTRACT_TAGS: () => EXTRACT_TAGS
6201
+ });
6202
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
6203
+ import { relative, resolve as resolve2, join as join6 } from "path";
6204
+ function tagToPriority(tag) {
6205
+ switch (tag) {
6206
+ case "BUG":
6207
+ case "FIXME":
6208
+ return "high";
6209
+ case "HACK":
6210
+ case "XXX":
6211
+ return "medium";
6212
+ case "TODO":
6213
+ return "medium";
6214
+ case "NOTE":
6215
+ return "low";
6216
+ }
6217
+ }
6218
+ function buildTagRegex(tags) {
6219
+ const tagPattern = tags.join("|");
6220
+ return new RegExp(`(?:^|\\s)(?:\\/\\/|\\/\\*|#|\\*|--|;;|%|<!--|\\{-)\\s*(?:@?)(${tagPattern})\\s*[:(]?\\s*(.*)$`, "i");
6221
+ }
6222
+ function extractFromSource(source, filePath, tags = [...EXTRACT_TAGS]) {
6223
+ const regex = buildTagRegex(tags);
6224
+ const results = [];
6225
+ const lines = source.split(`
6226
+ `);
6227
+ for (let i = 0;i < lines.length; i++) {
6228
+ const line = lines[i];
6229
+ const match = line.match(regex);
6230
+ if (match) {
6231
+ const tag = match[1].toUpperCase();
6232
+ let message = match[2].trim();
6233
+ message = message.replace(/\s*\*\/\s*$/, "").replace(/\s*-->\s*$/, "").replace(/\s*-\}\s*$/, "").trim();
6234
+ if (message) {
6235
+ results.push({
6236
+ tag,
6237
+ message,
6238
+ file: filePath,
6239
+ line: i + 1,
6240
+ raw: line
6241
+ });
6242
+ }
6243
+ }
6244
+ }
6245
+ return results;
6246
+ }
6247
+ function collectFiles(basePath, extensions) {
6248
+ const stat = statSync2(basePath);
6249
+ if (stat.isFile()) {
6250
+ return [basePath];
6251
+ }
6252
+ const glob = new Bun.Glob("**/*");
6253
+ const files = [];
6254
+ for (const entry of glob.scanSync({ cwd: basePath, onlyFiles: true, dot: false })) {
6255
+ const parts = entry.split("/");
6256
+ if (parts.some((p) => SKIP_DIRS.has(p)))
6257
+ continue;
6258
+ const dotIdx = entry.lastIndexOf(".");
6259
+ if (dotIdx === -1)
6260
+ continue;
6261
+ const ext = entry.slice(dotIdx);
6262
+ if (!extensions.has(ext))
6263
+ continue;
6264
+ files.push(entry);
6265
+ }
6266
+ return files.sort();
6267
+ }
6268
+ function extractTodos(options, db) {
6269
+ const basePath = resolve2(options.path);
6270
+ const tags = options.patterns || [...EXTRACT_TAGS];
6271
+ const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
6272
+ const files = collectFiles(basePath, extensions);
6273
+ const allComments = [];
6274
+ for (const file of files) {
6275
+ const fullPath = statSync2(basePath).isFile() ? basePath : join6(basePath, file);
6276
+ try {
6277
+ const source = readFileSync3(fullPath, "utf-8");
6278
+ const relPath = statSync2(basePath).isFile() ? relative(resolve2(basePath, ".."), fullPath) : file;
6279
+ const comments = extractFromSource(source, relPath, tags);
6280
+ allComments.push(...comments);
6281
+ } catch {}
6282
+ }
6283
+ if (options.dry_run) {
6284
+ return { comments: allComments, tasks: [], skipped: 0 };
6285
+ }
6286
+ const tasks = [];
6287
+ let skipped = 0;
6288
+ const existingTasks = options.project_id ? listTasks({ project_id: options.project_id, tags: ["extracted"] }, db) : listTasks({ tags: ["extracted"] }, db);
6289
+ const existingKeys = new Set;
6290
+ for (const t of existingTasks) {
6291
+ const meta = t.metadata;
6292
+ if (meta?.["source_file"] && meta?.["source_line"]) {
6293
+ existingKeys.add(`${meta["source_file"]}:${meta["source_line"]}`);
6294
+ }
6295
+ }
6296
+ for (const comment of allComments) {
6297
+ const dedupKey = `${comment.file}:${comment.line}`;
6298
+ if (existingKeys.has(dedupKey)) {
6299
+ skipped++;
6300
+ continue;
6301
+ }
6302
+ const taskTags = ["extracted", comment.tag.toLowerCase(), ...options.tags || []];
6303
+ const task = createTask({
6304
+ title: `[${comment.tag}] ${comment.message}`,
6305
+ description: `Extracted from code comment in \`${comment.file}\` at line ${comment.line}:
6306
+ \`\`\`
6307
+ ${comment.raw.trim()}
6308
+ \`\`\``,
6309
+ priority: tagToPriority(comment.tag),
6310
+ project_id: options.project_id,
6311
+ task_list_id: options.task_list_id,
6312
+ assigned_to: options.assigned_to,
6313
+ agent_id: options.agent_id,
6314
+ tags: taskTags,
6315
+ metadata: {
6316
+ source: "code_comment",
6317
+ comment_type: comment.tag,
6318
+ source_file: comment.file,
6319
+ source_line: comment.line
6320
+ }
6321
+ }, db);
6322
+ addTaskFile({
6323
+ task_id: task.id,
6324
+ path: comment.file,
6325
+ note: `Line ${comment.line}: ${comment.tag} comment`
6326
+ }, db);
6327
+ tasks.push(task);
6328
+ existingKeys.add(dedupKey);
6329
+ }
6330
+ return { comments: allComments, tasks, skipped };
6331
+ }
6332
+ var EXTRACT_TAGS, DEFAULT_EXTENSIONS, SKIP_DIRS;
6333
+ var init_extract = __esm(() => {
6334
+ init_tasks();
6335
+ init_task_files();
6336
+ EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
6337
+ DEFAULT_EXTENSIONS = new Set([
6338
+ ".ts",
6339
+ ".tsx",
6340
+ ".js",
6341
+ ".jsx",
6342
+ ".mjs",
6343
+ ".cjs",
6344
+ ".py",
6345
+ ".rb",
6346
+ ".go",
6347
+ ".rs",
6348
+ ".c",
6349
+ ".cpp",
6350
+ ".h",
6351
+ ".hpp",
6352
+ ".java",
6353
+ ".kt",
6354
+ ".swift",
6355
+ ".cs",
6356
+ ".php",
6357
+ ".sh",
6358
+ ".bash",
6359
+ ".zsh",
6360
+ ".lua",
6361
+ ".sql",
6362
+ ".r",
6363
+ ".R",
6364
+ ".yaml",
6365
+ ".yml",
6366
+ ".toml",
6367
+ ".css",
6368
+ ".scss",
6369
+ ".less",
6370
+ ".vue",
6371
+ ".svelte",
6372
+ ".ex",
6373
+ ".exs",
6374
+ ".erl",
6375
+ ".hs",
6376
+ ".ml",
6377
+ ".mli",
6378
+ ".clj",
6379
+ ".cljs"
6380
+ ]);
6381
+ SKIP_DIRS = new Set([
6382
+ "node_modules",
6383
+ ".git",
6384
+ "dist",
6385
+ "build",
6386
+ "out",
6387
+ ".next",
6388
+ ".turbo",
6389
+ "coverage",
6390
+ "__pycache__",
6391
+ ".venv",
6392
+ "venv",
6393
+ "vendor",
6394
+ "target",
6395
+ ".cache",
6396
+ ".parcel-cache"
6397
+ ]);
6398
+ });
6399
+
6400
+ // node_modules/zod/v3/helpers/util.js
6401
+ var util, objectUtil, ZodParsedType, getParsedType = (data) => {
6402
+ const t = typeof data;
6403
+ switch (t) {
6404
+ case "undefined":
6405
+ return ZodParsedType.undefined;
6406
+ case "string":
6407
+ return ZodParsedType.string;
6408
+ case "number":
6409
+ return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number;
6410
+ case "boolean":
6411
+ return ZodParsedType.boolean;
6412
+ case "function":
6413
+ return ZodParsedType.function;
6414
+ case "bigint":
6415
+ return ZodParsedType.bigint;
6416
+ case "symbol":
6417
+ return ZodParsedType.symbol;
6418
+ case "object":
6419
+ if (Array.isArray(data)) {
6420
+ return ZodParsedType.array;
6421
+ }
6422
+ if (data === null) {
6423
+ return ZodParsedType.null;
6424
+ }
6425
+ if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") {
6426
+ return ZodParsedType.promise;
6427
+ }
6428
+ if (typeof Map !== "undefined" && data instanceof Map) {
6429
+ return ZodParsedType.map;
6430
+ }
6431
+ if (typeof Set !== "undefined" && data instanceof Set) {
6432
+ return ZodParsedType.set;
6433
+ }
6434
+ if (typeof Date !== "undefined" && data instanceof Date) {
6435
+ return ZodParsedType.date;
6436
+ }
6437
+ return ZodParsedType.object;
6438
+ default:
6439
+ return ZodParsedType.unknown;
6440
+ }
6441
+ };
6442
+ var init_util = __esm(() => {
6443
+ (function(util2) {
6444
+ util2.assertEqual = (_) => {};
6445
+ function assertIs(_arg) {}
6446
+ util2.assertIs = assertIs;
6447
+ function assertNever(_x) {
6448
+ throw new Error;
6449
+ }
6450
+ util2.assertNever = assertNever;
6451
+ util2.arrayToEnum = (items) => {
6452
+ const obj = {};
6453
+ for (const item of items) {
6454
+ obj[item] = item;
6455
+ }
6456
+ return obj;
6457
+ };
6458
+ util2.getValidEnumValues = (obj) => {
6459
+ const validKeys = util2.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number");
6460
+ const filtered = {};
6461
+ for (const k of validKeys) {
6462
+ filtered[k] = obj[k];
6463
+ }
6464
+ return util2.objectValues(filtered);
6465
+ };
6466
+ util2.objectValues = (obj) => {
6467
+ return util2.objectKeys(obj).map(function(e) {
6468
+ return obj[e];
6469
+ });
6470
+ };
6471
+ util2.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object) => {
6472
+ const keys = [];
6473
+ for (const key in object) {
6474
+ if (Object.prototype.hasOwnProperty.call(object, key)) {
6475
+ keys.push(key);
6476
+ }
6477
+ }
6478
+ return keys;
6479
+ };
6480
+ util2.find = (arr, checker) => {
6481
+ for (const item of arr) {
6482
+ if (checker(item))
6483
+ return item;
6484
+ }
6485
+ return;
6486
+ };
6487
+ util2.isInteger = typeof Number.isInteger === "function" ? (val) => Number.isInteger(val) : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val;
6488
+ function joinValues(array, separator = " | ") {
6489
+ return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator);
6490
+ }
6491
+ util2.joinValues = joinValues;
6492
+ util2.jsonStringifyReplacer = (_, value) => {
6493
+ if (typeof value === "bigint") {
6494
+ return value.toString();
6495
+ }
6496
+ return value;
6497
+ };
6498
+ })(util || (util = {}));
6499
+ (function(objectUtil2) {
6500
+ objectUtil2.mergeShapes = (first, second) => {
6501
+ return {
6502
+ ...first,
6503
+ ...second
6504
+ };
6505
+ };
6506
+ })(objectUtil || (objectUtil = {}));
6507
+ ZodParsedType = util.arrayToEnum([
6508
+ "string",
6509
+ "nan",
6510
+ "number",
6511
+ "integer",
6512
+ "float",
6513
+ "boolean",
6514
+ "date",
6515
+ "bigint",
6516
+ "symbol",
6517
+ "function",
6518
+ "undefined",
6519
+ "null",
6520
+ "array",
6521
+ "object",
6522
+ "unknown",
6523
+ "promise",
6524
+ "void",
6525
+ "never",
6526
+ "map",
5997
6527
  "set"
5998
6528
  ]);
5999
6529
  });
@@ -9833,47 +10363,148 @@ var init_zod = __esm(() => {
9833
10363
  init_external();
9834
10364
  });
9835
10365
 
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
10366
+ // src/lib/github.ts
10367
+ var exports_github = {};
10368
+ __export(exports_github, {
10369
+ parseGitHubUrl: () => parseGitHubUrl,
10370
+ issueToTask: () => issueToTask,
10371
+ fetchGitHubIssue: () => fetchGitHubIssue
9843
10372
  });
9844
- function rowToCommit(row) {
10373
+ import { execSync } from "child_process";
10374
+ function parseGitHubUrl(url) {
10375
+ const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
10376
+ if (!match)
10377
+ return null;
10378
+ return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
10379
+ }
10380
+ function fetchGitHubIssue(owner, repo, number) {
10381
+ const json = execSync(`gh api repos/${owner}/${repo}/issues/${number}`, { encoding: "utf-8", timeout: 15000 });
10382
+ const data = JSON.parse(json);
9845
10383
  return {
9846
- ...row,
9847
- files_changed: row.files_changed ? JSON.parse(row.files_changed) : null
10384
+ number: data.number,
10385
+ title: data.title,
10386
+ body: data.body,
10387
+ labels: (data.labels || []).map((l) => l.name),
10388
+ state: data.state,
10389
+ assignee: data.assignee?.login || null,
10390
+ url: data.html_url
9848
10391
  };
9849
10392
  }
9850
- function linkTaskToCommit(input, db) {
10393
+ function issueToTask(issue, opts) {
10394
+ const labelToPriority = {
10395
+ critical: "critical",
10396
+ "priority:critical": "critical",
10397
+ high: "high",
10398
+ "priority:high": "high",
10399
+ urgent: "high",
10400
+ low: "low",
10401
+ "priority:low": "low"
10402
+ };
10403
+ let priority = "medium";
10404
+ for (const label of issue.labels) {
10405
+ const mapped = labelToPriority[label.toLowerCase()];
10406
+ if (mapped) {
10407
+ priority = mapped;
10408
+ break;
10409
+ }
10410
+ }
10411
+ return {
10412
+ title: `[GH#${issue.number}] ${issue.title}`,
10413
+ description: issue.body ? issue.body.slice(0, 4000) : undefined,
10414
+ tags: issue.labels.slice(0, 10),
10415
+ priority,
10416
+ metadata: { github_url: issue.url, github_number: issue.number, github_state: issue.state },
10417
+ project_id: opts?.project_id,
10418
+ task_list_id: opts?.task_list_id,
10419
+ agent_id: opts?.agent_id
10420
+ };
10421
+ }
10422
+ var init_github = () => {};
10423
+
10424
+ // src/lib/burndown.ts
10425
+ var exports_burndown = {};
10426
+ __export(exports_burndown, {
10427
+ getBurndown: () => getBurndown
10428
+ });
10429
+ function getBurndown(opts, db) {
9851
10430
  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));
10431
+ const conditions = [];
10432
+ const params = [];
10433
+ if (opts.plan_id) {
10434
+ conditions.push("plan_id = ?");
10435
+ params.push(opts.plan_id);
10436
+ }
10437
+ if (opts.project_id) {
10438
+ conditions.push("project_id = ?");
10439
+ params.push(opts.project_id);
10440
+ }
10441
+ if (opts.task_list_id) {
10442
+ conditions.push("task_list_id = ?");
10443
+ params.push(opts.task_list_id);
10444
+ }
10445
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
10446
+ const total = d.query(`SELECT COUNT(*) as c FROM tasks ${where}`).get(...params).c;
10447
+ const completed = d.query(`SELECT COUNT(*) as c FROM tasks ${where}${where ? " AND" : " WHERE"} status = 'completed'`).get(...params).c;
10448
+ 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);
10449
+ const firstTask = d.query(`SELECT MIN(created_at) as min_date FROM tasks ${where}`).get(...params);
10450
+ const startDate = firstTask?.min_date ? new Date(firstTask.min_date) : new Date;
10451
+ const endDate = new Date;
10452
+ const days = [];
10453
+ const totalDays = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)));
10454
+ let cumulative = 0;
10455
+ const completionMap = new Map(completions.map((c) => [c.date, c.count]));
10456
+ const current = new Date(startDate);
10457
+ for (let i = 0;i <= totalDays; i++) {
10458
+ const dateStr = current.toISOString().slice(0, 10);
10459
+ cumulative += completionMap.get(dateStr) || 0;
10460
+ days.push({
10461
+ date: dateStr,
10462
+ completed_cumulative: cumulative,
10463
+ ideal: Math.round(total / totalDays * i)
10464
+ });
10465
+ current.setDate(current.getDate() + 1);
9856
10466
  }
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);
9864
- }
9865
- function findTaskByCommit(sha, db) {
9866
- 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) };
10467
+ const chart = renderBurndownChart(total, days);
10468
+ return { total, completed, remaining: total - completed, days, chart };
9871
10469
  }
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;
10470
+ function renderBurndownChart(total, days) {
10471
+ const height = 12;
10472
+ const width = Math.min(60, days.length);
10473
+ const step = Math.max(1, Math.floor(days.length / width));
10474
+ const sampled = days.filter((_, i) => i % step === 0 || i === days.length - 1).slice(0, width);
10475
+ const lines = [];
10476
+ lines.push(` ${total} \u2524`);
10477
+ for (let row = height - 1;row >= 0; row--) {
10478
+ const threshold = Math.round(total / height * row);
10479
+ let line = "";
10480
+ for (const day of sampled) {
10481
+ const remaining = total - day.completed_cumulative;
10482
+ const idealRemaining = total - day.ideal;
10483
+ if (remaining >= threshold && remaining > threshold - Math.round(total / height)) {
10484
+ line += "\u2588";
10485
+ } else if (idealRemaining >= threshold && idealRemaining > threshold - Math.round(total / height)) {
10486
+ line += "\xB7";
10487
+ } else {
10488
+ line += " ";
10489
+ }
10490
+ }
10491
+ const label = String(threshold).padStart(4);
10492
+ lines.push(`${label} \u2524${line}`);
10493
+ }
10494
+ lines.push(` 0 \u2524${"\u2500".repeat(sampled.length)}`);
10495
+ lines.push(` \u2514${"\u2500".repeat(sampled.length)}`);
10496
+ if (sampled.length > 0) {
10497
+ const first = sampled[0].date.slice(5);
10498
+ const last = sampled[sampled.length - 1].date.slice(5);
10499
+ const pad = sampled.length - first.length - last.length;
10500
+ lines.push(` ${first}${" ".repeat(Math.max(1, pad))}${last}`);
10501
+ }
10502
+ lines.push("");
10503
+ lines.push(` \u2588 actual remaining \xB7 ideal burndown`);
10504
+ return lines.join(`
10505
+ `);
9875
10506
  }
9876
- var init_task_commits = __esm(() => {
10507
+ var init_burndown = __esm(() => {
9877
10508
  init_database();
9878
10509
  });
9879
10510
 
@@ -9921,281 +10552,115 @@ function buildPrompt(task, agents) {
9921
10552
  TASK:
9922
10553
  Title: ${task.title}
9923
10554
  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();
10555
+ Tags: ${task.tags.join(", ") || "none"}
10556
+ Description: ${task.description?.slice(0, 300) || "none"}
10557
+
10558
+ AVAILABLE AGENTS:
10559
+ ${agentList}
10560
+
10561
+ Rules:
10562
+ - Match task tags/content to agent capabilities
10563
+ - Prefer agents with fewer active tasks
10564
+ - Prefer agents whose role fits the task (lead for critical, developer for features, qa for testing)
10565
+ - If no clear match, pick the agent with fewest active tasks
10566
+
10567
+ Respond with ONLY a JSON object: {"agent_name": "<name>", "reason": "<one sentence>"}`;
10155
10568
  }
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
- }));
10569
+ async function callCerebras(prompt, apiKey) {
10570
+ try {
10571
+ const resp = await fetch(CEREBRAS_API_URL, {
10572
+ method: "POST",
10573
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
10574
+ body: JSON.stringify({
10575
+ model: CEREBRAS_MODEL,
10576
+ messages: [{ role: "user", content: prompt }],
10577
+ max_tokens: 150,
10578
+ temperature: 0
10579
+ }),
10580
+ signal: AbortSignal.timeout(1e4)
10581
+ });
10582
+ if (!resp.ok)
10583
+ return null;
10584
+ const data = await resp.json();
10585
+ const content = data?.choices?.[0]?.message?.content?.trim();
10586
+ if (!content)
10587
+ return null;
10588
+ const match = content.match(/\{[^}]+\}/s);
10589
+ if (!match)
10590
+ return null;
10591
+ return JSON.parse(match[0]);
10592
+ } catch {
10593
+ return null;
10594
+ }
10185
10595
  }
10186
- function bulkAddTaskFiles(taskId, paths, agentId, db) {
10596
+ async function autoAssignTask(taskId, db) {
10187
10597
  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));
10598
+ const task = getTask(taskId, d);
10599
+ if (!task)
10600
+ throw new Error(`Task ${taskId} not found`);
10601
+ const agents = listAgents(d).filter((a) => a.status === "active");
10602
+ if (agents.length === 0) {
10603
+ return { task_id: taskId, assigned_to: null, agent_name: null, method: "no_agents" };
10604
+ }
10605
+ const workloads = getAgentWorkloads(d);
10606
+ const apiKey = process.env["CEREBRAS_API_KEY"];
10607
+ let selectedAgent = null;
10608
+ let method = "capability_match";
10609
+ let reason;
10610
+ if (apiKey) {
10611
+ const agentData = agents.map((a) => ({
10612
+ id: a.id,
10613
+ name: a.name,
10614
+ role: a.role || "agent",
10615
+ capabilities: a.capabilities || [],
10616
+ in_progress_tasks: workloads.get(a.id) ?? 0
10617
+ }));
10618
+ const result = await callCerebras(buildPrompt({
10619
+ title: task.title,
10620
+ description: task.description,
10621
+ priority: task.priority,
10622
+ tags: task.tags || []
10623
+ }, agentData), apiKey);
10624
+ if (result?.agent_name) {
10625
+ selectedAgent = agents.find((a) => a.name === result.agent_name) ?? null;
10626
+ if (selectedAgent) {
10627
+ method = "cerebras";
10628
+ reason = result.reason;
10629
+ }
10192
10630
  }
10193
- });
10194
- tx();
10195
- return results;
10631
+ }
10632
+ if (!selectedAgent) {
10633
+ const taskTags = task.tags || [];
10634
+ const capable = getCapableAgents(taskTags, { min_score: 0, limit: 10 }, d);
10635
+ if (capable.length > 0) {
10636
+ const sorted = capable.sort((a, b) => {
10637
+ if (b.score !== a.score)
10638
+ return b.score - a.score;
10639
+ return (workloads.get(a.agent.id) ?? 0) - (workloads.get(b.agent.id) ?? 0);
10640
+ });
10641
+ selectedAgent = sorted[0].agent;
10642
+ reason = `Capability match (score: ${sorted[0].score.toFixed(2)})`;
10643
+ } else {
10644
+ selectedAgent = agents.slice().sort((a, b) => (workloads.get(a.id) ?? 0) - (workloads.get(b.id) ?? 0))[0];
10645
+ reason = `Least busy agent (${workloads.get(selectedAgent.id) ?? 0} active tasks)`;
10646
+ }
10647
+ }
10648
+ if (selectedAgent) {
10649
+ updateTask(taskId, { assigned_to: selectedAgent.id, version: task.version }, d);
10650
+ }
10651
+ return {
10652
+ task_id: taskId,
10653
+ assigned_to: selectedAgent?.id ?? null,
10654
+ agent_name: selectedAgent?.name ?? null,
10655
+ method,
10656
+ reason
10657
+ };
10196
10658
  }
10197
- var init_task_files = __esm(() => {
10659
+ var CEREBRAS_API_URL = "https://api.cerebras.ai/v1/chat/completions", CEREBRAS_MODEL = "llama-3.3-70b";
10660
+ var init_auto_assign = __esm(() => {
10198
10661
  init_database();
10662
+ init_tasks();
10663
+ init_agents();
10199
10664
  });
10200
10665
 
10201
10666
  // src/db/file-locks.ts
@@ -10997,14 +11462,14 @@ __export(exports_mcp, {
10997
11462
  });
10998
11463
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10999
11464
  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";
11465
+ import { readFileSync as readFileSync4 } from "fs";
11466
+ import { join as join7, dirname as dirname2 } from "path";
11002
11467
  import { fileURLToPath } from "url";
11003
11468
  function getMcpVersion() {
11004
11469
  try {
11005
11470
  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";
11471
+ const pkgPath = join7(__dir, "..", "package.json");
11472
+ return JSON.parse(readFileSync4(pkgPath, "utf-8")).version || "0.0.0";
11008
11473
  } catch {
11009
11474
  return "0.0.0";
11010
11475
  }
@@ -11168,7 +11633,8 @@ var init_mcp = __esm(() => {
11168
11633
  "get_next_task",
11169
11634
  "bootstrap",
11170
11635
  "get_tasks_changed_since",
11171
- "heartbeat"
11636
+ "heartbeat",
11637
+ "release_agent"
11172
11638
  ]);
11173
11639
  STANDARD_EXCLUDED = new Set([
11174
11640
  "rename_agent",
@@ -11206,7 +11672,8 @@ var init_mcp = __esm(() => {
11206
11672
  spawns_template_id: exports_external.string().optional().describe("Template ID to auto-create as next task when this task is completed (pipeline/handoff chains)"),
11207
11673
  reason: exports_external.string().optional().describe("Why this task exists \u2014 context for agents picking it up"),
11208
11674
  spawned_from_session: exports_external.string().optional().describe("Session ID that created this task (for tracing task lineage)"),
11209
- assigned_from_project: exports_external.string().optional().describe("Override: project ID the assigning agent is working from. Auto-detected from agent focus if omitted.")
11675
+ assigned_from_project: exports_external.string().optional().describe("Override: project ID the assigning agent is working from. Auto-detected from agent focus if omitted."),
11676
+ task_type: exports_external.string().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or any custom string")
11210
11677
  }, async (params) => {
11211
11678
  try {
11212
11679
  if (!params.agent_id) {
@@ -11252,6 +11719,7 @@ var init_mcp = __esm(() => {
11252
11719
  has_recurrence: exports_external.boolean().optional(),
11253
11720
  due_today: exports_external.boolean().optional(),
11254
11721
  overdue: exports_external.boolean().optional(),
11722
+ task_type: exports_external.union([exports_external.string(), exports_external.array(exports_external.string())]).optional().describe("Filter by task type: bug, feature, chore, improvement, docs, test, security, or custom"),
11255
11723
  limit: exports_external.number().optional(),
11256
11724
  offset: exports_external.number().optional(),
11257
11725
  summary_only: exports_external.boolean().optional().describe("When true, return only id, short_id, title, status, priority \u2014 minimal tokens for navigation"),
@@ -11386,11 +11854,17 @@ Checklist (${done}/${task.checklist.length}):`);
11386
11854
  tags: exports_external.array(exports_external.string()).optional(),
11387
11855
  metadata: exports_external.record(exports_external.unknown()).optional(),
11388
11856
  plan_id: exports_external.string().optional(),
11389
- task_list_id: exports_external.string().optional()
11857
+ task_list_id: exports_external.string().optional(),
11858
+ task_type: exports_external.string().nullable().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or custom. null to clear.")
11390
11859
  }, async ({ id, ...rest }) => {
11391
11860
  try {
11392
11861
  const resolvedId = resolveId(id);
11393
- const task = updateTask(resolvedId, rest);
11862
+ const resolved = { ...rest };
11863
+ if (resolved.task_list_id)
11864
+ resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
11865
+ if (resolved.plan_id)
11866
+ resolved.plan_id = resolveId(resolved.plan_id, "plans");
11867
+ const task = updateTask(resolvedId, resolved);
11394
11868
  return { content: [{ type: "text", text: `updated: ${formatTask(task)}` }] };
11395
11869
  } catch (e) {
11396
11870
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -12030,11 +12504,12 @@ ${text}` }] };
12030
12504
  description: exports_external.string().optional(),
12031
12505
  capabilities: exports_external.array(exports_external.string()).optional().describe("Agent capabilities/skills for task routing (e.g. ['typescript', 'testing', 'devops'])"),
12032
12506
  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 }) => {
12507
+ 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"),
12508
+ 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.")
12509
+ }, async ({ name, description, capabilities, session_id, working_dir, force }) => {
12035
12510
  try {
12036
12511
  const pool = getAgentPoolForProject(working_dir);
12037
- const result = registerAgent({ name, description, capabilities, session_id, working_dir, pool: pool || undefined });
12512
+ const result = registerAgent({ name, description, capabilities, session_id, working_dir, force, pool: pool || undefined });
12038
12513
  if (isAgentConflict(result)) {
12039
12514
  const suggestLine = result.suggestions && result.suggestions.length > 0 ? `
12040
12515
  Available names: ${result.suggestions.join(", ")}` : "";
@@ -12253,6 +12728,31 @@ ID: ${updated.id}`
12253
12728
  }
12254
12729
  });
12255
12730
  }
12731
+ if (shouldRegisterTool("release_agent")) {
12732
+ 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.", {
12733
+ agent_id: exports_external.string().describe("Your agent ID or name."),
12734
+ 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).")
12735
+ }, async ({ agent_id, session_id }) => {
12736
+ try {
12737
+ const agent = getAgent(agent_id) || getAgentByName(agent_id);
12738
+ if (!agent) {
12739
+ return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
12740
+ }
12741
+ const released = releaseAgent(agent.id, session_id);
12742
+ if (!released) {
12743
+ return { content: [{ type: "text", text: `Release denied: session_id does not match agent's current session.` }], isError: true };
12744
+ }
12745
+ return {
12746
+ content: [{
12747
+ type: "text",
12748
+ text: `Agent released: ${agent.name} (${agent.id}) \u2014 session cleared, name is now available.`
12749
+ }]
12750
+ };
12751
+ } catch (e) {
12752
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12753
+ }
12754
+ });
12755
+ }
12256
12756
  if (shouldRegisterTool("create_task_list")) {
12257
12757
  server.tool("create_task_list", "Create a task list container for organizing tasks.", {
12258
12758
  name: exports_external.string(),
@@ -12356,56 +12856,228 @@ Slug: ${list.slug}`
12356
12856
  }
12357
12857
  });
12358
12858
  }
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 }) => {
12859
+ if (shouldRegisterTool("delete_task_list")) {
12860
+ server.tool("delete_task_list", "Delete a task list. Tasks are orphaned, not deleted.", {
12861
+ id: exports_external.string()
12862
+ }, async ({ id }) => {
12863
+ try {
12864
+ const resolvedId = resolveId(id, "task_lists");
12865
+ const deleted = deleteTaskList(resolvedId);
12866
+ return {
12867
+ content: [{
12868
+ type: "text",
12869
+ text: deleted ? `Task list ${id} deleted.` : `Task list ${id} not found.`
12870
+ }]
12871
+ };
12872
+ } catch (e) {
12873
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12874
+ }
12875
+ });
12876
+ }
12877
+ if (shouldRegisterTool("get_task_history")) {
12878
+ server.tool("get_task_history", "Get audit log \u2014 field changes with timestamps and actors.", {
12879
+ task_id: exports_external.string()
12880
+ }, async ({ task_id }) => {
12881
+ try {
12882
+ const resolvedId = resolveId(task_id);
12883
+ const { getTaskHistory: getTaskHistory2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
12884
+ const history = getTaskHistory2(resolvedId);
12885
+ if (history.length === 0)
12886
+ return { content: [{ type: "text", text: "No history for this task." }] };
12887
+ 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(`
12888
+ `);
12889
+ return { content: [{ type: "text", text: `${history.length} change(s):
12890
+ ${text}` }] };
12891
+ } catch (e) {
12892
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12893
+ }
12894
+ });
12895
+ }
12896
+ if (shouldRegisterTool("get_recent_activity")) {
12897
+ server.tool("get_recent_activity", "Get recent task changes \u2014 global activity feed.", {
12898
+ limit: exports_external.number().optional()
12899
+ }, async ({ limit }) => {
12900
+ try {
12901
+ const { getRecentActivity: getRecentActivity2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
12902
+ const activity = getRecentActivity2(limit || 50);
12903
+ if (activity.length === 0)
12904
+ return { content: [{ type: "text", text: "No recent activity." }] };
12905
+ 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(`
12906
+ `);
12907
+ return { content: [{ type: "text", text: `${activity.length} recent change(s):
12908
+ ${text}` }] };
12909
+ } catch (e) {
12910
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12911
+ }
12912
+ });
12913
+ }
12914
+ if (shouldRegisterTool("recap")) {
12915
+ 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.", {
12916
+ hours: exports_external.number().optional().describe("Look back N hours (default: 8)"),
12917
+ project_id: exports_external.string().optional().describe("Filter to a specific project")
12918
+ }, async ({ hours, project_id }) => {
12919
+ try {
12920
+ const { getRecap: getRecap2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
12921
+ const recap = getRecap2(hours || 8, project_id);
12922
+ const lines = [`Recap \u2014 last ${recap.hours}h (since ${recap.since})`];
12923
+ if (recap.completed.length > 0) {
12924
+ lines.push(`
12925
+ Completed (${recap.completed.length}):`);
12926
+ for (const t of recap.completed) {
12927
+ const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
12928
+ lines.push(` \u2713 ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
12929
+ }
12930
+ }
12931
+ if (recap.in_progress.length > 0) {
12932
+ lines.push(`
12933
+ In Progress (${recap.in_progress.length}):`);
12934
+ for (const t of recap.in_progress)
12935
+ lines.push(` \u2192 ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
12936
+ }
12937
+ if (recap.blocked.length > 0) {
12938
+ lines.push(`
12939
+ Blocked (${recap.blocked.length}):`);
12940
+ for (const t of recap.blocked)
12941
+ lines.push(` \u2717 ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
12942
+ }
12943
+ if (recap.stale.length > 0) {
12944
+ lines.push(`
12945
+ Stale (${recap.stale.length}):`);
12946
+ for (const t of recap.stale)
12947
+ lines.push(` ! ${t.short_id || t.id.slice(0, 8)} ${t.title} \u2014 updated ${t.updated_at}`);
12948
+ }
12949
+ if (recap.agents.length > 0) {
12950
+ lines.push(`
12951
+ Agents:`);
12952
+ for (const a of recap.agents)
12953
+ lines.push(` ${a.name}: ${a.completed_count} done, ${a.in_progress_count} active (seen ${a.last_seen_at})`);
12954
+ }
12955
+ lines.push(`
12956
+ Created: ${recap.created.length} new tasks`);
12957
+ return { content: [{ type: "text", text: lines.join(`
12958
+ `) }] };
12959
+ } catch (e) {
12960
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12961
+ }
12962
+ });
12963
+ }
12964
+ if (shouldRegisterTool("standup")) {
12965
+ server.tool("standup", "Generate standup notes \u2014 completed tasks since yesterday grouped by agent, in-progress work, and blockers. Copy-paste ready.", {
12966
+ hours: exports_external.number().optional().describe("Look back N hours (default: 24)"),
12967
+ project_id: exports_external.string().optional()
12968
+ }, async ({ hours, project_id }) => {
12969
+ try {
12970
+ const { getRecap: getRecap2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
12971
+ const recap = getRecap2(hours || 24, project_id);
12972
+ const lines = [`Standup \u2014 last ${recap.hours}h`];
12973
+ const byAgent = new Map;
12974
+ for (const t of recap.completed) {
12975
+ const agent = t.assigned_to || "unassigned";
12976
+ if (!byAgent.has(agent))
12977
+ byAgent.set(agent, []);
12978
+ byAgent.get(agent).push(t);
12979
+ }
12980
+ if (byAgent.size > 0) {
12981
+ lines.push(`
12982
+ Done:`);
12983
+ for (const [agent, tasks] of byAgent) {
12984
+ lines.push(` ${agent}:`);
12985
+ for (const t of tasks) {
12986
+ const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
12987
+ lines.push(` \u2713 ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}`);
12988
+ }
12989
+ }
12990
+ } else {
12991
+ lines.push(`
12992
+ Nothing completed.`);
12993
+ }
12994
+ if (recap.in_progress.length > 0) {
12995
+ lines.push(`
12996
+ In Progress:`);
12997
+ for (const t of recap.in_progress)
12998
+ lines.push(` \u2192 ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
12999
+ }
13000
+ if (recap.blocked.length > 0) {
13001
+ lines.push(`
13002
+ Blocked:`);
13003
+ for (const t of recap.blocked)
13004
+ lines.push(` \u2717 ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
13005
+ }
13006
+ return { content: [{ type: "text", text: lines.join(`
13007
+ `) }] };
13008
+ } catch (e) {
13009
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13010
+ }
13011
+ });
13012
+ }
13013
+ if (shouldRegisterTool("import_github_issue")) {
13014
+ server.tool("import_github_issue", "Import a GitHub issue as a task. Requires gh CLI installed and authenticated.", {
13015
+ url: exports_external.string().describe("GitHub issue URL (e.g. https://github.com/owner/repo/issues/42)"),
13016
+ project_id: exports_external.string().optional(),
13017
+ task_list_id: exports_external.string().optional()
13018
+ }, async ({ url, project_id, task_list_id }) => {
12363
13019
  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
- };
13020
+ const { parseGitHubUrl: parseGitHubUrl2, fetchGitHubIssue: fetchGitHubIssue2, issueToTask: issueToTask2 } = await Promise.resolve().then(() => (init_github(), exports_github));
13021
+ const parsed = parseGitHubUrl2(url);
13022
+ if (!parsed)
13023
+ return { content: [{ type: "text", text: "Invalid GitHub issue URL." }], isError: true };
13024
+ const issue = fetchGitHubIssue2(parsed.owner, parsed.repo, parsed.number);
13025
+ const input = issueToTask2(issue, { project_id, task_list_id });
13026
+ const task = createTask(input);
13027
+ return { content: [{ type: "text", text: `Imported GH#${issue.number}: ${issue.title}
13028
+ Task: ${task.short_id || task.id} [${task.priority}]
13029
+ Labels: ${issue.labels.join(", ") || "none"}` }] };
12372
13030
  } catch (e) {
12373
13031
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
12374
13032
  }
12375
13033
  });
12376
13034
  }
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 }) => {
13035
+ if (shouldRegisterTool("blame")) {
13036
+ server.tool("blame", "Show which tasks and agents touched a file \u2014 combines task_files and task_commits data.", {
13037
+ path: exports_external.string().describe("File path to look up")
13038
+ }, async ({ path }) => {
12381
13039
  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}` }] };
13040
+ const { findTasksByFile: findTasksByFile2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
13041
+ const db = getDatabase();
13042
+ const taskFiles = findTasksByFile2(path, db);
13043
+ 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}%`);
13044
+ const lines = [`Blame: ${path}`];
13045
+ if (taskFiles.length > 0) {
13046
+ lines.push(`
13047
+ Task File Links (${taskFiles.length}):`);
13048
+ for (const tf of taskFiles) {
13049
+ const task = getTask(tf.task_id, db);
13050
+ lines.push(` ${task?.short_id || tf.task_id.slice(0, 8)} ${task?.title || "?"} \u2014 ${tf.role || "file"}`);
13051
+ }
13052
+ }
13053
+ if (commitRows.length > 0) {
13054
+ lines.push(`
13055
+ Commit Links (${commitRows.length}):`);
13056
+ for (const c of commitRows)
13057
+ lines.push(` ${c.sha?.slice(0, 7)} ${c.short_id || c.task_id.slice(0, 8)} ${c.title || ""} \u2014 ${c.author || ""}`);
13058
+ }
13059
+ if (taskFiles.length === 0 && commitRows.length === 0) {
13060
+ lines.push("No task or commit links found.");
13061
+ }
13062
+ return { content: [{ type: "text", text: lines.join(`
13063
+ `) }] };
12391
13064
  } catch (e) {
12392
13065
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
12393
13066
  }
12394
13067
  });
12395
13068
  }
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 }) => {
13069
+ if (shouldRegisterTool("burndown")) {
13070
+ server.tool("burndown", "ASCII burndown chart showing actual vs ideal progress for a plan, project, or task list.", {
13071
+ plan_id: exports_external.string().optional(),
13072
+ project_id: exports_external.string().optional(),
13073
+ task_list_id: exports_external.string().optional()
13074
+ }, async ({ plan_id, project_id, task_list_id }) => {
12400
13075
  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}` }] };
13076
+ const { getBurndown: getBurndown2 } = await Promise.resolve().then(() => (init_burndown(), exports_burndown));
13077
+ const data = getBurndown2({ plan_id, project_id, task_list_id });
13078
+ return { content: [{ type: "text", text: `Burndown: ${data.completed}/${data.total} done, ${data.remaining} remaining
13079
+
13080
+ ${data.chart}` }] };
12409
13081
  } catch (e) {
12410
13082
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
12411
13083
  }
@@ -13216,6 +13888,91 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
13216
13888
  }
13217
13889
  });
13218
13890
  }
13891
+ if (shouldRegisterTool("task_context")) {
13892
+ 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.", {
13893
+ id: exports_external.string().describe("Task ID, short_id, or partial ID")
13894
+ }, async ({ id }) => {
13895
+ try {
13896
+ const resolvedId = resolveId(id, "tasks");
13897
+ const task = getTaskWithRelations(resolvedId);
13898
+ if (!task)
13899
+ return { content: [{ type: "text", text: `Task not found: ${id}` }], isError: true };
13900
+ const lines = [];
13901
+ const sid = task.short_id || task.id.slice(0, 8);
13902
+ lines.push(`${sid} [${task.status}] [${task.priority}] ${task.title}`);
13903
+ if (task.description)
13904
+ lines.push(`
13905
+ Description:
13906
+ ${task.description}`);
13907
+ if (task.assigned_to)
13908
+ lines.push(`Assigned: ${task.assigned_to}`);
13909
+ if (task.started_at)
13910
+ lines.push(`Started: ${task.started_at}`);
13911
+ if (task.completed_at) {
13912
+ lines.push(`Completed: ${task.completed_at}`);
13913
+ if (task.started_at) {
13914
+ const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
13915
+ lines.push(`Duration: ${dur}m`);
13916
+ }
13917
+ }
13918
+ if (task.tags.length > 0)
13919
+ lines.push(`Tags: ${task.tags.join(", ")}`);
13920
+ if (task.dependencies.length > 0) {
13921
+ lines.push(`
13922
+ Depends on (${task.dependencies.length}):`);
13923
+ for (const dep of task.dependencies) {
13924
+ const blocked = dep.status !== "completed" && dep.status !== "cancelled";
13925
+ lines.push(` ${blocked ? "\u2717" : "\u2713"} ${dep.short_id || dep.id.slice(0, 8)} [${dep.status}] ${dep.title}`);
13926
+ }
13927
+ const unfinished = task.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
13928
+ if (unfinished.length > 0)
13929
+ lines.push(`\u26A0 BLOCKED by ${unfinished.length} unfinished dep(s)`);
13930
+ }
13931
+ if (task.blocked_by.length > 0) {
13932
+ lines.push(`
13933
+ Blocks (${task.blocked_by.length}):`);
13934
+ for (const b of task.blocked_by)
13935
+ lines.push(` ${b.short_id || b.id.slice(0, 8)} [${b.status}] ${b.title}`);
13936
+ }
13937
+ if (task.subtasks.length > 0) {
13938
+ lines.push(`
13939
+ Subtasks (${task.subtasks.length}):`);
13940
+ for (const st of task.subtasks)
13941
+ lines.push(` ${st.short_id || st.id.slice(0, 8)} [${st.status}] ${st.title}`);
13942
+ }
13943
+ try {
13944
+ const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
13945
+ const files = listTaskFiles2(task.id);
13946
+ if (files.length > 0) {
13947
+ lines.push(`
13948
+ Files (${files.length}):`);
13949
+ for (const f of files)
13950
+ lines.push(` ${f.role || "file"}: ${f.path}`);
13951
+ }
13952
+ } catch {}
13953
+ try {
13954
+ const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
13955
+ const commits = getTaskCommits2(task.id);
13956
+ if (commits.length > 0) {
13957
+ lines.push(`
13958
+ Commits (${commits.length}):`);
13959
+ for (const c of commits)
13960
+ lines.push(` ${c.commit_hash?.slice(0, 7)} ${c.message || ""}`);
13961
+ }
13962
+ } catch {}
13963
+ if (task.comments.length > 0) {
13964
+ lines.push(`
13965
+ Comments (${task.comments.length}):`);
13966
+ for (const c of task.comments)
13967
+ lines.push(` [${c.agent_id || "?"}] ${c.created_at}: ${c.content}`);
13968
+ }
13969
+ return { content: [{ type: "text", text: lines.join(`
13970
+ `) }] };
13971
+ } catch (e) {
13972
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13973
+ }
13974
+ });
13975
+ }
13219
13976
  if (shouldRegisterTool("get_context")) {
13220
13977
  server.tool("get_context", "Get a compact task summary for agent prompt injection. Returns formatted text.", {
13221
13978
  agent_id: exports_external.string().optional(),
@@ -13478,6 +14235,7 @@ ${stack_trace.slice(0, 1500)}
13478
14235
  "delete_agent",
13479
14236
  "unarchive_agent",
13480
14237
  "heartbeat",
14238
+ "release_agent",
13481
14239
  "get_my_tasks",
13482
14240
  "get_org_chart",
13483
14241
  "set_reports_to",
@@ -13494,6 +14252,12 @@ ${stack_trace.slice(0, 1500)}
13494
14252
  "claim_next_task",
13495
14253
  "get_task_history",
13496
14254
  "get_recent_activity",
14255
+ "recap",
14256
+ "task_context",
14257
+ "standup",
14258
+ "burndown",
14259
+ "blame",
14260
+ "import_github_issue",
13497
14261
  "create_webhook",
13498
14262
  "list_webhooks",
13499
14263
  "delete_webhook",
@@ -13618,8 +14382,8 @@ ${stack_trace.slice(0, 1500)}
13618
14382
  suggest_agent_name: `Check available agent names before registering. Shows active agents and, if a pool is configured, which pool names are free.
13619
14383
  Params: working_dir(string \u2014 your working directory, used to look up project pool from config)
13620
14384
  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)
14385
+ 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.
14386
+ 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
14387
  Example: {name: 'my-agent', session_id: 'abc123-1741952000', working_dir: '/workspace/platform'}`,
13624
14388
  list_agents: `List all registered agents (active by default). Set include_archived: true to see archived agents.
13625
14389
  Params: include_archived(boolean, optional)
@@ -13639,6 +14403,9 @@ ${stack_trace.slice(0, 1500)}
13639
14403
  heartbeat: `Update last_seen_at timestamp to signal you're still active. Call periodically during long tasks.
13640
14404
  Params: agent_id(string, req \u2014 your agent ID or name)
13641
14405
  Example: {agent_id: 'maximus'}`,
14406
+ release_agent: `Explicitly release/logout an agent \u2014 clears session binding and makes name immediately available. Call when session ends.
14407
+ Params: agent_id(string, req), session_id(string \u2014 only releases if matching)
14408
+ Example: {agent_id: 'maximus', session_id: 'my-session-123'}`,
13642
14409
  get_my_tasks: `Get all tasks assigned to/created by an agent, with stats (pending/active/done/rate).
13643
14410
  Params: agent_name(string, req)
13644
14411
  Example: {agent_name: 'maximus'}`,
@@ -13699,6 +14466,24 @@ ${stack_trace.slice(0, 1500)}
13699
14466
  get_recent_activity: `Get recent task changes across all tasks \u2014 global activity feed.
13700
14467
  Params: limit(number, default:50)
13701
14468
  Example: {limit: 20}`,
14469
+ recap: `Summary of what happened in the last N hours \u2014 completed tasks with durations, new tasks, in-progress, blocked, stale, agent activity.
14470
+ Params: hours(number, default:8), project_id(string)
14471
+ Example: {hours: 4}`,
14472
+ task_context: `Full orientation for a specific task \u2014 description, dependencies with blocked status, files, commits, comments, checklist, duration. Use before starting work.
14473
+ Params: id(string, req)
14474
+ Example: {id: 'OPE-00042'}`,
14475
+ standup: `Generate standup notes \u2014 completed tasks grouped by agent, in-progress, blocked. Copy-paste ready.
14476
+ Params: hours(number, default:24), project_id(string)
14477
+ Example: {hours: 24}`,
14478
+ import_github_issue: `Import a GitHub issue as a task. Requires gh CLI.
14479
+ Params: url(string, req), project_id(string), task_list_id(string)
14480
+ Example: {url: 'https://github.com/owner/repo/issues/42'}`,
14481
+ blame: `Show which tasks/agents touched a file \u2014 combines task_files and task_commits.
14482
+ Params: path(string, req)
14483
+ Example: {path: 'src/db/agents.ts'}`,
14484
+ burndown: `ASCII burndown chart \u2014 actual vs ideal progress for a plan, project, or task list.
14485
+ Params: plan_id(string), project_id(string), task_list_id(string)
14486
+ Example: {plan_id: 'abc123'}`,
13702
14487
  create_webhook: `Register a webhook for task change events.
13703
14488
  Params: url(string, req), events(string[] \u2014 empty=all), secret(string \u2014 HMAC signing)
13704
14489
  Example: {url: 'https://example.com/hook', events: ['task.created', 'task.completed']}`,
@@ -14474,6 +15259,48 @@ ${lines.join(`
14474
15259
  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
15260
  return { content: [{ type: "text", text: `Leaderboard:
14476
15261
  ${lines.join(`
15262
+ `)}` }] };
15263
+ } catch (e) {
15264
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
15265
+ }
15266
+ });
15267
+ }
15268
+ if (shouldRegisterTool("extract_todos")) {
15269
+ server.tool("extract_todos", "Scan source files for TODO/FIXME/HACK/BUG/XXX/NOTE comments and create tasks from them. Deduplicates on re-runs.", {
15270
+ path: exports_external.string().describe("Directory or file path to scan"),
15271
+ project_id: exports_external.string().optional().describe("Project to assign tasks to"),
15272
+ task_list_id: exports_external.string().optional().describe("Task list to add tasks to"),
15273
+ patterns: exports_external.array(exports_external.enum(["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"])).optional().describe("Tags to search for (default: all)"),
15274
+ tags: exports_external.array(exports_external.string()).optional().describe("Extra tags to add to created tasks"),
15275
+ assigned_to: exports_external.string().optional().describe("Agent to assign tasks to"),
15276
+ agent_id: exports_external.string().optional().describe("Agent performing the extraction"),
15277
+ dry_run: exports_external.boolean().optional().describe("If true, return found comments without creating tasks"),
15278
+ extensions: exports_external.array(exports_external.string()).optional().describe("File extensions to scan (e.g. ['ts', 'py'])")
15279
+ }, async (params) => {
15280
+ try {
15281
+ const { extractTodos: extractTodos2 } = (init_extract(), __toCommonJS(exports_extract));
15282
+ const resolved = { ...params };
15283
+ if (resolved["project_id"])
15284
+ resolved["project_id"] = resolveId(resolved["project_id"], "projects");
15285
+ if (resolved["task_list_id"])
15286
+ resolved["task_list_id"] = resolveId(resolved["task_list_id"], "task_lists");
15287
+ const result = extractTodos2(resolved);
15288
+ if (params.dry_run) {
15289
+ const lines = result.comments.map((c) => `[${c.tag}] ${c.message} \u2014 ${c.file}:${c.line}`);
15290
+ return { content: [{ type: "text", text: `Found ${result.comments.length} comment(s):
15291
+ ${lines.join(`
15292
+ `)}` }] };
15293
+ }
15294
+ const summary = [
15295
+ `Created ${result.tasks.length} task(s)`,
15296
+ result.skipped > 0 ? `Skipped ${result.skipped} duplicate(s)` : null,
15297
+ `Total comments found: ${result.comments.length}`
15298
+ ].filter(Boolean).join(`
15299
+ `);
15300
+ const taskLines = result.tasks.map((t) => formatTask(t));
15301
+ return { content: [{ type: "text", text: `${summary}
15302
+
15303
+ ${taskLines.join(`
14477
15304
  `)}` }] };
14478
15305
  } catch (e) {
14479
15306
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -14561,26 +15388,26 @@ __export(exports_serve, {
14561
15388
  startServer: () => startServer
14562
15389
  });
14563
15390
  import { existsSync as existsSync6 } from "fs";
14564
- import { join as join7, dirname as dirname3, extname, resolve as resolve2, sep } from "path";
15391
+ import { join as join8, dirname as dirname3, extname, resolve as resolve3, sep } from "path";
14565
15392
  import { fileURLToPath as fileURLToPath2 } from "url";
14566
15393
  function resolveDashboardDir() {
14567
15394
  const candidates = [];
14568
15395
  try {
14569
15396
  const scriptDir = dirname3(fileURLToPath2(import.meta.url));
14570
- candidates.push(join7(scriptDir, "..", "dashboard", "dist"));
14571
- candidates.push(join7(scriptDir, "..", "..", "dashboard", "dist"));
15397
+ candidates.push(join8(scriptDir, "..", "dashboard", "dist"));
15398
+ candidates.push(join8(scriptDir, "..", "..", "dashboard", "dist"));
14572
15399
  } catch {}
14573
15400
  if (process.argv[1]) {
14574
15401
  const mainDir = dirname3(process.argv[1]);
14575
- candidates.push(join7(mainDir, "..", "dashboard", "dist"));
14576
- candidates.push(join7(mainDir, "..", "..", "dashboard", "dist"));
15402
+ candidates.push(join8(mainDir, "..", "dashboard", "dist"));
15403
+ candidates.push(join8(mainDir, "..", "..", "dashboard", "dist"));
14577
15404
  }
14578
- candidates.push(join7(process.cwd(), "dashboard", "dist"));
15405
+ candidates.push(join8(process.cwd(), "dashboard", "dist"));
14579
15406
  for (const candidate of candidates) {
14580
15407
  if (existsSync6(candidate))
14581
15408
  return candidate;
14582
15409
  }
14583
- return join7(process.cwd(), "dashboard", "dist");
15410
+ return join8(process.cwd(), "dashboard", "dist");
14584
15411
  }
14585
15412
  function json(data, status = 200, port) {
14586
15413
  return new Response(JSON.stringify(data), {
@@ -15384,9 +16211,9 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
15384
16211
  }
15385
16212
  if (dashboardExists && (method === "GET" || method === "HEAD")) {
15386
16213
  if (path !== "/") {
15387
- const filePath = join7(dashboardDir, path);
15388
- const resolvedFile = resolve2(filePath);
15389
- const resolvedBase = resolve2(dashboardDir);
16214
+ const filePath = join8(dashboardDir, path);
16215
+ const resolvedFile = resolve3(filePath);
16216
+ const resolvedBase = resolve3(dashboardDir);
15390
16217
  if (!resolvedFile.startsWith(resolvedBase + sep) && resolvedFile !== resolvedBase) {
15391
16218
  return json({ error: "Forbidden" }, 403, port);
15392
16219
  }
@@ -15394,7 +16221,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
15394
16221
  if (res2)
15395
16222
  return res2;
15396
16223
  }
15397
- const indexPath = join7(dashboardDir, "index.html");
16224
+ const indexPath = join8(dashboardDir, "index.html");
15398
16225
  const res = serveStaticFile(indexPath);
15399
16226
  if (res)
15400
16227
  return res;
@@ -16483,21 +17310,307 @@ function App({ projectId }) {
16483
17310
  ]
16484
17311
  }, undefined, true, undefined, this);
16485
17312
  }
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();
17313
+ function renderApp(projectId) {
17314
+ render(/* @__PURE__ */ jsxDEV7(App, {
17315
+ projectId
17316
+ }, undefined, false, undefined, this));
17317
+ }
17318
+ var init_App = __esm(() => {
17319
+ init_Header();
17320
+ init_TaskList();
17321
+ init_TaskDetail();
17322
+ init_TaskForm();
17323
+ init_ProjectList();
17324
+ init_SearchView();
17325
+ init_tasks();
17326
+ init_projects();
17327
+ init_search();
17328
+ });
17329
+
17330
+ // src/cli/components/Dashboard.tsx
17331
+ var exports_Dashboard = {};
17332
+ __export(exports_Dashboard, {
17333
+ Dashboard: () => Dashboard
17334
+ });
17335
+ import { useState as useState4, useEffect as useEffect2 } from "react";
17336
+ import { Box as Box8, Text as Text7, useApp as useApp2, useInput as useInput4 } from "ink";
17337
+ import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
17338
+ function AgentStatus({ name, lastSeen, sessionId }) {
17339
+ const ago = Math.round((Date.now() - new Date(lastSeen).getTime()) / 60000);
17340
+ const color = ago < 5 ? "green" : ago < 15 ? "yellow" : "red";
17341
+ const symbol = ago < 5 ? "\u25CF" : ago < 15 ? "\u25D0" : "\u25CB";
17342
+ return /* @__PURE__ */ jsxDEV8(Box8, {
17343
+ children: [
17344
+ /* @__PURE__ */ jsxDEV8(Text7, {
17345
+ color,
17346
+ children: [
17347
+ symbol,
17348
+ " "
17349
+ ]
17350
+ }, undefined, true, undefined, this),
17351
+ /* @__PURE__ */ jsxDEV8(Text7, {
17352
+ bold: true,
17353
+ children: name
17354
+ }, undefined, false, undefined, this),
17355
+ /* @__PURE__ */ jsxDEV8(Text7, {
17356
+ dimColor: true,
17357
+ children: [
17358
+ " ",
17359
+ ago,
17360
+ "m ago"
17361
+ ]
17362
+ }, undefined, true, undefined, this),
17363
+ sessionId && /* @__PURE__ */ jsxDEV8(Text7, {
17364
+ dimColor: true,
17365
+ children: [
17366
+ " [",
17367
+ sessionId.slice(0, 8),
17368
+ "]"
17369
+ ]
17370
+ }, undefined, true, undefined, this)
17371
+ ]
17372
+ }, undefined, true, undefined, this);
17373
+ }
17374
+ function Dashboard({ projectId, refreshMs = 2000 }) {
17375
+ const { exit } = useApp2();
17376
+ const [tick, setTick] = useState4(0);
17377
+ const [recap, setRecap] = useState4(null);
17378
+ const [counts, setCounts] = useState4({ pending: 0, in_progress: 0, completed: 0, failed: 0, total: 0 });
17379
+ const [agents, setAgents] = useState4([]);
17380
+ useInput4((input) => {
17381
+ if (input === "q" || input === "Q")
17382
+ exit();
17383
+ });
17384
+ useEffect2(() => {
17385
+ const timer = setInterval(() => setTick((t) => t + 1), refreshMs);
17386
+ return () => clearInterval(timer);
17387
+ }, [refreshMs]);
17388
+ useEffect2(() => {
17389
+ try {
17390
+ const db = getDatabase();
17391
+ const filters = projectId ? { project_id: projectId } : {};
17392
+ const pending = countTasks({ ...filters, status: "pending" }, db);
17393
+ const in_progress = countTasks({ ...filters, status: "in_progress" }, db);
17394
+ const completed = countTasks({ ...filters, status: "completed" }, db);
17395
+ const failed = countTasks({ ...filters, status: "failed" }, db);
17396
+ setCounts({ pending, in_progress, completed, failed, total: pending + in_progress + completed + failed });
17397
+ setAgents(listAgents());
17398
+ setRecap(getRecap(1, projectId, db));
17399
+ } catch {}
17400
+ }, [tick, projectId]);
17401
+ return /* @__PURE__ */ jsxDEV8(Box8, {
17402
+ flexDirection: "column",
17403
+ padding: 1,
17404
+ children: [
17405
+ /* @__PURE__ */ jsxDEV8(Box8, {
17406
+ marginBottom: 1,
17407
+ children: [
17408
+ /* @__PURE__ */ jsxDEV8(Text7, {
17409
+ bold: true,
17410
+ color: "cyan",
17411
+ children: " todos dashboard "
17412
+ }, undefined, false, undefined, this),
17413
+ /* @__PURE__ */ jsxDEV8(Text7, {
17414
+ dimColor: true,
17415
+ children: [
17416
+ "| refreshing every ",
17417
+ refreshMs / 1000,
17418
+ "s | press q to quit"
17419
+ ]
17420
+ }, undefined, true, undefined, this)
17421
+ ]
17422
+ }, undefined, true, undefined, this),
17423
+ /* @__PURE__ */ jsxDEV8(Box8, {
17424
+ marginBottom: 1,
17425
+ children: [
17426
+ /* @__PURE__ */ jsxDEV8(Text7, {
17427
+ color: "yellow",
17428
+ children: [
17429
+ counts.pending,
17430
+ " pending"
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: "blue",
17439
+ children: [
17440
+ counts.in_progress,
17441
+ " active"
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
+ color: "green",
17450
+ children: [
17451
+ counts.completed,
17452
+ " done"
17453
+ ]
17454
+ }, undefined, true, undefined, this),
17455
+ /* @__PURE__ */ jsxDEV8(Text7, {
17456
+ dimColor: true,
17457
+ children: " | "
17458
+ }, undefined, false, undefined, this),
17459
+ /* @__PURE__ */ jsxDEV8(Text7, {
17460
+ color: "red",
17461
+ children: [
17462
+ counts.failed,
17463
+ " failed"
17464
+ ]
17465
+ }, undefined, true, undefined, this),
17466
+ /* @__PURE__ */ jsxDEV8(Text7, {
17467
+ dimColor: true,
17468
+ children: " | "
17469
+ }, undefined, false, undefined, this),
17470
+ /* @__PURE__ */ jsxDEV8(Text7, {
17471
+ children: [
17472
+ counts.total,
17473
+ " total"
17474
+ ]
17475
+ }, undefined, true, undefined, this)
17476
+ ]
17477
+ }, undefined, true, undefined, this),
17478
+ agents.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
17479
+ flexDirection: "column",
17480
+ marginBottom: 1,
17481
+ children: [
17482
+ /* @__PURE__ */ jsxDEV8(Text7, {
17483
+ bold: true,
17484
+ children: [
17485
+ "Agents (",
17486
+ agents.length,
17487
+ "):"
17488
+ ]
17489
+ }, undefined, true, undefined, this),
17490
+ agents.map((a) => /* @__PURE__ */ jsxDEV8(AgentStatus, {
17491
+ name: a.name,
17492
+ lastSeen: a.last_seen_at,
17493
+ sessionId: a.session_id
17494
+ }, a.id, false, undefined, this))
17495
+ ]
17496
+ }, undefined, true, undefined, this),
17497
+ recap && recap.in_progress.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
17498
+ flexDirection: "column",
17499
+ marginBottom: 1,
17500
+ children: [
17501
+ /* @__PURE__ */ jsxDEV8(Text7, {
17502
+ bold: true,
17503
+ color: "blue",
17504
+ children: [
17505
+ "In Progress (",
17506
+ recap.in_progress.length,
17507
+ "):"
17508
+ ]
17509
+ }, undefined, true, undefined, this),
17510
+ recap.in_progress.slice(0, 8).map((t) => /* @__PURE__ */ jsxDEV8(Box8, {
17511
+ children: [
17512
+ /* @__PURE__ */ jsxDEV8(Text7, {
17513
+ color: "cyan",
17514
+ children: [
17515
+ t.short_id || t.id.slice(0, 8),
17516
+ " "
17517
+ ]
17518
+ }, undefined, true, undefined, this),
17519
+ /* @__PURE__ */ jsxDEV8(Text7, {
17520
+ children: t.title
17521
+ }, undefined, false, undefined, this),
17522
+ t.assigned_to && /* @__PURE__ */ jsxDEV8(Text7, {
17523
+ dimColor: true,
17524
+ children: [
17525
+ " \u2014 ",
17526
+ t.assigned_to
17527
+ ]
17528
+ }, undefined, true, undefined, this)
17529
+ ]
17530
+ }, t.id, true, undefined, this))
17531
+ ]
17532
+ }, undefined, true, undefined, this),
17533
+ recap && recap.completed.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
17534
+ flexDirection: "column",
17535
+ marginBottom: 1,
17536
+ children: [
17537
+ /* @__PURE__ */ jsxDEV8(Text7, {
17538
+ bold: true,
17539
+ color: "green",
17540
+ children: [
17541
+ "Completed (last 1h: ",
17542
+ recap.completed.length,
17543
+ "):"
17544
+ ]
17545
+ }, undefined, true, undefined, this),
17546
+ recap.completed.slice(0, 5).map((t) => /* @__PURE__ */ jsxDEV8(Box8, {
17547
+ children: [
17548
+ /* @__PURE__ */ jsxDEV8(Text7, {
17549
+ color: "green",
17550
+ children: "\u2713 "
17551
+ }, undefined, false, undefined, this),
17552
+ /* @__PURE__ */ jsxDEV8(Text7, {
17553
+ color: "cyan",
17554
+ children: [
17555
+ t.short_id || t.id.slice(0, 8),
17556
+ " "
17557
+ ]
17558
+ }, undefined, true, undefined, this),
17559
+ /* @__PURE__ */ jsxDEV8(Text7, {
17560
+ children: t.title
17561
+ }, undefined, false, undefined, this),
17562
+ t.duration_minutes != null && /* @__PURE__ */ jsxDEV8(Text7, {
17563
+ dimColor: true,
17564
+ children: [
17565
+ " (",
17566
+ t.duration_minutes,
17567
+ "m)"
17568
+ ]
17569
+ }, undefined, true, undefined, this)
17570
+ ]
17571
+ }, t.id, true, undefined, this))
17572
+ ]
17573
+ }, undefined, true, undefined, this),
17574
+ recap && recap.stale.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
17575
+ flexDirection: "column",
17576
+ children: [
17577
+ /* @__PURE__ */ jsxDEV8(Text7, {
17578
+ bold: true,
17579
+ color: "red",
17580
+ children: [
17581
+ "Stale (",
17582
+ recap.stale.length,
17583
+ "):"
17584
+ ]
17585
+ }, undefined, true, undefined, this),
17586
+ recap.stale.slice(0, 3).map((t) => /* @__PURE__ */ jsxDEV8(Box8, {
17587
+ children: [
17588
+ /* @__PURE__ */ jsxDEV8(Text7, {
17589
+ color: "red",
17590
+ children: "! "
17591
+ }, undefined, false, undefined, this),
17592
+ /* @__PURE__ */ jsxDEV8(Text7, {
17593
+ color: "cyan",
17594
+ children: [
17595
+ t.short_id || t.id.slice(0, 8),
17596
+ " "
17597
+ ]
17598
+ }, undefined, true, undefined, this),
17599
+ /* @__PURE__ */ jsxDEV8(Text7, {
17600
+ children: t.title
17601
+ }, undefined, false, undefined, this)
17602
+ ]
17603
+ }, t.id, true, undefined, this))
17604
+ ]
17605
+ }, undefined, true, undefined, this)
17606
+ ]
17607
+ }, undefined, true, undefined, this);
17608
+ }
17609
+ var init_Dashboard = __esm(() => {
17610
+ init_database();
17611
+ init_agents();
16498
17612
  init_tasks();
16499
- init_projects();
16500
- init_search();
17613
+ init_audit();
16501
17614
  });
16502
17615
 
16503
17616
  // node_modules/commander/esm.mjs
@@ -16528,14 +17641,14 @@ init_search();
16528
17641
  init_sync();
16529
17642
  init_config();
16530
17643
  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";
17644
+ import { execSync as execSync2 } from "child_process";
17645
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
17646
+ import { basename, dirname as dirname4, join as join9, resolve as resolve4 } from "path";
16534
17647
  import { fileURLToPath as fileURLToPath3 } from "url";
16535
17648
  function getPackageVersion() {
16536
17649
  try {
16537
- const pkgPath = join8(dirname4(fileURLToPath3(import.meta.url)), "..", "..", "package.json");
16538
- return JSON.parse(readFileSync4(pkgPath, "utf-8")).version || "0.0.0";
17650
+ const pkgPath = join9(dirname4(fileURLToPath3(import.meta.url)), "..", "..", "package.json");
17651
+ return JSON.parse(readFileSync5(pkgPath, "utf-8")).version || "0.0.0";
16539
17652
  } catch {
16540
17653
  return "0.0.0";
16541
17654
  }
@@ -16567,14 +17680,14 @@ function resolveTaskId(partialId) {
16567
17680
  }
16568
17681
  function detectGitRoot() {
16569
17682
  try {
16570
- return execSync("git rev-parse --show-toplevel", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
17683
+ return execSync2("git rev-parse --show-toplevel", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
16571
17684
  } catch {
16572
17685
  return null;
16573
17686
  }
16574
17687
  }
16575
17688
  function autoDetectProject(opts) {
16576
17689
  if (opts.project) {
16577
- return getProjectByPath(resolve3(opts.project)) ?? undefined;
17690
+ return getProjectByPath(resolve4(opts.project)) ?? undefined;
16578
17691
  }
16579
17692
  if (process.env["TODOS_AUTO_PROJECT"] === "false")
16580
17693
  return;
@@ -16842,8 +17955,15 @@ program2.command("show <id>").description("Show full task details").action((id)
16842
17955
  console.log(` ${chalk.dim("Tags:")} ${task.tags.join(", ")}`);
16843
17956
  console.log(` ${chalk.dim("Version:")} ${task.version}`);
16844
17957
  console.log(` ${chalk.dim("Created:")} ${task.created_at}`);
16845
- if (task.completed_at)
17958
+ if (task.started_at)
17959
+ console.log(` ${chalk.dim("Started:")} ${task.started_at}`);
17960
+ if (task.completed_at) {
16846
17961
  console.log(` ${chalk.dim("Done:")} ${task.completed_at}`);
17962
+ if (task.started_at) {
17963
+ const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
17964
+ console.log(` ${chalk.dim("Duration:")} ${dur}m`);
17965
+ }
17966
+ }
16847
17967
  if (task.subtasks.length > 0) {
16848
17968
  console.log(chalk.bold(`
16849
17969
  Subtasks (${task.subtasks.length}):`));
@@ -16874,6 +17994,137 @@ program2.command("show <id>").description("Show full task details").action((id)
16874
17994
  }
16875
17995
  }
16876
17996
  });
17997
+ 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) => {
17998
+ const globalOpts = program2.opts();
17999
+ let resolvedId = id ? resolveTaskId(id) : null;
18000
+ if (!resolvedId && globalOpts.agent) {
18001
+ const { listTasks: listTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
18002
+ const active = listTasks2({ status: "in_progress", assigned_to: globalOpts.agent });
18003
+ if (active.length > 0)
18004
+ resolvedId = active[0].id;
18005
+ }
18006
+ if (!resolvedId) {
18007
+ console.error(chalk.red("No task ID given and no active task found. Pass an ID or use --agent."));
18008
+ process.exit(1);
18009
+ }
18010
+ const task = getTaskWithRelations(resolvedId);
18011
+ if (!task) {
18012
+ console.error(chalk.red(`Task not found: ${id || resolvedId}`));
18013
+ process.exit(1);
18014
+ }
18015
+ if (globalOpts.json) {
18016
+ const { listTaskFiles: listTaskFiles2 } = (init_task_files(), __toCommonJS(exports_task_files));
18017
+ const { getTaskCommits: getTaskCommits2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
18018
+ try {
18019
+ task.files = listTaskFiles2(task.id);
18020
+ } catch {
18021
+ task.files = [];
18022
+ }
18023
+ try {
18024
+ task.commits = getTaskCommits2(task.id);
18025
+ } catch {
18026
+ task.commits = [];
18027
+ }
18028
+ output(task, true);
18029
+ return;
18030
+ }
18031
+ const sid = task.short_id || task.id.slice(0, 8);
18032
+ const statusColor = statusColors4[task.status] || chalk.white;
18033
+ const prioColor = priorityColors2[task.priority] || chalk.white;
18034
+ console.log(chalk.bold(`
18035
+ ${chalk.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${task.title}
18036
+ `));
18037
+ if (task.description) {
18038
+ console.log(chalk.dim("Description:"));
18039
+ console.log(` ${task.description}
18040
+ `);
18041
+ }
18042
+ if (task.assigned_to)
18043
+ console.log(` ${chalk.dim("Assigned:")} ${task.assigned_to}`);
18044
+ if (task.locked_by)
18045
+ console.log(` ${chalk.dim("Locked by:")} ${task.locked_by}`);
18046
+ if (task.project_id)
18047
+ console.log(` ${chalk.dim("Project:")} ${task.project_id}`);
18048
+ if (task.plan_id)
18049
+ console.log(` ${chalk.dim("Plan:")} ${task.plan_id}`);
18050
+ if (task.started_at)
18051
+ console.log(` ${chalk.dim("Started:")} ${task.started_at}`);
18052
+ if (task.completed_at) {
18053
+ console.log(` ${chalk.dim("Completed:")} ${task.completed_at}`);
18054
+ if (task.started_at) {
18055
+ const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
18056
+ console.log(` ${chalk.dim("Duration:")} ${dur}m`);
18057
+ }
18058
+ }
18059
+ if (task.estimated_minutes)
18060
+ console.log(` ${chalk.dim("Estimate:")} ${task.estimated_minutes}m`);
18061
+ if (task.tags.length > 0)
18062
+ console.log(` ${chalk.dim("Tags:")} ${task.tags.join(", ")}`);
18063
+ const unfinishedDeps = task.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
18064
+ if (task.dependencies.length > 0) {
18065
+ console.log(chalk.bold(`
18066
+ Depends on (${task.dependencies.length}):`));
18067
+ for (const dep of task.dependencies) {
18068
+ const blocked = dep.status !== "completed" && dep.status !== "cancelled";
18069
+ const icon = blocked ? chalk.red("\u2717") : chalk.green("\u2713");
18070
+ console.log(` ${icon} ${formatTaskLine(dep)}`);
18071
+ }
18072
+ }
18073
+ if (unfinishedDeps.length > 0) {
18074
+ console.log(chalk.red(`
18075
+ BLOCKED by ${unfinishedDeps.length} unfinished dep(s)`));
18076
+ }
18077
+ if (task.blocked_by.length > 0) {
18078
+ console.log(chalk.bold(`
18079
+ Blocks (${task.blocked_by.length}):`));
18080
+ for (const b of task.blocked_by)
18081
+ console.log(` ${formatTaskLine(b)}`);
18082
+ }
18083
+ if (task.subtasks.length > 0) {
18084
+ console.log(chalk.bold(`
18085
+ Subtasks (${task.subtasks.length}):`));
18086
+ for (const st of task.subtasks)
18087
+ console.log(` ${formatTaskLine(st)}`);
18088
+ }
18089
+ try {
18090
+ const { listTaskFiles: listTaskFiles2 } = (init_task_files(), __toCommonJS(exports_task_files));
18091
+ const files = listTaskFiles2(task.id);
18092
+ if (files.length > 0) {
18093
+ console.log(chalk.bold(`
18094
+ Files (${files.length}):`));
18095
+ for (const f of files)
18096
+ console.log(` ${chalk.dim(f.role || "file")} ${f.path}`);
18097
+ }
18098
+ } catch {}
18099
+ try {
18100
+ const { getTaskCommits: getTaskCommits2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
18101
+ const commits = getTaskCommits2(task.id);
18102
+ if (commits.length > 0) {
18103
+ console.log(chalk.bold(`
18104
+ Commits (${commits.length}):`));
18105
+ for (const c of commits)
18106
+ console.log(` ${chalk.yellow(c.commit_hash.slice(0, 7))} ${c.message || ""}`);
18107
+ }
18108
+ } catch {}
18109
+ if (task.comments.length > 0) {
18110
+ console.log(chalk.bold(`
18111
+ Comments (${task.comments.length}):`));
18112
+ for (const c of task.comments) {
18113
+ const agent = c.agent_id ? chalk.cyan(`[${c.agent_id}] `) : "";
18114
+ console.log(` ${agent}${chalk.dim(c.created_at)}: ${c.content}`);
18115
+ }
18116
+ }
18117
+ if (task.checklist && task.checklist.length > 0) {
18118
+ const done = task.checklist.filter((c) => c.checked).length;
18119
+ console.log(chalk.bold(`
18120
+ Checklist (${done}/${task.checklist.length}):`));
18121
+ for (const item of task.checklist) {
18122
+ const icon = item.checked ? chalk.green("\u2611") : chalk.dim("\u2610");
18123
+ console.log(` ${icon} ${item.text || item.title}`);
18124
+ }
18125
+ }
18126
+ console.log();
18127
+ });
16877
18128
  program2.command("history <id>").description("Show change history for a task (audit log)").action((id) => {
16878
18129
  const globalOpts = program2.opts();
16879
18130
  const resolvedId = resolveTaskId(id);
@@ -17377,7 +18628,7 @@ program2.command("deps <id>").description("Manage task dependencies").option("--
17377
18628
  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
18629
  const globalOpts = program2.opts();
17379
18630
  if (opts.add) {
17380
- const projectPath = resolve3(opts.add);
18631
+ const projectPath = resolve4(opts.add);
17381
18632
  const name = opts.name || basename(projectPath);
17382
18633
  const existing = getProjectByPath(projectPath);
17383
18634
  let project;
@@ -17414,6 +18665,55 @@ program2.command("projects").description("List and manage projects").option("--a
17414
18665
  console.log(`${chalk.dim(p.id.slice(0, 8))} ${chalk.bold(p.name)} ${chalk.dim(p.path)}${taskList}${p.description ? ` - ${p.description}` : ""}`);
17415
18666
  }
17416
18667
  });
18668
+ 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) => {
18669
+ try {
18670
+ const globalOpts = program2.opts();
18671
+ const projectId = autoProject(globalOpts);
18672
+ const { extractTodos: extractTodos2, EXTRACT_TAGS: EXTRACT_TAGS2 } = (init_extract(), __toCommonJS(exports_extract));
18673
+ const patterns = opts.pattern ? opts.pattern.split(",").map((t) => t.trim().toUpperCase()) : undefined;
18674
+ const taskListId = opts.list ? (() => {
18675
+ const db = getDatabase();
18676
+ const id = resolvePartialId(db, "task_lists", opts.list);
18677
+ if (!id) {
18678
+ console.error(chalk.red(`Could not resolve task list ID: ${opts.list}`));
18679
+ process.exit(1);
18680
+ }
18681
+ return id;
18682
+ })() : undefined;
18683
+ const result = extractTodos2({
18684
+ path: resolve4(scanPath),
18685
+ patterns,
18686
+ project_id: projectId,
18687
+ task_list_id: taskListId,
18688
+ tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
18689
+ assigned_to: opts.assign,
18690
+ agent_id: globalOpts.agent,
18691
+ dry_run: opts.dryRun,
18692
+ extensions: opts.ext ? opts.ext.split(",").map((e) => e.trim()) : undefined
18693
+ });
18694
+ if (globalOpts.json) {
18695
+ console.log(JSON.stringify(opts.dryRun ? { comments: result.comments } : { tasks_created: result.tasks.length, skipped: result.skipped, comments: result.comments.length }, null, 2));
18696
+ } else if (opts.dryRun) {
18697
+ console.log(chalk.cyan(`Found ${result.comments.length} comment(s):
18698
+ `));
18699
+ for (const c of result.comments) {
18700
+ console.log(` ${chalk.yellow(`[${c.tag}]`)} ${c.message}`);
18701
+ console.log(` ${chalk.gray(`${c.file}:${c.line}`)}`);
18702
+ }
18703
+ } else {
18704
+ console.log(chalk.green(`Created ${result.tasks.length} task(s)`));
18705
+ if (result.skipped > 0) {
18706
+ console.log(chalk.gray(`Skipped ${result.skipped} duplicate(s)`));
18707
+ }
18708
+ console.log(chalk.gray(`Total comments found: ${result.comments.length}`));
18709
+ for (const t of result.tasks) {
18710
+ console.log(formatTaskLine(t));
18711
+ }
18712
+ }
18713
+ } catch (e) {
18714
+ handleError(e);
18715
+ }
18716
+ });
17417
18717
  program2.command("export").description("Export tasks").option("-f, --format <format>", "Format: json or md", "json").action((opts) => {
17418
18718
  const globalOpts = program2.opts();
17419
18719
  const projectId = autoProject(globalOpts);
@@ -17479,11 +18779,11 @@ var hooks = program2.command("hooks").description("Manage Claude Code hook integ
17479
18779
  hooks.command("install").description("Install Claude Code hooks for auto-sync").action(() => {
17480
18780
  let todosBin = "todos";
17481
18781
  try {
17482
- const p = execSync("which todos", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
18782
+ const p = execSync2("which todos", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
17483
18783
  if (p)
17484
18784
  todosBin = p;
17485
18785
  } catch {}
17486
- const hooksDir = join8(process.cwd(), ".claude", "hooks");
18786
+ const hooksDir = join9(process.cwd(), ".claude", "hooks");
17487
18787
  if (!existsSync7(hooksDir))
17488
18788
  mkdirSync3(hooksDir, { recursive: true });
17489
18789
  const hookScript = `#!/usr/bin/env bash
@@ -17509,11 +18809,11 @@ esac
17509
18809
 
17510
18810
  exit 0
17511
18811
  `;
17512
- const hookPath = join8(hooksDir, "todos-sync.sh");
18812
+ const hookPath = join9(hooksDir, "todos-sync.sh");
17513
18813
  writeFileSync3(hookPath, hookScript);
17514
- execSync(`chmod +x "${hookPath}"`);
18814
+ execSync2(`chmod +x "${hookPath}"`);
17515
18815
  console.log(chalk.green(`Hook script created: ${hookPath}`));
17516
- const settingsPath = join8(process.cwd(), ".claude", "settings.json");
18816
+ const settingsPath = join9(process.cwd(), ".claude", "settings.json");
17517
18817
  const settings = readJsonFile2(settingsPath);
17518
18818
  if (!settings["hooks"]) {
17519
18819
  settings["hooks"] = {};
@@ -17556,11 +18856,11 @@ program2.command("mcp").description("Start MCP server (stdio)").option("--regist
17556
18856
  var HOME2 = process.env["HOME"] || process.env["USERPROFILE"] || "~";
17557
18857
  function getMcpBinaryPath() {
17558
18858
  try {
17559
- const p = execSync("which todos-mcp", { encoding: "utf-8" }).trim();
18859
+ const p = execSync2("which todos-mcp", { encoding: "utf-8" }).trim();
17560
18860
  if (p)
17561
18861
  return p;
17562
18862
  } catch {}
17563
- const bunBin = join8(HOME2, ".bun", "bin", "todos-mcp");
18863
+ const bunBin = join9(HOME2, ".bun", "bin", "todos-mcp");
17564
18864
  if (existsSync7(bunBin))
17565
18865
  return bunBin;
17566
18866
  return "todos-mcp";
@@ -17569,7 +18869,7 @@ function readJsonFile2(path) {
17569
18869
  if (!existsSync7(path))
17570
18870
  return {};
17571
18871
  try {
17572
- return JSON.parse(readFileSync4(path, "utf-8"));
18872
+ return JSON.parse(readFileSync5(path, "utf-8"));
17573
18873
  } catch {
17574
18874
  return {};
17575
18875
  }
@@ -17584,7 +18884,7 @@ function writeJsonFile2(path, data) {
17584
18884
  function readTomlFile(path) {
17585
18885
  if (!existsSync7(path))
17586
18886
  return "";
17587
- return readFileSync4(path, "utf-8");
18887
+ return readFileSync5(path, "utf-8");
17588
18888
  }
17589
18889
  function writeTomlFile(path, content) {
17590
18890
  const dir = dirname4(path);
@@ -17596,8 +18896,8 @@ function registerClaude(binPath, global) {
17596
18896
  const scope = global ? "user" : "project";
17597
18897
  const cmd = `claude mcp add --transport stdio --scope ${scope} todos -- ${binPath}`;
17598
18898
  try {
17599
- const { execSync: execSync2 } = __require("child_process");
17600
- execSync2(cmd, { stdio: "pipe" });
18899
+ const { execSync: execSync3 } = __require("child_process");
18900
+ execSync3(cmd, { stdio: "pipe" });
17601
18901
  console.log(chalk.green(`Claude Code (${scope}): registered via 'claude mcp add'`));
17602
18902
  } catch {
17603
18903
  console.log(chalk.yellow(`Claude Code: could not auto-register. Run this command manually:`));
@@ -17606,8 +18906,8 @@ function registerClaude(binPath, global) {
17606
18906
  }
17607
18907
  function unregisterClaude(_global) {
17608
18908
  try {
17609
- const { execSync: execSync2 } = __require("child_process");
17610
- execSync2("claude mcp remove todos", { stdio: "pipe" });
18909
+ const { execSync: execSync3 } = __require("child_process");
18910
+ execSync3("claude mcp remove todos", { stdio: "pipe" });
17611
18911
  console.log(chalk.green(`Claude Code: removed todos MCP server`));
17612
18912
  } catch {
17613
18913
  console.log(chalk.yellow(`Claude Code: could not auto-remove. Run manually:`));
@@ -17615,7 +18915,7 @@ function unregisterClaude(_global) {
17615
18915
  }
17616
18916
  }
17617
18917
  function registerCodex(binPath) {
17618
- const configPath = join8(HOME2, ".codex", "config.toml");
18918
+ const configPath = join9(HOME2, ".codex", "config.toml");
17619
18919
  let content = readTomlFile(configPath);
17620
18920
  content = removeTomlBlock(content, "mcp_servers.todos");
17621
18921
  const block = `
@@ -17629,7 +18929,7 @@ args = []
17629
18929
  console.log(chalk.green(`Codex CLI: registered in ${configPath}`));
17630
18930
  }
17631
18931
  function unregisterCodex() {
17632
- const configPath = join8(HOME2, ".codex", "config.toml");
18932
+ const configPath = join9(HOME2, ".codex", "config.toml");
17633
18933
  let content = readTomlFile(configPath);
17634
18934
  if (!content.includes("[mcp_servers.todos]")) {
17635
18935
  console.log(chalk.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -17662,7 +18962,7 @@ function removeTomlBlock(content, blockName) {
17662
18962
  `);
17663
18963
  }
17664
18964
  function registerGemini(binPath) {
17665
- const configPath = join8(HOME2, ".gemini", "settings.json");
18965
+ const configPath = join9(HOME2, ".gemini", "settings.json");
17666
18966
  const config = readJsonFile2(configPath);
17667
18967
  if (!config["mcpServers"]) {
17668
18968
  config["mcpServers"] = {};
@@ -17676,7 +18976,7 @@ function registerGemini(binPath) {
17676
18976
  console.log(chalk.green(`Gemini CLI: registered in ${configPath}`));
17677
18977
  }
17678
18978
  function unregisterGemini() {
17679
- const configPath = join8(HOME2, ".gemini", "settings.json");
18979
+ const configPath = join9(HOME2, ".gemini", "settings.json");
17680
18980
  const config = readJsonFile2(configPath);
17681
18981
  const servers = config["mcpServers"];
17682
18982
  if (!servers || !("todos" in servers)) {
@@ -17724,6 +19024,115 @@ function unregisterMcp(agent, global) {
17724
19024
  }
17725
19025
  }
17726
19026
  }
19027
+ 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) => {
19028
+ const globalOpts = program2.opts();
19029
+ const { parseGitHubUrl: parseGitHubUrl2, fetchGitHubIssue: fetchGitHubIssue2, issueToTask: issueToTask2 } = (init_github(), __toCommonJS(exports_github));
19030
+ const parsed = parseGitHubUrl2(url);
19031
+ if (!parsed) {
19032
+ console.error(chalk.red("Invalid GitHub issue URL. Expected: https://github.com/owner/repo/issues/123"));
19033
+ process.exit(1);
19034
+ }
19035
+ try {
19036
+ const issue = fetchGitHubIssue2(parsed.owner, parsed.repo, parsed.number);
19037
+ const projectId = opts.project || autoProject(globalOpts) || undefined;
19038
+ const input = issueToTask2(issue, { project_id: projectId, task_list_id: opts.list });
19039
+ const task = createTask(input);
19040
+ if (globalOpts.json) {
19041
+ output(task, true);
19042
+ return;
19043
+ }
19044
+ console.log(chalk.green(`Imported GH#${issue.number}: ${issue.title}`));
19045
+ console.log(` ${chalk.dim("Task ID:")} ${task.short_id || task.id}`);
19046
+ console.log(` ${chalk.dim("Labels:")} ${issue.labels.join(", ") || "none"}`);
19047
+ console.log(` ${chalk.dim("Priority:")} ${task.priority}`);
19048
+ } catch (e) {
19049
+ if (e.message?.includes("gh")) {
19050
+ console.error(chalk.red("GitHub CLI (gh) not found or not authenticated. Install: https://cli.github.com"));
19051
+ } else {
19052
+ console.error(chalk.red(`Import failed: ${e.message}`));
19053
+ }
19054
+ process.exit(1);
19055
+ }
19056
+ });
19057
+ 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) => {
19058
+ const globalOpts = program2.opts();
19059
+ const resolvedId = resolveTaskId(taskId);
19060
+ const { linkTaskToCommit: linkTaskToCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
19061
+ const commit = linkTaskToCommit2({
19062
+ task_id: resolvedId,
19063
+ sha,
19064
+ message: opts.message,
19065
+ author: opts.author,
19066
+ files_changed: opts.files ? opts.files.split(",").filter(Boolean) : undefined
19067
+ });
19068
+ if (globalOpts.json) {
19069
+ output(commit, true);
19070
+ return;
19071
+ }
19072
+ console.log(chalk.green(`Linked commit ${sha.slice(0, 7)} to task ${taskId}`));
19073
+ });
19074
+ var hookCmd = program2.command("hook").description("Manage git hooks for auto-linking commits to tasks");
19075
+ hookCmd.command("install").description("Install post-commit hook that auto-links commits to tasks").action(() => {
19076
+ const { execSync: execSync3 } = __require("child_process");
19077
+ try {
19078
+ const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
19079
+ const hookPath = `${gitDir}/hooks/post-commit`;
19080
+ const { existsSync: existsSync8, readFileSync: readFileSync6, writeFileSync: writeFileSync4, chmodSync } = __require("fs");
19081
+ const marker = "# todos-auto-link";
19082
+ if (existsSync8(hookPath)) {
19083
+ const existing = readFileSync6(hookPath, "utf-8");
19084
+ if (existing.includes(marker)) {
19085
+ console.log(chalk.yellow("Hook already installed."));
19086
+ return;
19087
+ }
19088
+ writeFileSync4(hookPath, existing + `
19089
+ ${marker}
19090
+ $(dirname "$0")/../../scripts/post-commit-hook.sh
19091
+ `);
19092
+ } else {
19093
+ writeFileSync4(hookPath, `#!/usr/bin/env bash
19094
+ ${marker}
19095
+ $(dirname "$0")/../../scripts/post-commit-hook.sh
19096
+ `);
19097
+ chmodSync(hookPath, 493);
19098
+ }
19099
+ console.log(chalk.green("Post-commit hook installed. Commits with task IDs (e.g. OPE-00042) will auto-link."));
19100
+ } catch (e) {
19101
+ console.error(chalk.red("Not in a git repository or hook install failed."));
19102
+ process.exit(1);
19103
+ }
19104
+ });
19105
+ hookCmd.command("uninstall").description("Remove the todos post-commit hook").action(() => {
19106
+ const { execSync: execSync3 } = __require("child_process");
19107
+ try {
19108
+ const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
19109
+ const hookPath = `${gitDir}/hooks/post-commit`;
19110
+ const { existsSync: existsSync8, readFileSync: readFileSync6, writeFileSync: writeFileSync4 } = __require("fs");
19111
+ const marker = "# todos-auto-link";
19112
+ if (!existsSync8(hookPath)) {
19113
+ console.log(chalk.dim("No post-commit hook found."));
19114
+ return;
19115
+ }
19116
+ const content = readFileSync6(hookPath, "utf-8");
19117
+ if (!content.includes(marker)) {
19118
+ console.log(chalk.dim("Hook not managed by todos."));
19119
+ return;
19120
+ }
19121
+ const cleaned = content.split(`
19122
+ `).filter((l) => !l.includes(marker) && !l.includes("post-commit-hook.sh")).join(`
19123
+ `).trim();
19124
+ if (cleaned === "#!/usr/bin/env bash" || cleaned === "") {
19125
+ __require("fs").unlinkSync(hookPath);
19126
+ } else {
19127
+ writeFileSync4(hookPath, cleaned + `
19128
+ `);
19129
+ }
19130
+ console.log(chalk.green("Post-commit hook removed."));
19131
+ } catch (e) {
19132
+ console.error(chalk.red("Not in a git repository or hook removal failed."));
19133
+ process.exit(1);
19134
+ }
19135
+ });
17727
19136
  program2.command("init <name>").description("Register an agent and get a short UUID").option("-d, --description <text>", "Agent description").action((name, opts) => {
17728
19137
  const globalOpts = program2.opts();
17729
19138
  try {
@@ -17765,6 +19174,30 @@ program2.command("heartbeat [agent]").description("Update last_seen_at to signal
17765
19174
  console.log(chalk.green(`\u2665 ${a.name} (${a.id.slice(0, 8)}) \u2014 heartbeat sent`));
17766
19175
  }
17767
19176
  });
19177
+ 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) => {
19178
+ const globalOpts = program2.opts();
19179
+ const agentId = agent || globalOpts.agent;
19180
+ if (!agentId) {
19181
+ console.error(chalk.red("Agent ID or name required. Use --agent or pass as argument."));
19182
+ process.exit(1);
19183
+ }
19184
+ const { getAgent: getAgent2, getAgentByName: getAgentByName2 } = (init_agents(), __toCommonJS(exports_agents));
19185
+ const a = getAgent2(agentId) || getAgentByName2(agentId);
19186
+ if (!a) {
19187
+ console.error(chalk.red(`Agent not found: ${agentId}`));
19188
+ process.exit(1);
19189
+ }
19190
+ const released = releaseAgent(a.id, opts?.sessionId);
19191
+ if (!released) {
19192
+ console.error(chalk.red("Release denied: session_id does not match agent's current session."));
19193
+ process.exit(1);
19194
+ }
19195
+ if (globalOpts.json) {
19196
+ console.log(JSON.stringify({ agent_id: a.id, name: a.name, released: true }));
19197
+ } else {
19198
+ console.log(chalk.green(`\u2713 ${a.name} (${a.id}) released \u2014 name is now available.`));
19199
+ }
19200
+ });
17768
19201
  program2.command("focus [project]").description("Focus on a project (or clear focus if no project given)").action((project) => {
17769
19202
  const globalOpts = program2.opts();
17770
19203
  const agentId = globalOpts.agent;
@@ -17990,13 +19423,13 @@ Update available: ${currentVersion} \u2192 ${latestVersion}`));
17990
19423
  }
17991
19424
  let useBun = false;
17992
19425
  try {
17993
- execSync("which bun", { stdio: "ignore" });
19426
+ execSync2("which bun", { stdio: "ignore" });
17994
19427
  useBun = true;
17995
19428
  } catch {}
17996
19429
  const cmd = useBun ? "bun add -g @hasna/todos@latest" : "npm install -g @hasna/todos@latest";
17997
19430
  console.log(chalk.dim(`
17998
19431
  Running: ${cmd}`));
17999
- execSync(cmd, { stdio: "inherit" });
19432
+ execSync2(cmd, { stdio: "inherit" });
18000
19433
  console.log(chalk.green(`
18001
19434
  Updated to ${latestVersion}!`));
18002
19435
  } catch (e) {
@@ -18005,7 +19438,7 @@ Updated to ${latestVersion}!`));
18005
19438
  });
18006
19439
  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
19440
  const globalOpts = program2.opts();
18008
- const configPath = join8(process.env["HOME"] || "~", ".todos", "config.json");
19441
+ const configPath = join9(process.env["HOME"] || "~", ".todos", "config.json");
18009
19442
  if (opts.get) {
18010
19443
  const config2 = loadConfig();
18011
19444
  const keys = opts.get.split(".");
@@ -18031,7 +19464,7 @@ program2.command("config").description("View or update configuration").option("-
18031
19464
  }
18032
19465
  let config2 = {};
18033
19466
  try {
18034
- config2 = JSON.parse(readFileSync4(configPath, "utf-8"));
19467
+ config2 = JSON.parse(readFileSync5(configPath, "utf-8"));
18035
19468
  } catch {}
18036
19469
  const keys = key.split(".");
18037
19470
  let obj = config2;
@@ -18188,6 +19621,51 @@ program2.command("interactive").description("Launch interactive TUI").action(asy
18188
19621
  const projectId = autoProject(globalOpts);
18189
19622
  renderApp2(projectId);
18190
19623
  });
19624
+ program2.command("blame <file>").description("Show which tasks/agents touched a file and why \u2014 combines task_files + task_commits").action((filePath) => {
19625
+ const globalOpts = program2.opts();
19626
+ const { findTasksByFile: findTasksByFile2 } = (init_task_files(), __toCommonJS(exports_task_files));
19627
+ const { getTask: getTask2 } = (init_tasks(), __toCommonJS(exports_tasks));
19628
+ const db = getDatabase();
19629
+ const taskFiles = findTasksByFile2(filePath, db);
19630
+ 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}%`);
19631
+ if (globalOpts.json) {
19632
+ output({ file: filePath, task_files: taskFiles, commits: commitRows }, true);
19633
+ return;
19634
+ }
19635
+ console.log(chalk.bold(`
19636
+ Blame: ${filePath}
19637
+ `));
19638
+ if (taskFiles.length > 0) {
19639
+ console.log(chalk.bold("Task File Links:"));
19640
+ for (const tf of taskFiles) {
19641
+ const task = getTask2(tf.task_id, db);
19642
+ const title = task ? task.title : "unknown";
19643
+ const sid = task?.short_id || tf.task_id.slice(0, 8);
19644
+ console.log(` ${chalk.cyan(sid)} ${title} \u2014 ${chalk.dim(tf.role || "file")} ${chalk.dim(tf.updated_at)}`);
19645
+ }
19646
+ }
19647
+ if (commitRows.length > 0) {
19648
+ console.log(chalk.bold(`
19649
+ Commit Links (${commitRows.length}):`));
19650
+ for (const c of commitRows) {
19651
+ const sid = c.short_id || c.task_id.slice(0, 8);
19652
+ console.log(` ${chalk.yellow(c.sha?.slice(0, 7) || "?")} ${chalk.cyan(sid)} ${c.title || ""} \u2014 ${chalk.dim(c.author || "")} ${chalk.dim(c.committed_at || "")}`);
19653
+ }
19654
+ }
19655
+ if (taskFiles.length === 0 && commitRows.length === 0) {
19656
+ console.log(chalk.dim("No task or commit links found for this file."));
19657
+ console.log(chalk.dim("Use 'todos hook install' to auto-link future commits."));
19658
+ }
19659
+ console.log();
19660
+ });
19661
+ 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) => {
19662
+ const { render: render2 } = await import("ink");
19663
+ const React = await import("react");
19664
+ const { Dashboard: Dashboard2 } = await Promise.resolve().then(() => (init_Dashboard(), exports_Dashboard));
19665
+ const globalOpts = program2.opts();
19666
+ const projectId = opts.project || autoProject(globalOpts) || undefined;
19667
+ render2(React.createElement(Dashboard2, { projectId, refreshMs: parseInt(opts.refresh, 10) }));
19668
+ });
18191
19669
  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
19670
  const db = getDatabase();
18193
19671
  const filters = {};
@@ -18253,6 +19731,120 @@ Next up:`));
18253
19731
  console.log(` ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${chalk.yellow(t.priority)} ${t.title}`);
18254
19732
  }
18255
19733
  });
19734
+ 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) => {
19735
+ const globalOpts = program2.opts();
19736
+ const { getRecap: getRecap2 } = (init_audit(), __toCommonJS(exports_audit));
19737
+ const recap = getRecap2(parseInt(opts.hours, 10), opts.project);
19738
+ if (globalOpts.json) {
19739
+ output(recap, true);
19740
+ return;
19741
+ }
19742
+ console.log(chalk.bold(`
19743
+ Recap \u2014 last ${recap.hours} hours (since ${new Date(recap.since).toLocaleString()})
19744
+ `));
19745
+ if (recap.completed.length > 0) {
19746
+ console.log(chalk.green.bold(`Completed (${recap.completed.length}):`));
19747
+ for (const t of recap.completed) {
19748
+ const id = t.short_id || t.id.slice(0, 8);
19749
+ const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
19750
+ console.log(` ${chalk.green("\u2713")} ${chalk.cyan(id)} ${t.title}${dur}${t.assigned_to ? ` \u2014 ${chalk.dim(t.assigned_to)}` : ""}`);
19751
+ }
19752
+ } else {
19753
+ console.log(chalk.dim("No tasks completed in this period."));
19754
+ }
19755
+ if (recap.in_progress.length > 0) {
19756
+ console.log(chalk.blue.bold(`
19757
+ In Progress (${recap.in_progress.length}):`));
19758
+ for (const t of recap.in_progress) {
19759
+ const id = t.short_id || t.id.slice(0, 8);
19760
+ console.log(` ${chalk.blue("\u2192")} ${chalk.cyan(id)} ${t.title}${t.assigned_to ? ` \u2014 ${chalk.dim(t.assigned_to)}` : ""}`);
19761
+ }
19762
+ }
19763
+ if (recap.blocked.length > 0) {
19764
+ console.log(chalk.red.bold(`
19765
+ Blocked (${recap.blocked.length}):`));
19766
+ for (const t of recap.blocked) {
19767
+ const id = t.short_id || t.id.slice(0, 8);
19768
+ console.log(` ${chalk.red("\u2717")} ${chalk.cyan(id)} ${t.title}`);
19769
+ }
19770
+ }
19771
+ if (recap.stale.length > 0) {
19772
+ console.log(chalk.yellow.bold(`
19773
+ Stale (${recap.stale.length}):`));
19774
+ for (const t of recap.stale) {
19775
+ const id = t.short_id || t.id.slice(0, 8);
19776
+ const ago = Math.round((Date.now() - new Date(t.updated_at).getTime()) / 60000);
19777
+ console.log(` ${chalk.yellow("!")} ${chalk.cyan(id)} ${t.title} \u2014 last update ${ago}m ago`);
19778
+ }
19779
+ }
19780
+ if (recap.created.length > 0) {
19781
+ console.log(chalk.dim.bold(`
19782
+ Created (${recap.created.length}):`));
19783
+ for (const t of recap.created.slice(0, 10)) {
19784
+ const id = t.short_id || t.id.slice(0, 8);
19785
+ console.log(` ${chalk.dim("+")} ${chalk.cyan(id)} ${t.title}`);
19786
+ }
19787
+ if (recap.created.length > 10)
19788
+ console.log(chalk.dim(` ... and ${recap.created.length - 10} more`));
19789
+ }
19790
+ if (recap.agents.length > 0) {
19791
+ console.log(chalk.bold(`
19792
+ Agents:`));
19793
+ for (const a of recap.agents) {
19794
+ const seen = Math.round((Date.now() - new Date(a.last_seen_at).getTime()) / 60000);
19795
+ console.log(` ${a.name}: ${chalk.green(a.completed_count + " done")} | ${chalk.blue(a.in_progress_count + " active")} | last seen ${seen}m ago`);
19796
+ }
19797
+ }
19798
+ console.log();
19799
+ });
19800
+ 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) => {
19801
+ const globalOpts = program2.opts();
19802
+ const sinceDate = opts.since === "yesterday" || !opts.since ? new Date(Date.now() - 24 * 60 * 60 * 1000) : new Date(opts.since);
19803
+ const hours = Math.max(1, Math.round((Date.now() - sinceDate.getTime()) / (60 * 60 * 1000)));
19804
+ const { getRecap: getRecap2 } = (init_audit(), __toCommonJS(exports_audit));
19805
+ const recap = getRecap2(hours, opts.project);
19806
+ if (globalOpts.json) {
19807
+ output(recap, true);
19808
+ return;
19809
+ }
19810
+ console.log(chalk.bold(`
19811
+ Standup \u2014 since ${sinceDate.toLocaleDateString()}
19812
+ `));
19813
+ const byAgent = new Map;
19814
+ for (const t of recap.completed) {
19815
+ const agent = t.assigned_to || "unassigned";
19816
+ if (!byAgent.has(agent))
19817
+ byAgent.set(agent, []);
19818
+ byAgent.get(agent).push(t);
19819
+ }
19820
+ if (byAgent.size > 0) {
19821
+ console.log(chalk.green.bold("Done:"));
19822
+ for (const [agent, tasks] of byAgent) {
19823
+ console.log(` ${chalk.cyan(agent)}:`);
19824
+ for (const t of tasks) {
19825
+ const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
19826
+ console.log(` ${chalk.green("\u2713")} ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}`);
19827
+ }
19828
+ }
19829
+ } else {
19830
+ console.log(chalk.dim("Nothing completed."));
19831
+ }
19832
+ if (recap.in_progress.length > 0) {
19833
+ console.log(chalk.blue.bold(`
19834
+ In Progress:`));
19835
+ for (const t of recap.in_progress) {
19836
+ console.log(` ${chalk.blue("\u2192")} ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${chalk.dim(t.assigned_to)}` : ""}`);
19837
+ }
19838
+ }
19839
+ if (recap.blocked.length > 0) {
19840
+ console.log(chalk.red.bold(`
19841
+ Blocked:`));
19842
+ for (const t of recap.blocked) {
19843
+ console.log(` ${chalk.red("\u2717")} ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
19844
+ }
19845
+ }
19846
+ console.log();
19847
+ });
18256
19848
  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
19849
  const db = getDatabase();
18258
19850
  const resolvedId = resolvePartialId(db, "tasks", id);
@@ -18541,11 +20133,11 @@ program2.command("health").description("Check todos system health \u2014 databas
18541
20133
  try {
18542
20134
  const db = getDatabase();
18543
20135
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
18544
- const { statSync: statSync2 } = __require("fs");
20136
+ const { statSync: statSync3 } = __require("fs");
18545
20137
  const dbPath = process.env["TODOS_DB_PATH"] || __require("path").join(process.env["HOME"] || "~", ".todos", "todos.db");
18546
20138
  let size = "unknown";
18547
20139
  try {
18548
- size = `${(statSync2(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
20140
+ size = `${(statSync3(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
18549
20141
  } catch {}
18550
20142
  checks.push({ name: "Database", ok: true, message: `${row.count} tasks \xB7 ${size} \xB7 ${chalk.dim(dbPath)}` });
18551
20143
  } catch (e) {