@hasna/todos 0.10.17 → 0.10.19

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
@@ -2911,6 +2911,22 @@ var init_database = __esm(() => {
2911
2911
  CREATE INDEX IF NOT EXISTS idx_project_agent_roles_project ON project_agent_roles(project_id);
2912
2912
  CREATE INDEX IF NOT EXISTS idx_project_agent_roles_agent ON project_agent_roles(agent_id);
2913
2913
  INSERT OR IGNORE INTO _migrations (id) VALUES (31);
2914
+ `,
2915
+ `
2916
+ CREATE TABLE IF NOT EXISTS task_commits (
2917
+ id TEXT PRIMARY KEY,
2918
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
2919
+ sha TEXT NOT NULL,
2920
+ message TEXT,
2921
+ author TEXT,
2922
+ files_changed TEXT,
2923
+ committed_at TEXT,
2924
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2925
+ UNIQUE(task_id, sha)
2926
+ );
2927
+ CREATE INDEX IF NOT EXISTS idx_task_commits_task ON task_commits(task_id);
2928
+ CREATE INDEX IF NOT EXISTS idx_task_commits_sha ON task_commits(sha);
2929
+ INSERT OR IGNORE INTO _migrations (id) VALUES (32);
2914
2930
  `,
2915
2931
  `
2916
2932
  CREATE TABLE IF NOT EXISTS file_locks (
@@ -2924,7 +2940,7 @@ var init_database = __esm(() => {
2924
2940
  CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
2925
2941
  CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
2926
2942
  CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
2927
- INSERT OR IGNORE INTO _migrations (id) VALUES (32);
2943
+ INSERT OR IGNORE INTO _migrations (id) VALUES (33);
2928
2944
  `
2929
2945
  ];
2930
2946
  });
@@ -9817,6 +9833,50 @@ var init_zod = __esm(() => {
9817
9833
  init_external();
9818
9834
  });
9819
9835
 
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
9843
+ });
9844
+ function rowToCommit(row) {
9845
+ return {
9846
+ ...row,
9847
+ files_changed: row.files_changed ? JSON.parse(row.files_changed) : null
9848
+ };
9849
+ }
9850
+ function linkTaskToCommit(input, db) {
9851
+ const d = db || getDatabase();
9852
+ const existing = d.query("SELECT * FROM task_commits WHERE task_id = ? AND sha = ?").get(input.task_id, input.sha);
9853
+ if (existing) {
9854
+ d.run("UPDATE task_commits SET message = COALESCE(?, message), author = COALESCE(?, author), files_changed = COALESCE(?, files_changed), committed_at = COALESCE(?, committed_at) WHERE id = ?", [input.message ?? null, input.author ?? null, input.files_changed ? JSON.stringify(input.files_changed) : null, input.committed_at ?? null, existing.id]);
9855
+ return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(existing.id));
9856
+ }
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) };
9871
+ }
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;
9875
+ }
9876
+ var init_task_commits = __esm(() => {
9877
+ init_database();
9878
+ });
9879
+
9820
9880
  // src/lib/auto-assign.ts
9821
9881
  var exports_auto_assign = {};
9822
9882
  __export(exports_auto_assign, {
@@ -9980,6 +10040,7 @@ __export(exports_task_files, {
9980
10040
  listTaskFiles: () => listTaskFiles,
9981
10041
  listActiveFiles: () => listActiveFiles,
9982
10042
  getTaskFile: () => getTaskFile,
10043
+ getFileHeatMap: () => getFileHeatMap,
9983
10044
  findTasksByFile: () => findTasksByFile,
9984
10045
  detectFileConflicts: () => detectFileConflicts,
9985
10046
  bulkFindTasksByFiles: () => bulkFindTasksByFiles,
@@ -10092,6 +10153,36 @@ function listActiveFiles(db) {
10092
10153
  ORDER BY tf.updated_at DESC
10093
10154
  `).all();
10094
10155
  }
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
+ }));
10185
+ }
10095
10186
  function bulkAddTaskFiles(taskId, paths, agentId, db) {
10096
10187
  const d = db || getDatabase();
10097
10188
  const results = [];
@@ -11354,6 +11445,12 @@ Checklist (${done}/${task.checklist.length}):`);
11354
11445
  const resolvedId = resolveId(id);
11355
11446
  const evidence = files_changed || test_results || commit_hash || notes || attachment_ids ? { files_changed, test_results, commit_hash, notes, attachment_ids } : undefined;
11356
11447
  const task = completeTask(resolvedId, agent_id, undefined, { skip_recurrence, confidence, ...evidence });
11448
+ if (commit_hash) {
11449
+ try {
11450
+ const { linkTaskToCommit: linkTaskToCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
11451
+ linkTaskToCommit2({ task_id: resolvedId, sha: commit_hash, files_changed });
11452
+ } catch {}
11453
+ }
11357
11454
  let text = `completed: ${formatTask(task)}`;
11358
11455
  if (task.metadata._next_recurrence) {
11359
11456
  const next = task.metadata._next_recurrence;
@@ -13760,6 +13857,22 @@ ${lines.join(`
13760
13857
  }
