@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/index.js CHANGED
@@ -746,6 +746,15 @@ var MIGRATIONS = [
746
746
  CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
747
747
  CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
748
748
  INSERT OR IGNORE INTO _migrations (id) VALUES (33);
749
+ `,
750
+ `
751
+ ALTER TABLE tasks ADD COLUMN started_at TEXT;
752
+ INSERT OR IGNORE INTO _migrations (id) VALUES (34);
753
+ `,
754
+ `
755
+ ALTER TABLE tasks ADD COLUMN task_type TEXT;
756
+ CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type);
757
+ INSERT OR IGNORE INTO _migrations (id) VALUES (35);
749
758
  `
750
759
  ];
751
760
  var _db = null;
@@ -926,6 +935,7 @@ function ensureSchema(db) {
926
935
  ensureColumn("tasks", "spawned_from_session", "TEXT");
927
936
  ensureColumn("tasks", "assigned_by", "TEXT");
928
937
  ensureColumn("tasks", "assigned_from_project", "TEXT");
938
+ ensureColumn("tasks", "started_at", "TEXT");
929
939
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
930
940
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
931
941
  ensureColumn("agents", "reports_to", "TEXT");
@@ -1043,6 +1053,16 @@ function resolvePartialId(db, table, partialId) {
1043
1053
  return shortIdRows[0].id;
1044
1054
  }
1045
1055
  }
1056
+ if (table === "task_lists") {
1057
+ const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
1058
+ if (slugRow)
1059
+ return slugRow.id;
1060
+ }
1061
+ if (table === "projects") {
1062
+ const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
1063
+ if (nameRow)
1064
+ return nameRow.id;
1065
+ }
1046
1066
  return null;
1047
1067
  }
1048
1068
  // src/types/index.ts
@@ -1484,6 +1504,32 @@ function getRecentActivity(limit = 50, db) {
1484
1504
  const d = db || getDatabase();
1485
1505
  return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
1486
1506
  }
1507
+ function getRecap(hours = 8, projectId, db) {
1508
+ const d = db || getDatabase();
1509
+ const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
1510
+ const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
1511
+ const pf = projectId ? " AND project_id = ?" : "";
1512
+ const tpf = projectId ? " AND t.project_id = ?" : "";
1513
+ 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);
1514
+ 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);
1515
+ 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();
1516
+ 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();
1517
+ 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);
1518
+ 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);
1519
+ return {
1520
+ hours,
1521
+ since,
1522
+ completed: completed.map((r) => ({
1523
+ ...r,
1524
+ duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
1525
+ })),
1526
+ created,
1527
+ in_progress,
1528
+ blocked,
1529
+ stale,
1530
+ agents
1531
+ };
1532
+ }
1487
1533
 
1488
1534
  // src/lib/recurrence.ts
1489
1535
  var DAY_NAMES = {
@@ -1762,8 +1808,8 @@ function createTask(input, db) {
1762
1808
  const title = shortId ? `${shortId}: ${input.title}` : input.title;
1763
1809
  const assignedBy = input.assigned_by || input.agent_id;
1764
1810
  const assignedFromProject = input.assigned_from_project || null;
1765
- 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)
1766
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1811
+ 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)
1812
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1767
1813
  id,
1768
1814
  shortId,
1769
1815
  input.project_id || null,
@@ -1793,7 +1839,8 @@ function createTask(input, db) {
1793
1839
  input.reason || null,
1794
1840
  input.spawned_from_session || null,
1795
1841
  assignedBy || null,
1796
- assignedFromProject || null
1842
+ assignedFromProject || null,
1843
+ input.task_type || null
1797
1844
  ]);
1798
1845
  if (tags.length > 0) {
1799
1846
  insertTaskTags(id, tags, d);
@@ -1902,6 +1949,15 @@ function listTasks(filter = {}, db) {
1902
1949
  } else if (filter.has_recurrence === false) {
1903
1950
  conditions.push("recurrence_rule IS NULL");
1904
1951
  }
1952
+ if (filter.task_type) {
1953
+ if (Array.isArray(filter.task_type)) {
1954
+ conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
1955
+ params.push(...filter.task_type);
1956
+ } else {
1957
+ conditions.push("task_type = ?");
1958
+ params.push(filter.task_type);
1959
+ }
1960
+ }
1905
1961
  const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
1906
1962
  if (filter.cursor) {
1907
1963
  try {
@@ -2061,6 +2117,10 @@ function updateTask(id, input, db) {
2061
2117
  sets.push("recurrence_rule = ?");
2062
2118
  params.push(input.recurrence_rule);
2063
2119
  }
2120
+ if (input.task_type !== undefined) {
2121
+ sets.push("task_type = ?");
2122
+ params.push(input.task_type ?? null);
2123
+ }
2064
2124
  params.push(id, input.version);
2065
2125
  const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
2066
2126
  if (result.changes === 0) {
@@ -2130,8 +2190,8 @@ function startTask(id, agentId, db) {
2130
2190
  }
2131
2191
  const cutoff = lockExpiryCutoff();
2132
2192
  const timestamp = now();
2133
- const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
2134
- WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, id, agentId, cutoff]);
2193
+ 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 = ?
2194
+ WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
2135
2195
  if (result.changes === 0) {
2136
2196
  if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
2137
2197
  throw new LockError(id, task.locked_by);
@@ -2139,7 +2199,7 @@ function startTask(id, agentId, db) {
2139
2199
  }
2140
2200
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
2141
2201
  dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
2142
- return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
2202
+ 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 };
2143
2203
  }
2144
2204
  function completeTask(id, agentId, db, options) {
2145
2205
  const d = db || getDatabase();
@@ -2877,9 +2937,26 @@ function deleteComment(id, db) {
2877
2937
  return result.changes > 0;
2878
2938
  }
2879
2939
  // src/db/agents.ts
2880
- var AGENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
2940
+ function getActiveWindowMs() {
2941
+ const env = process.env["TODOS_AGENT_TIMEOUT_MS"];
2942
+ if (env) {
2943
+ const parsed = parseInt(env, 10);
2944
+ if (!isNaN(parsed) && parsed > 0)
2945
+ return parsed;
2946
+ }
2947
+ return 30 * 60 * 1000;
2948
+ }
2949
+ function autoReleaseStaleAgents(db) {
2950
+ if (process.env["TODOS_AGENT_AUTO_RELEASE"] !== "true")
2951
+ return 0;
2952
+ const d = db || getDatabase();
2953
+ const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
2954
+ const result = d.run("UPDATE agents SET session_id = NULL WHERE status = 'active' AND session_id IS NOT NULL AND last_seen_at < ?", [cutoff]);
2955
+ return result.changes;
2956
+ }
2881
2957
  function getAvailableNamesFromPool(pool, db) {
2882
- const cutoff = new Date(Date.now() - AGENT_ACTIVE_WINDOW_MS).toISOString();
2958
+ autoReleaseStaleAgents(db);
2959
+ const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
2883
2960
  const activeNames = new Set(db.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
2884
2961
  return pool.filter((name) => !activeNames.has(name.toLowerCase()));
2885
2962
  }
@@ -2901,22 +2978,19 @@ function registerAgent(input, db) {
2901
2978
  const existing = getAgentByName(normalizedName, d);
2902
2979
  if (existing) {
2903
2980
  const lastSeenMs = new Date(existing.last_seen_at).getTime();
2904
- const isActive = Date.now() - lastSeenMs < AGENT_ACTIVE_WINDOW_MS;
2981
+ const activeWindowMs = getActiveWindowMs();
2982
+ const isActive = Date.now() - lastSeenMs < activeWindowMs;
2905
2983
  const sameSession = input.session_id && existing.session_id && input.session_id === existing.session_id;
2906
2984
  const differentSession = input.session_id && existing.session_id && input.session_id !== existing.session_id;
2907
- if (isActive && differentSession) {
2908
- const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
2909
- const suggestions = input.pool ? getAvailableNamesFromPool(input.pool, d) : [];
2910
- return {
2911
- conflict: true,
2912
- existing_id: existing.id,
2913
- existing_name: existing.name,
2914
- last_seen_at: existing.last_seen_at,
2915
- session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
2916
- working_dir: existing.working_dir,
2917
- suggestions: suggestions.slice(0, 5),
2918
- 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(", ")}` : ""}`
2919
- };
2985
+ const callerHasNoSession = !input.session_id;
2986
+ const existingHasActiveSession = existing.session_id && isActive;
2987
+ if (!input.force) {
2988
+ if (isActive && differentSession) {
2989
+ return buildConflictError(existing, lastSeenMs, input.pool, d);
2990
+ }
2991
+ if (callerHasNoSession && existingHasActiveSession) {
2992
+ return buildConflictError(existing, lastSeenMs, input.pool, d);
2993
+ }
2920
2994
  }
2921
2995
  const updates = ["last_seen_at = ?", "status = 'active'"];
2922
2996
  const params = [now()];
@@ -2961,6 +3035,32 @@ function registerAgent(input, db) {
2961
3035
  function isAgentConflict(result) {
2962
3036
  return result.conflict === true;
2963
3037
  }
3038
+ function buildConflictError(existing, lastSeenMs, pool, d) {
3039
+ const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
3040
+ const suggestions = pool ? getAvailableNamesFromPool(pool, d) : [];
3041
+ return {
3042
+ conflict: true,
3043
+ existing_id: existing.id,
3044
+ existing_name: existing.name,
3045
+ last_seen_at: existing.last_seen_at,
3046
+ session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
3047
+ working_dir: existing.working_dir,
3048
+ suggestions: suggestions.slice(0, 5),
3049
+ 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(", ")}` : ""}`
3050
+ };
3051
+ }
3052
+ function releaseAgent(id, session_id, db) {
3053
+ const d = db || getDatabase();
3054
+ const agent = getAgent(id, d);
3055
+ if (!agent)
3056
+ return false;
3057
+ if (session_id && agent.session_id && agent.session_id !== session_id) {
3058
+ return false;
3059
+ }
3060
+ const epoch = new Date(0).toISOString();
3061
+ d.run("UPDATE agents SET session_id = NULL, last_seen_at = ? WHERE id = ?", [epoch, id]);
3062
+ return true;
3063
+ }
2964
3064
  function getAgent(id, db) {
2965
3065
  const d = db || getDatabase();
2966
3066
  const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
@@ -2981,6 +3081,7 @@ function listAgents(opts, db) {
2981
3081
  includeArchived = opts?.include_archived ?? false;
2982
3082
  d = db || getDatabase();
2983
3083
  }
3084
+ autoReleaseStaleAgents(d);
2984
3085
  if (includeArchived) {
2985
3086
  return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
2986
3087
  }
@@ -2998,8 +3099,19 @@ function updateAgent(id, input, db) {
2998
3099
  const sets = ["last_seen_at = ?"];
2999
3100
  const params = [now()];
3000
3101
  if (input.name !== undefined) {
3102
+ const newName = input.name.trim().toLowerCase();
3103
+ const holder = getAgentByName(newName, d);
3104
+ if (holder && holder.id !== id) {
3105
+ const lastSeenMs = new Date(holder.last_seen_at).getTime();
3106
+ const isActive = Date.now() - lastSeenMs < getActiveWindowMs();
3107
+ if (isActive && holder.status === "active") {
3108
+ throw new Error(`Cannot rename: name "${newName}" is held by active agent ${holder.id} (last seen ${Math.round((Date.now() - lastSeenMs) / 60000)}m ago)`);
3109
+ }
3110
+ const evictedName = `${holder.name}__evicted_${holder.id}`;
3111
+ d.run("UPDATE agents SET name = ? WHERE id = ?", [evictedName, holder.id]);
3112
+ }
3001
3113
  sets.push("name = ?");
3002
- params.push(input.name.trim().toLowerCase());
3114
+ params.push(newName);
3003
3115
  }
3004
3116
  if (input.description !== undefined) {
3005
3117
  sets.push("description = ?");
@@ -4564,6 +4676,328 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
4564
4676
  }
4565
4677
  return { pushed, pulled, errors };
4566
4678
  }
4679
+ // src/lib/extract.ts
4680
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
4681
+ import { relative, resolve as resolve2, join as join6 } from "path";
4682
+ var EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
4683
+ var DEFAULT_EXTENSIONS = new Set([
4684
+ ".ts",
4685
+ ".tsx",
4686
+ ".js",
4687
+ ".jsx",
4688
+ ".mjs",
4689
+ ".cjs",
4690
+ ".py",
4691
+ ".rb",
4692
+ ".go",
4693
+ ".rs",
4694
+ ".c",
4695
+ ".cpp",
4696
+ ".h",
4697
+ ".hpp",
4698
+ ".java",
4699
+ ".kt",
4700
+ ".swift",
4701
+ ".cs",
4702
+ ".php",
4703
+ ".sh",
4704
+ ".bash",
4705
+ ".zsh",
4706
+ ".lua",
4707
+ ".sql",
4708
+ ".r",
4709
+ ".R",
4710
+ ".yaml",
4711
+ ".yml",
4712
+ ".toml",
4713
+ ".css",
4714
+ ".scss",
4715
+ ".less",
4716
+ ".vue",
4717
+ ".svelte",
4718
+ ".ex",
4719
+ ".exs",
4720
+ ".erl",
4721
+ ".hs",
4722
+ ".ml",
4723
+ ".mli",
4724
+ ".clj",
4725
+ ".cljs"
4726
+ ]);
4727
+ var SKIP_DIRS = new Set([
4728
+ "node_modules",
4729
+ ".git",
4730
+ "dist",
4731
+ "build",
4732
+ "out",
4733
+ ".next",
4734
+ ".turbo",
4735
+ "coverage",
4736
+ "__pycache__",
4737
+ ".venv",
4738
+ "venv",
4739
+ "vendor",
4740
+ "target",
4741
+ ".cache",
4742
+ ".parcel-cache"
4743
+ ]);
4744
+ function tagToPriority(tag) {
4745
+ switch (tag) {
4746
+ case "BUG":
4747
+ case "FIXME":
4748
+ return "high";
4749
+ case "HACK":
4750
+ case "XXX":
4751
+ return "medium";
4752
+ case "TODO":
4753
+ return "medium";
4754
+ case "NOTE":
4755
+ return "low";
4756
+ }
4757
+ }
4758
+ function buildTagRegex(tags) {
4759
+ const tagPattern = tags.join("|");
4760
+ return new RegExp(`(?:^|\\s)(?:\\/\\/|\\/\\*|#|\\*|--|;;|%|<!--|\\{-)\\s*(?:@?)(${tagPattern})\\s*[:(]?\\s*(.*)$`, "i");
4761
+ }
4762
+ function extractFromSource(source, filePath, tags = [...EXTRACT_TAGS]) {
4763
+ const regex = buildTagRegex(tags);
4764
+ const results = [];
4765
+ const lines = source.split(`
4766
+ `);
4767
+ for (let i = 0;i < lines.length; i++) {
4768
+ const line = lines[i];
4769
+ const match = line.match(regex);
4770
+ if (match) {
4771
+ const tag = match[1].toUpperCase();
4772
+ let message = match[2].trim();
4773
+ message = message.replace(/\s*\*\/\s*$/, "").replace(/\s*-->\s*$/, "").replace(/\s*-\}\s*$/, "").trim();
4774
+ if (message) {
4775
+ results.push({
4776
+ tag,
4777
+ message,
4778
+ file: filePath,
4779
+ line: i + 1,
4780
+ raw: line
4781
+ });
4782
+ }
4783
+ }
4784
+ }
4785
+ return results;
4786
+ }
4787
+ function collectFiles(basePath, extensions) {
4788
+ const stat = statSync2(basePath);
4789
+ if (stat.isFile()) {
4790
+ return [basePath];
4791
+ }
4792
+ const glob = new Bun.Glob("**/*");
4793
+ const files = [];
4794
+ for (const entry of glob.scanSync({ cwd: basePath, onlyFiles: true, dot: false })) {
4795
+ const parts = entry.split("/");
4796
+ if (parts.some((p) => SKIP_DIRS.has(p)))
4797
+ continue;
4798
+ const dotIdx = entry.lastIndexOf(".");
4799
+ if (dotIdx === -1)
4800
+ continue;
4801
+ const ext = entry.slice(dotIdx);
4802
+ if (!extensions.has(ext))
4803
+ continue;
4804
+ files.push(entry);
4805
+ }
4806
+ return files.sort();
4807
+ }
4808
+ function extractTodos(options, db) {
4809
+ const basePath = resolve2(options.path);
4810
+ const tags = options.patterns || [...EXTRACT_TAGS];
4811
+ const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
4812
+ const files = collectFiles(basePath, extensions);
4813
+ const allComments = [];
4814
+ for (const file of files) {
4815
+ const fullPath = statSync2(basePath).isFile() ? basePath : join6(basePath, file);
4816
+ try {
4817
+ const source = readFileSync3(fullPath, "utf-8");
4818
+ const relPath = statSync2(basePath).isFile() ? relative(resolve2(basePath, ".."), fullPath) : file;
4819
+ const comments = extractFromSource(source, relPath, tags);
4820
+ allComments.push(...comments);
4821
+ } catch {}
4822
+ }
4823
+ if (options.dry_run) {
4824
+ return { comments: allComments, tasks: [], skipped: 0 };
4825
+ }
4826
+ const tasks = [];
4827
+ let skipped = 0;
4828
+ const existingTasks = options.project_id ? listTasks({ project_id: options.project_id, tags: ["extracted"] }, db) : listTasks({ tags: ["extracted"] }, db);
4829
+ const existingKeys = new Set;
4830
+ for (const t of existingTasks) {
4831
+ const meta = t.metadata;
4832
+ if (meta?.["source_file"] && meta?.["source_line"]) {
4833
+ existingKeys.add(`${meta["source_file"]}:${meta["source_line"]}`);
4834
+ }
4835
+ }
4836
+ for (const comment of allComments) {
4837
+ const dedupKey = `${comment.file}:${comment.line}`;
4838
+ if (existingKeys.has(dedupKey)) {
4839
+ skipped++;
4840
+ continue;
4841
+ }
4842
+ const taskTags = ["extracted", comment.tag.toLowerCase(), ...options.tags || []];
4843
+ const task = createTask({
4844
+ title: `[${comment.tag}] ${comment.message}`,
4845
+ description: `Extracted from code comment in \`${comment.file}\` at line ${comment.line}:
4846
+ \`\`\`
4847
+ ${comment.raw.trim()}
4848
+ \`\`\``,
4849
+ priority: tagToPriority(comment.tag),
4850
+ project_id: options.project_id,
4851
+ task_list_id: options.task_list_id,
4852
+ assigned_to: options.assigned_to,
4853
+ agent_id: options.agent_id,
4854
+ tags: taskTags,
4855
+ metadata: {
4856
+ source: "code_comment",
4857
+ comment_type: comment.tag,
4858
+ source_file: comment.file,
4859
+ source_line: comment.line
4860
+ }
4861
+ }, db);
4862
+ addTaskFile({
4863
+ task_id: task.id,
4864
+ path: comment.file,
4865
+ note: `Line ${comment.line}: ${comment.tag} comment`
4866
+ }, db);
4867
+ tasks.push(task);
4868
+ existingKeys.add(dedupKey);
4869
+ }
4870
+ return { comments: allComments, tasks, skipped };
4871
+ }
4872
+ // src/lib/burndown.ts
4873
+ function getBurndown(opts, db) {
4874
+ const d = db || getDatabase();
4875
+ const conditions = [];
4876
+ const params = [];
4877
+ if (opts.plan_id) {
4878
+ conditions.push("plan_id = ?");
4879
+ params.push(opts.plan_id);
4880
+ }
4881
+ if (opts.project_id) {
4882
+ conditions.push("project_id = ?");
4883
+ params.push(opts.project_id);
4884
+ }
4885
+ if (opts.task_list_id) {
4886
+ conditions.push("task_list_id = ?");
4887
+ params.push(opts.task_list_id);
4888
+ }
4889
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4890
+ const total = d.query(`SELECT COUNT(*) as c FROM tasks ${where}`).get(...params).c;
4891
+ const completed = d.query(`SELECT COUNT(*) as c FROM tasks ${where}${where ? " AND" : " WHERE"} status = 'completed'`).get(...params).c;
4892
+ 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);
4893
+ const firstTask = d.query(`SELECT MIN(created_at) as min_date FROM tasks ${where}`).get(...params);
4894
+ const startDate = firstTask?.min_date ? new Date(firstTask.min_date) : new Date;
4895
+ const endDate = new Date;
4896
+ const days = [];
4897
+ const totalDays = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)));
4898
+ let cumulative = 0;
4899
+ const completionMap = new Map(completions.map((c) => [c.date, c.count]));
4900
+ const current = new Date(startDate);
4901
+ for (let i = 0;i <= totalDays; i++) {
4902
+ const dateStr = current.toISOString().slice(0, 10);
4903
+ cumulative += completionMap.get(dateStr) || 0;
4904
+ days.push({
4905
+ date: dateStr,
4906
+ completed_cumulative: cumulative,
4907
+ ideal: Math.round(total / totalDays * i)
4908
+ });
4909
+ current.setDate(current.getDate() + 1);
4910
+ }
4911
+ const chart = renderBurndownChart(total, days);
4912
+ return { total, completed, remaining: total - completed, days, chart };
4913
+ }
4914
+ function renderBurndownChart(total, days) {
4915
+ const height = 12;
4916
+ const width = Math.min(60, days.length);
4917
+ const step = Math.max(1, Math.floor(days.length / width));
4918
+ const sampled = days.filter((_, i) => i % step === 0 || i === days.length - 1).slice(0, width);
4919
+ const lines = [];
4920
+ lines.push(` ${total} \u2524`);
4921
+ for (let row = height - 1;row >= 0; row--) {
4922
+ const threshold = Math.round(total / height * row);
4923
+ let line = "";
4924
+ for (const day of sampled) {
4925
+ const remaining = total - day.completed_cumulative;
4926
+ const idealRemaining = total - day.ideal;
4927
+ if (remaining >= threshold && remaining > threshold - Math.round(total / height)) {
4928
+ line += "\u2588";
4929
+ } else if (idealRemaining >= threshold && idealRemaining > threshold - Math.round(total / height)) {
4930
+ line += "\xB7";
4931
+ } else {
4932
+ line += " ";
4933
+ }
4934
+ }
4935
+ const label = String(threshold).padStart(4);
4936
+ lines.push(`${label} \u2524${line}`);
4937
+ }
4938
+ lines.push(` 0 \u2524${"\u2500".repeat(sampled.length)}`);
4939
+ lines.push(` \u2514${"\u2500".repeat(sampled.length)}`);
4940
+ if (sampled.length > 0) {
4941
+ const first = sampled[0].date.slice(5);
4942
+ const last = sampled[sampled.length - 1].date.slice(5);
4943
+ const pad = sampled.length - first.length - last.length;
4944
+ lines.push(` ${first}${" ".repeat(Math.max(1, pad))}${last}`);
4945
+ }
4946
+ lines.push("");
4947
+ lines.push(` \u2588 actual remaining \xB7 ideal burndown`);
4948
+ return lines.join(`
4949
+ `);
4950
+ }
4951
+ // src/lib/github.ts
4952
+ import { execSync } from "child_process";
4953
+ function parseGitHubUrl(url) {
4954
+ const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
4955
+ if (!match)
4956
+ return null;
4957
+ return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
4958
+ }
4959
+ function fetchGitHubIssue(owner, repo, number) {
4960
+ const json = execSync(`gh api repos/${owner}/${repo}/issues/${number}`, { encoding: "utf-8", timeout: 15000 });
4961
+ const data = JSON.parse(json);
4962
+ return {
4963
+ number: data.number,
4964
+ title: data.title,
4965
+ body: data.body,
4966
+ labels: (data.labels || []).map((l) => l.name),
4967
+ state: data.state,
4968
+ assignee: data.assignee?.login || null,
4969
+ url: data.html_url
4970
+ };
4971
+ }
4972
+ function issueToTask(issue, opts) {
4973
+ const labelToPriority = {
4974
+ critical: "critical",
4975
+ "priority:critical": "critical",
4976
+ high: "high",
4977
+ "priority:high": "high",
4978
+ urgent: "high",
4979
+ low: "low",
4980
+ "priority:low": "low"
4981
+ };
4982
+ let priority = "medium";
4983
+ for (const label of issue.labels) {
4984
+ const mapped = labelToPriority[label.toLowerCase()];
4985
+ if (mapped) {
4986
+ priority = mapped;
4987
+ break;
4988
+ }
4989
+ }
4990
+ return {
4991
+ title: `[GH#${issue.number}] ${issue.title}`,
4992
+ description: issue.body ? issue.body.slice(0, 4000) : undefined,
4993
+ tags: issue.labels.slice(0, 10),
4994
+ priority,
4995
+ metadata: { github_url: issue.url, github_number: issue.number, github_state: issue.state },
4996
+ project_id: opts?.project_id,
4997
+ task_list_id: opts?.task_list_id,
4998
+ agent_id: opts?.agent_id
4999
+ };
5000
+ }
4567
5001
  export {
4568
5002
  uuid,
4569
5003
  updateTaskList,
@@ -4579,6 +5013,7 @@ export {
4579
5013
  unlockTask,
4580
5014
  unarchiveAgent,
4581
5015
  taskFromTemplate,
5016
+ tagToPriority,
4582
5017
  syncWithAgents,
4583
5018
  syncWithAgent,
4584
5019
  syncKgEdges,
@@ -4598,10 +5033,12 @@ export {
4598
5033
  removeDependency,
4599
5034
  removeChecklistItem,
4600
5035
  releaseLock,
5036
+ releaseAgent,
4601
5037
  registerAgent,
4602
5038
  redistributeStaleTasks,
4603
5039
  patrolTasks,
4604
5040
  parseRecurrenceRule,
5041
+ parseGitHubUrl,
4605
5042
  now,
4606
5043
  nextTaskShortId,
4607
5044
  nextOccurrence,
@@ -4624,6 +5061,7 @@ export {
4624
5061
  listHandoffs,
4625
5062
  listComments,
4626
5063
  listAgents,
5064
+ issueToTask,
4627
5065
  isValidRecurrenceRule,
4628
5066
  isAgentConflict,
4629
5067
  getWebhook,
@@ -4647,6 +5085,7 @@ export {
4647
5085
  getReviewQueue,
4648
5086
  getRelated,
4649
5087
  getRecentActivity,
5088
+ getRecap,
4650
5089
  getProjectWithSources,
4651
5090
  getProjectByPath,
4652
5091
  getProject,
@@ -4667,6 +5106,7 @@ export {
4667
5106
  getChecklistStats,
4668
5107
  getChecklist,
4669
5108
  getCapableAgents,
5109
+ getBurndown,
4670
5110
  getBlockingDeps,
4671
5111
  getAgentMetrics,
4672
5112
  getAgentByName,
@@ -4675,7 +5115,10 @@ export {
4675
5115
  findTasksByFile,
4676
5116
  findRelatedTaskIds,
4677
5117
  findPath,
5118
+ fetchGitHubIssue,
4678
5119
  failTask,
5120
+ extractTodos,
5121
+ extractFromSource,
4679
5122
  ensureTaskList,
4680
5123
  ensureProject,
4681
5124
  dispatchWebhook,
@@ -4714,6 +5157,7 @@ export {
4714
5157
  bulkUpdateTasks,
4715
5158
  bulkCreateTasks,
4716
5159
  bulkAddTaskFiles,
5160
+ autoReleaseStaleAgents,
4717
5161
  autoDetectFileRelationships,
4718
5162
  archiveAgent,
4719
5163
  addTaskRelationship,
@@ -4735,6 +5179,7 @@ export {
4735
5179
  PlanNotFoundError,
4736
5180
  PLAN_STATUSES,
4737
5181
  LockError,
5182
+ EXTRACT_TAGS,
4738
5183
  DependencyCycleError,
4739
5184
  CompletionGuardError,
4740
5185
  AgentNotFoundError
@@ -0,0 +1,18 @@
1
+ import type { Database } from "bun:sqlite";
2
+ export interface BurndownData {
3
+ total: number;
4
+ completed: number;
5
+ remaining: number;
6
+ days: {
7
+ date: string;
8
+ completed_cumulative: number;
9
+ ideal: number;
10
+ }[];
11
+ chart: string;
12
+ }
13
+ export declare function getBurndown(opts: {
14
+ plan_id?: string;
15
+ project_id?: string;
16
+ task_list_id?: string;
17
+ }, db?: Database): BurndownData;
18
+ //# sourceMappingURL=burndown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"burndown.d.ts","sourceRoot":"","sources":["../../src/lib/burndown.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,oBAAoB,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtE,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,YAAY,CA8C/H"}