@hasna/todos 0.10.16 → 0.10.18

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, {
@@ -11354,6 +11414,12 @@ Checklist (${done}/${task.checklist.length}):`);
11354
11414
  const resolvedId = resolveId(id);
11355
11415
  const evidence = files_changed || test_results || commit_hash || notes || attachment_ids ? { files_changed, test_results, commit_hash, notes, attachment_ids } : undefined;
11356
11416
  const task = completeTask(resolvedId, agent_id, undefined, { skip_recurrence, confidence, ...evidence });
11417
+ if (commit_hash) {
11418
+ try {
11419
+ const { linkTaskToCommit: linkTaskToCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
11420
+ linkTaskToCommit2({ task_id: resolvedId, sha: commit_hash, files_changed });
11421
+ } catch {}
11422
+ }
11357
11423
  let text = `completed: ${formatTask(task)}`;
11358
11424
  if (task.metadata._next_recurrence) {
11359
11425
  const next = task.metadata._next_recurrence;
@@ -13816,6 +13882,49 @@ ${lines.join(`
13816
13882
  }
13817
13883
  });
13818
13884
  }
13885
+ if (shouldRegisterTool("link_task_to_commit")) {
13886
+ 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.", {
13887
+ task_id: exports_external.string().describe("Task ID"),
13888
+ sha: exports_external.string().describe("Git commit SHA (full or short)"),
13889
+ message: exports_external.string().optional().describe("Commit message"),
13890
+ author: exports_external.string().optional().describe("Commit author"),
13891
+ files_changed: exports_external.array(exports_external.string()).optional().describe("Files changed in this commit"),
13892
+ committed_at: exports_external.string().optional().describe("ISO timestamp of commit")
13893
+ }, async ({ task_id, sha, message, author, files_changed, committed_at }) => {
13894
+ try {
13895
+ const { linkTaskToCommit: linkTaskToCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
13896
+ const resolvedId = resolveId(task_id);
13897
+ const commit = linkTaskToCommit2({ task_id: resolvedId, sha, message, author, files_changed, committed_at });
13898
+ return { content: [{ type: "text", text: JSON.stringify(commit, null, 2) }] };
13899
+ } catch (e) {
13900
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13901
+ }
13902
+ });
13903
+ }
13904
+ if (shouldRegisterTool("get_task_commits")) {
13905
+ server.tool("get_task_commits", "Get all git commits linked to a task.", { task_id: exports_external.string().describe("Task ID") }, async ({ task_id }) => {
13906
+ try {
13907
+ const { getTaskCommits: getTaskCommits2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
13908
+ const commits = getTaskCommits2(resolveId(task_id));
13909
+ return { content: [{ type: "text", text: JSON.stringify(commits, null, 2) }] };
13910
+ } catch (e) {
13911
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13912
+ }
13913
+ });
13914
+ }
13915
+ if (shouldRegisterTool("find_task_by_commit")) {
13916
+ 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 }) => {
13917
+ try {
13918
+ const { findTaskByCommit: findTaskByCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
13919
+ const result = findTaskByCommit2(sha);
13920
+ if (!result)
13921
+ return { content: [{ type: "text", text: `No task linked to commit ${sha}` }] };
13922
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
13923
+ } catch (e) {
13924
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13925
+ }
13926
+ });
13927
+ }
13819
13928
  if (shouldRegisterTool("lock_file")) {
13820
13929
  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
13930
  path: exports_external.string().describe("File path to lock"),
@@ -16894,6 +17003,19 @@ program2.command("delete <id>").description("Delete a task").action((id) => {
16894
17003
  process.exit(1);
16895
17004
  }
16896
17005
  });