13761
13858
  });
13762
13859
  }
13860
+ if (shouldRegisterTool("get_file_heat_map")) {
13861
+ server.tool("get_file_heat_map", "Aggregate file edit frequency across all tasks and agents. Returns hottest files with edit count, unique agents, and last edit. Hot files = high coordination risk, good candidates for extra test coverage.", {
13862
+ limit: exports_external.number().optional().describe("Max files to return (default: 20)"),
13863
+ project_id: exports_external.string().optional().describe("Filter to a specific project"),
13864
+ min_edits: exports_external.number().optional().describe("Minimum edit count to include (default: 1)")
13865
+ }, async ({ limit, project_id, min_edits }) => {
13866
+ try {
13867
+ const { getFileHeatMap: getFileHeatMap2 } = (init_task_files(), __toCommonJS(exports_task_files));
13868
+ const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
13869
+ const results = getFileHeatMap2({ limit, project_id: resolvedProjectId, min_edits });
13870
+ return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
13871
+ } catch (e) {
13872
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13873
+ }
13874
+ });
13875
+ }
13763
13876
  if (shouldRegisterTool("bulk_find_tasks_by_files")) {
13764
13877
  server.tool("bulk_find_tasks_by_files", "Check multiple file paths at once for task/agent collisions. Returns per-path task list, in-progress count, and conflict flag.", {
13765
13878
  paths: exports_external.array(exports_external.string()).describe("Array of file paths to check")
@@ -13816,6 +13929,49 @@ ${lines.join(`
13816
13929
  }
13817
13930
  });
13818
13931
  }
13932
+ if (shouldRegisterTool("link_task_to_commit")) {
13933
+ server.tool("link_task_to_commit", "Link a git commit SHA to a task. Creates an audit trail: task \u2192 commits. Upserts on same task+sha.", {
13934
+ task_id: exports_external.string().describe("Task ID"),
13935
+ sha: exports_external.string().describe("Git commit SHA (full or short)"),
13936
+ message: exports_external.string().optional().describe("Commit message"),
13937
+ author: exports_external.string().optional().describe("Commit author"),
13938
+ files_changed: exports_external.array(exports_external.string()).optional().describe("Files changed in this commit"),
13939
+ committed_at: exports_external.string().optional().describe("ISO timestamp of commit")
13940
+ }, async ({ task_id, sha, message, author, files_changed, committed_at }) => {
13941
+ try {
13942
+ const { linkTaskToCommit: linkTaskToCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
13943
+ const resolvedId = resolveId(task_id);
13944
+ const commit = linkTaskToCommit2({ task_id: resolvedId, sha, message, author, files_changed, committed_at });
13945
+ return { content: [{ type: "text", text: JSON.stringify(commit, null, 2) }] };
13946
+ } catch (e) {
13947
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13948
+ }
13949
+ });
13950
+ }
13951
+ if (shouldRegisterTool("get_task_commits")) {
13952
+ server.tool("get_task_commits", "Get all git commits linked to a task.", { task_id: exports_external.string().describe("Task ID") }, async ({ task_id }) => {
13953
+ try {
13954
+ const { getTaskCommits: getTaskCommits2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
13955
+ const commits = getTaskCommits2(resolveId(task_id));
13956
+ return { content: [{ type: "text", text: JSON.stringify(commits, null, 2) }] };
13957
+ } catch (e) {
13958
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13959
+ }
13960
+ });
13961
+ }
13962
+ if (shouldRegisterTool("find_task_by_commit")) {
13963
+ server.tool("find_task_by_commit", "Find which task a git commit SHA is linked to. Supports prefix matching.", { sha: exports_external.string().describe("Git commit SHA (full or short prefix)") }, async ({ sha }) => {
13964
+ try {
13965
+ const { findTaskByCommit: findTaskByCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
13966
+ const result = findTaskByCommit2(sha);
13967
+ if (!result)
13968
+ return { content: [{ type: "text", text: `No task linked to commit ${sha}` }] };
13969
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
13970
+ } catch (e) {
13971
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13972
+ }
13973
+ });
13974
+ }
13819
13975
  if (shouldRegisterTool("lock_file")) {
13820
13976
  server.tool("lock_file", "Acquire an exclusive lock on a file path. Throws if another agent holds an active lock. Same agent re-locks refreshes the TTL.", {
13821
13977
  path: exports_external.string().describe("File path to lock"),
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AA4iBtC,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAkBrD;AAyRD,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,wBAAgB,GAAG,IAAI,MAAM,CAE5B;AAED,wBAAgB,IAAI,IAAI,MAAM,CAE7B;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAK9D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,SAAa,GAAG,MAAM,CAG3D;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAGpD;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA0B9F"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AA6jBtC,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAkBrD;AAyRD,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,wBAAgB,GAAG,IAAI,MAAM,CAE5B;AAED,wBAAgB,IAAI,IAAI,MAAM,CAE7B;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAK9D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,SAAa,GAAG,MAAM,CAG3D;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAGpD;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA0B9F"}
@@ -0,0 +1,31 @@
1
+ import type { Database } from "bun:sqlite";
2
+ export interface TaskCommit {
3
+ id: string;
4
+ task_id: string;
5
+ sha: string;
6
+ message: string | null;
7
+ author: string | null;
8
+ files_changed: string[] | null;
9
+ committed_at: string | null;
10
+ created_at: string;
11
+ }
12
+ export interface LinkTaskToCommitInput {
13
+ task_id: string;
14
+ sha: string;
15
+ message?: string;
16
+ author?: string;
17
+ files_changed?: string[];
18
+ committed_at?: string;
19
+ }
20
+ /** Link a git commit SHA to a task. Upserts on same task+sha. */
21
+ export declare function linkTaskToCommit(input: LinkTaskToCommitInput, db?: Database): TaskCommit;
22
+ /** Get all commits linked to a task. */
23
+ export declare function getTaskCommits(taskId: string, db?: Database): TaskCommit[];
24
+ /** Find which task a commit SHA is linked to. */
25
+ export declare function findTaskByCommit(sha: string, db?: Database): {
26
+ task_id: string;
27
+ commit: TaskCommit;
28
+ } | null;
29
+ /** Remove a commit link. */
30
+ export declare function unlinkTaskCommit(taskId: string, sha: string, db?: Database): boolean;
31
+ //# sourceMappingURL=task-commits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-commits.d.ts","sourceRoot":"","sources":["../../src/db/task-commits.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,aAAa,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;CACpB;AAoBD,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,UAAU,CAoBxF;AAED,wCAAwC;AACxC,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,UAAU,EAAE,CAG1E;AAED,iDAAiD;AACjD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GAAG,IAAI,CAM3G;AAED,4BAA4B;AAC5B,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAGpF"}
@@ -57,5 +57,18 @@ export interface ActiveFileInfo {
57
57
  agent_name: string | null;
58
58
  }
59
59
  export declare function listActiveFiles(db?: Database): ActiveFileInfo[];
60
+ export interface FileHeatMapEntry {
61
+ path: string;
62
+ edit_count: number;
63
+ unique_agents: number;
64
+ agent_ids: string[];
65
+ last_edited_at: string;
66
+ active_task_count: number;
67
+ }
68
+ export declare function getFileHeatMap(opts?: {
69
+ limit?: number;
70
+ project_id?: string;
71
+ min_edits?: number;
72
+ }, db?: Database): FileHeatMapEntry[];
60
73
  export declare function bulkAddTaskFiles(taskId: string, paths: string[], agentId?: string, db?: Database): TaskFile[];
61
74
  //# sourceMappingURL=task-files.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"task-files.d.ts","sourceRoot":"","sources":["../../src/db/task-files.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"task-files.d.ts","sourceRoot":"","sources":["../../src/db/task-files.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -718,6 +718,22 @@ var MIGRATIONS = [
718
718
  INSERT OR IGNORE INTO _migrations (id) VALUES (31);
719
719
  `,
720
720
  `
721
+ CREATE TABLE IF NOT EXISTS task_commits (
722
+ id TEXT PRIMARY KEY,
723
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
724
+ sha TEXT NOT NULL,
725
+ message TEXT,
726
+ author TEXT,
727
+ files_changed TEXT,
728
+ committed_at TEXT,
729
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
730
+ UNIQUE(task_id, sha)
731
+ );
732
+ CREATE INDEX IF NOT EXISTS idx_task_commits_task ON task_commits(task_id);
733
+ CREATE INDEX IF NOT EXISTS idx_task_commits_sha ON task_commits(sha);
734
+ INSERT OR IGNORE INTO _migrations (id) VALUES (32);
735
+ `,
736
+ `
721
737
  CREATE TABLE IF NOT EXISTS file_locks (
722
738
  id TEXT PRIMARY KEY,
723
739
  path TEXT NOT NULL UNIQUE,
@@ -729,7 +745,7 @@ var MIGRATIONS = [
729
745
  CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
730
746
  CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
731
747
  CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
732
- INSERT OR IGNORE INTO _migrations (id) VALUES (32);
748
+ INSERT OR IGNORE INTO _migrations (id) VALUES (33);
733
749
  `
734
750
  ];
735
751
  var _db = null;
package/dist/mcp/index.js CHANGED
@@ -956,6 +956,22 @@ var init_database = __esm(() => {
956
956
  CREATE INDEX IF NOT EXISTS idx_project_agent_roles_project ON project_agent_roles(project_id);
957
957
  CREATE INDEX IF NOT EXISTS idx_project_agent_roles_agent ON project_agent_roles(agent_id);
958
958
  INSERT OR IGNORE INTO _migrations (id) VALUES (31);
959
+ `,
960
+ `
961
+ CREATE TABLE IF NOT EXISTS task_commits (
962
+ id TEXT PRIMARY KEY,
963
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
964
+ sha TEXT NOT NULL,
965
+ message TEXT,
966
+ author TEXT,
967
+ files_changed TEXT,
968
+ committed_at TEXT,
969
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
970
+ UNIQUE(task_id, sha)
971
+ );
972
+ CREATE INDEX IF NOT EXISTS idx_task_commits_task ON task_commits(task_id);
973
+ CREATE INDEX IF NOT EXISTS idx_task_commits_sha ON task_commits(sha);
974
+ INSERT OR IGNORE INTO _migrations (id) VALUES (32);
959
975
  `,
960
976
  `
961
977
  CREATE TABLE IF NOT EXISTS file_locks (
@@ -969,7 +985,7 @@ var init_database = __esm(() => {
969
985
  CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
970
986
  CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
971
987
  CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
972
- INSERT OR IGNORE INTO _migrations (id) VALUES (32);
988
+ INSERT OR IGNORE INTO _migrations (id) VALUES (33);
973
989
  `
974
990
  ];
975
991
  });
@@ -2868,6 +2884,50 @@ var init_agents = __esm(() => {
2868
2884
  AGENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
2869
2885
  });
2870
2886
 
2887
+ // src/db/task-commits.ts
2888
+ var exports_task_commits = {};
2889
+ __export(exports_task_commits, {
2890
+ unlinkTaskCommit: () => unlinkTaskCommit,
2891
+ linkTaskToCommit: () => linkTaskToCommit,
2892
+ getTaskCommits: () => getTaskCommits,
2893
+ findTaskByCommit: () => findTaskByCommit
2894
+ });
2895
+ function rowToCommit(row) {
2896
+ return {
2897
+ ...row,
2898
+ files_changed: row.files_changed ? JSON.parse(row.files_changed) : null
2899
+ };
2900
+ }
2901
+ function linkTaskToCommit(input, db) {
2902
+ const d = db || getDatabase();
2903
+ const existing = d.query("SELECT * FROM task_commits WHERE task_id = ? AND sha = ?").get(input.task_id, input.sha);
2904
+ if (existing) {
2905
+ 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]);
2906
+ return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(existing.id));
2907
+ }
2908
+ const id = uuid();
2909
+ 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()]);
2910
+ return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(id));
2911
+ }
2912
+ function getTaskCommits(taskId, db) {
2913
+ const d = db || getDatabase();
2914
+ return d.query("SELECT * FROM task_commits WHERE task_id = ? ORDER BY committed_at DESC, created_at DESC").all(taskId).map(rowToCommit);
2915
+ }
2916
+ function findTaskByCommit(sha, db) {
2917
+ const d = db || getDatabase();
2918
+ const row = d.query("SELECT * FROM task_commits WHERE sha = ? OR sha LIKE ? LIMIT 1").get(sha, `${sha}%`);
2919
+ if (!row)
2920
+ return null;
2921
+ return { task_id: row.task_id, commit: rowToCommit(row) };
2922
+ }
2923
+ function unlinkTaskCommit(taskId, sha, db) {
2924
+ const d = db || getDatabase();
2925
+ return d.run("DELETE FROM task_commits WHERE task_id = ? AND (sha = ? OR sha LIKE ?)", [taskId, sha, `${sha}%`]).changes > 0;
2926
+ }
2927
+ var init_task_commits = __esm(() => {
2928
+ init_database();
2929
+ });
2930
+
2871
2931
  // src/lib/auto-assign.ts
2872
2932
  var exports_auto_assign = {};
2873
2933
  __export(exports_auto_assign, {
@@ -3031,6 +3091,7 @@ __export(exports_task_files, {
3031
3091
  listTaskFiles: () => listTaskFiles,
3032
3092
  listActiveFiles: () => listActiveFiles,
3033
3093
  getTaskFile: () => getTaskFile,
3094
+ getFileHeatMap: () => getFileHeatMap,
3034
3095
  findTasksByFile: () => findTasksByFile,
3035
3096
  detectFileConflicts: () => detectFileConflicts,
3036
3097
  bulkFindTasksByFiles: () => bulkFindTasksByFiles,
@@ -3143,6 +3204,36 @@ function listActiveFiles(db) {
3143
3204
  ORDER BY tf.updated_at DESC
3144
3205
  `).all();
3145
3206
  }
3207
+ function getFileHeatMap(opts, db) {
3208
+ const d = db || getDatabase();
3209
+ const limit = opts?.limit ?? 20;
3210
+ const minEdits = opts?.min_edits ?? 1;
3211
+ const rows = d.query(`
3212
+ SELECT
3213
+ tf.path,
3214
+ COUNT(*) AS edit_count,
3215
+ COUNT(DISTINCT COALESCE(tf.agent_id, t.assigned_to)) AS unique_agents,
3216
+ GROUP_CONCAT(DISTINCT COALESCE(tf.agent_id, t.assigned_to)) AS agent_ids,
3217
+ MAX(tf.updated_at) AS last_edited_at,
3218
+ SUM(CASE WHEN t.status = 'in_progress' THEN 1 ELSE 0 END) AS active_task_count
3219
+ FROM task_files tf
3220
+ JOIN tasks t ON tf.task_id = t.id
3221
+ WHERE tf.status != 'removed'
3222
+ ${opts?.project_id ? `AND t.project_id = '${opts.project_id}'` : ""}
3223
+ GROUP BY tf.path
3224
+ HAVING edit_count >= ${minEdits}
3225
+ ORDER BY edit_count DESC, last_edited_at DESC
3226
+ LIMIT ${limit}
3227
+ `).all();
3228
+ return rows.map((r) => ({
3229
+ path: r.path,
3230
+ edit_count: r.edit_count,
3231
+ unique_agents: r.unique_agents,
3232
+ agent_ids: r.agent_ids ? r.agent_ids.split(",").filter(Boolean) : [],
3233
+ last_edited_at: r.last_edited_at,
3234
+ active_task_count: r.active_task_count
3235
+ }));
3236
+ }
3146
3237
  function bulkAddTaskFiles(taskId, paths, agentId, db) {
3147
3238
  const d = db || getDatabase();
3148
3239
  const results = [];
@@ -9166,6 +9257,12 @@ if (shouldRegisterTool("complete_task")) {
9166
9257
  const resolvedId = resolveId(id);
9167
9258
  const evidence = files_changed || test_results || commit_hash || notes || attachment_ids ? { files_changed, test_results, commit_hash, notes, attachment_ids } : undefined;
9168
9259
  const task = completeTask(resolvedId, agent_id, undefined, { skip_recurrence, confidence, ...evidence });
9260
+ if (commit_hash) {
9261
+ try {
9262
+ const { linkTaskToCommit: linkTaskToCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
9263
+ linkTaskToCommit2({ task_id: resolvedId, sha: commit_hash, files_changed });
9264
+ } catch {}
9265
+ }
9169
9266
  let text = `completed: ${formatTask(task)}`;
9170
9267
  if (task.metadata._next_recurrence) {
9171
9268
  const next = task.metadata._next_recurrence;
@@ -11572,6 +11669,22 @@ ${lines.join(`
11572
11669
  }
11573
11670
  });
11574
11671
  }
11672
+ if (shouldRegisterTool("get_file_heat_map")) {
11673
+ server.tool("get_file_heat_map", "Aggregate file edit frequency across all tasks and agents. Returns hottest files with edit count, unique agents, and last edit. Hot files = high coordination risk, good candidates for extra test coverage.", {
11674
+ limit: exports_external.number().optional().describe("Max files to return (default: 20)"),
11675
+ project_id: exports_external.string().optional().describe("Filter to a specific project"),
11676
+ min_edits: exports_external.number().optional().describe("Minimum edit count to include (default: 1)")
11677
+ }, async ({ limit, project_id, min_edits }) => {
11678
+ try {
11679
+ const { getFileHeatMap: getFileHeatMap2 } = (init_task_files(), __toCommonJS(exports_task_files));
11680
+ const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
11681
+ const results = getFileHeatMap2({ limit, project_id: resolvedProjectId, min_edits });
11682
+ return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
11683
+ } catch (e) {
11684
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11685
+ }
11686
+ });
11687
+ }
11575
11688
  if (shouldRegisterTool("bulk_find_tasks_by_files")) {
11576
11689
  server.tool("bulk_find_tasks_by_files", "Check multiple file paths at once for task/agent collisions. Returns per-path task list, in-progress count, and conflict flag.", {
11577
11690
  paths: exports_external.array(exports_external.string()).describe("Array of file paths to check")
@@ -11628,6 +11741,49 @@ if (shouldRegisterTool("list_active_files")) {
11628
11741
  }
11629
11742
  });
11630
11743
  }
11744
+ if (shouldRegisterTool("link_task_to_commit")) {
11745
+ server.tool("link_task_to_commit", "Link a git commit SHA to a task. Creates an audit trail: task \u2192 commits. Upserts on same task+sha.", {
11746
+ task_id: exports_external.string().describe("Task ID"),
11747
+ sha: exports_external.string().describe("Git commit SHA (full or short)"),
11748
+ message: exports_external.string().optional().describe("Commit message"),
11749
+ author: exports_external.string().optional().describe("Commit author"),
11750
+ files_changed: exports_external.array(exports_external.string()).optional().describe("Files changed in this commit"),
11751
+ committed_at: exports_external.string().optional().describe("ISO timestamp of commit")
11752
+ }, async ({ task_id, sha, message, author, files_changed, committed_at }) => {
11753
+ try {
11754
+ const { linkTaskToCommit: linkTaskToCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
11755
+ const resolvedId = resolveId(task_id);
11756
+ const commit = linkTaskToCommit2({ task_id: resolvedId, sha, message, author, files_changed, committed_at });
11757
+ return { content: [{ type: "text", text: JSON.stringify(commit, null, 2) }] };
11758
+ } catch (e) {
11759
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11760
+ }
11761
+ });
11762
+ }
11763
+ if (shouldRegisterTool("get_task_commits")) {
11764
+ server.tool("get_task_commits", "Get all git commits linked to a task.", { task_id: exports_external.string().describe("Task ID") }, async ({ task_id }) => {
11765
+ try {
11766
+ const { getTaskCommits: getTaskCommits2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
11767
+ const commits = getTaskCommits2(resolveId(task_id));
11768
+ return { content: [{ type: "text", text: JSON.stringify(commits, null, 2) }] };
11769
+ } catch (e) {
11770
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11771
+ }
11772
+ });
11773
+ }
11774
+ if (shouldRegisterTool("find_task_by_commit")) {
11775
+ server.tool("find_task_by_commit", "Find which task a git commit SHA is linked to. Supports prefix matching.", { sha: exports_external.string().describe("Git commit SHA (full or short prefix)") }, async ({ sha }) => {
11776
+ try {
11777
+ const { findTaskByCommit: findTaskByCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
11778
+ const result = findTaskByCommit2(sha);
11779
+ if (!result)
11780
+ return { content: [{ type: "text", text: `No task linked to commit ${sha}` }] };
11781
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11782
+ } catch (e) {
11783
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11784
+ }
11785
+ });
11786
+ }
11631
11787
  if (shouldRegisterTool("lock_file")) {
11632
11788
  server.tool("lock_file", "Acquire an exclusive lock on a file path. Throws if another agent holds an active lock. Same agent re-locks refreshes the TTL.", {
11633
11789
  path: exports_external.string().describe("File path to lock"),
@@ -872,6 +872,22 @@ var init_database = __esm(() => {
872
872
  CREATE INDEX IF NOT EXISTS idx_project_agent_roles_project ON project_agent_roles(project_id);
873
873
  CREATE INDEX IF NOT EXISTS idx_project_agent_roles_agent ON project_agent_roles(agent_id);
874
874
  INSERT OR IGNORE INTO _migrations (id) VALUES (31);
875
+ `,
876
+ `
877
+ CREATE TABLE IF NOT EXISTS task_commits (
878
+ id TEXT PRIMARY KEY,
879
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
880
+ sha TEXT NOT NULL,
881
+ message TEXT,
882
+ author TEXT,
883
+ files_changed TEXT,
884
+ committed_at TEXT,
885
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
886
+ UNIQUE(task_id, sha)
887
+ );
888
+ CREATE INDEX IF NOT EXISTS idx_task_commits_task ON task_commits(task_id);
889
+ CREATE INDEX IF NOT EXISTS idx_task_commits_sha ON task_commits(sha);
890
+ INSERT OR IGNORE INTO _migrations (id) VALUES (32);
875
891
  `,
876
892
  `
877
893
  CREATE TABLE IF NOT EXISTS file_locks (
@@ -885,7 +901,7 @@ var init_database = __esm(() => {
885
901
  CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
886
902
  CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
887
903
  CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
888
- INSERT OR IGNORE INTO _migrations (id) VALUES (32);
904
+ INSERT OR IGNORE INTO _migrations (id) VALUES (33);
889
905
  `
890
906
  ];
891
907
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.10.17",
3
+ "version": "0.10.19",
4
4
  "description": "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",