17006
+ program2.command("remove <id>").description("Remove/delete a task (alias for delete)").action((id) => {
17007
+ const globalOpts = program2.opts();
17008
+ const resolvedId = resolveTaskId(id);
17009
+ const deleted = deleteTask(resolvedId);
17010
+ if (globalOpts.json) {
17011
+ output({ deleted }, true);
17012
+ } else if (deleted) {
17013
+ console.log(chalk.green("Task removed."));
17014
+ } else {
17015
+ console.error(chalk.red("Task not found."));
17016
+ process.exit(1);
17017
+ }
17018
+ });
16897
17019
  program2.command("bulk <action> <ids...>").description("Bulk operation on multiple tasks (done, start, delete)").action((action, ids) => {
16898
17020
  const globalOpts = program2.opts();
16899
17021
  const results = [];
@@ -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"}
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, {
@@ -9166,6 +9226,12 @@ if (shouldRegisterTool("complete_task")) {
9166
9226
  const resolvedId = resolveId(id);
9167
9227
  const evidence = files_changed || test_results || commit_hash || notes || attachment_ids ? { files_changed, test_results, commit_hash, notes, attachment_ids } : undefined;
9168
9228
  const task = completeTask(resolvedId, agent_id, undefined, { skip_recurrence, confidence, ...evidence });
9229
+ if (commit_hash) {
9230
+ try {
9231
+ const { linkTaskToCommit: linkTaskToCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
9232
+ linkTaskToCommit2({ task_id: resolvedId, sha: commit_hash, files_changed });
9233
+ } catch {}
9234
+ }
9169
9235
  let text = `completed: ${formatTask(task)}`;
9170
9236
  if (task.metadata._next_recurrence) {
9171
9237
  const next = task.metadata._next_recurrence;
@@ -11628,6 +11694,49 @@ if (shouldRegisterTool("list_active_files")) {
11628
11694
  }
11629
11695
  });
11630
11696
  }
11697
+ if (shouldRegisterTool("link_task_to_commit")) {
11698
+ 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.", {
11699
+ task_id: exports_external.string().describe("Task ID"),
11700
+ sha: exports_external.string().describe("Git commit SHA (full or short)"),
11701
+ message: exports_external.string().optional().describe("Commit message"),
11702
+ author: exports_external.string().optional().describe("Commit author"),
11703
+ files_changed: exports_external.array(exports_external.string()).optional().describe("Files changed in this commit"),
11704
+ committed_at: exports_external.string().optional().describe("ISO timestamp of commit")
11705
+ }, async ({ task_id, sha, message, author, files_changed, committed_at }) => {
11706
+ try {
11707
+ const { linkTaskToCommit: linkTaskToCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
11708
+ const resolvedId = resolveId(task_id);
11709
+ const commit = linkTaskToCommit2({ task_id: resolvedId, sha, message, author, files_changed, committed_at });
11710
+ return { content: [{ type: "text", text: JSON.stringify(commit, null, 2) }] };
11711
+ } catch (e) {
11712
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11713
+ }
11714
+ });
11715
+ }
11716
+ if (shouldRegisterTool("get_task_commits")) {
11717
+ server.tool("get_task_commits", "Get all git commits linked to a task.", { task_id: exports_external.string().describe("Task ID") }, async ({ task_id }) => {
11718
+ try {
11719
+ const { getTaskCommits: getTaskCommits2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
11720
+ const commits = getTaskCommits2(resolveId(task_id));
11721
+ return { content: [{ type: "text", text: JSON.stringify(commits, null, 2) }] };
11722
+ } catch (e) {
11723
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11724
+ }
11725
+ });
11726
+ }
11727
+ if (shouldRegisterTool("find_task_by_commit")) {
11728
+ 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 }) => {
11729
+ try {
11730
+ const { findTaskByCommit: findTaskByCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
11731
+ const result = findTaskByCommit2(sha);
11732
+ if (!result)
11733
+ return { content: [{ type: "text", text: `No task linked to commit ${sha}` }] };
11734
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11735
+ } catch (e) {
11736
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11737
+ }
11738
+ });
11739
+ }
11631
11740
  if (shouldRegisterTool("lock_file")) {
11632
11741
  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
11742
  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.16",
3
+ "version": "0.10.18",
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",