@hasna/todos 0.11.45 → 0.11.47

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.
Files changed (236) hide show
  1. package/README.md +11 -1097
  2. package/dashboard/dist/assets/{index-CVF1vn7Z.js → index-B-w1tUlm.js} +23 -23
  3. package/dashboard/dist/assets/index-BXQ39iMX.css +1 -0
  4. package/dashboard/dist/index.html +2 -2
  5. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/cloud-commands.d.ts +3 -0
  7. package/dist/cli/commands/cloud-commands.d.ts.map +1 -0
  8. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  9. package/dist/cli/commands/machines.d.ts.map +1 -1
  10. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  11. package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
  12. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  13. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  14. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  15. package/dist/cli/index.js +28761 -42785
  16. package/dist/db/agent-metrics.d.ts +0 -101
  17. package/dist/db/agent-metrics.d.ts.map +1 -1
  18. package/dist/db/agent-names.d.ts +5 -1
  19. package/dist/db/agent-names.d.ts.map +1 -1
  20. package/dist/db/builtin-templates.d.ts +0 -17
  21. package/dist/db/builtin-templates.d.ts.map +1 -1
  22. package/dist/db/comments.d.ts.map +1 -1
  23. package/dist/db/database.d.ts +1 -2
  24. package/dist/db/database.d.ts.map +1 -1
  25. package/dist/db/handoffs.d.ts +1 -52
  26. package/dist/db/handoffs.d.ts.map +1 -1
  27. package/dist/db/machines.d.ts +6 -19
  28. package/dist/db/machines.d.ts.map +1 -1
  29. package/dist/db/migrations.d.ts.map +1 -1
  30. package/dist/db/pg-migrate.d.ts +14 -0
  31. package/dist/db/pg-migrate.d.ts.map +1 -0
  32. package/dist/db/pg-migrations.d.ts +8 -0
  33. package/dist/db/pg-migrations.d.ts.map +1 -0
  34. package/dist/db/plans.d.ts.map +1 -1
  35. package/dist/db/schema.d.ts.map +1 -1
  36. package/dist/db/task-commits.d.ts +0 -51
  37. package/dist/db/task-commits.d.ts.map +1 -1
  38. package/dist/db/task-crud.d.ts.map +1 -1
  39. package/dist/db/task-lifecycle.d.ts +1 -16
  40. package/dist/db/task-lifecycle.d.ts.map +1 -1
  41. package/dist/db/task-relations.d.ts +9 -69
  42. package/dist/db/task-relations.d.ts.map +1 -1
  43. package/dist/db/tasks.d.ts +4 -8
  44. package/dist/db/tasks.d.ts.map +1 -1
  45. package/dist/index.d.ts +14 -119
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +15761 -24428
  48. package/dist/lib/auto-assign.d.ts +5 -3
  49. package/dist/lib/auto-assign.d.ts.map +1 -1
  50. package/dist/lib/config.d.ts +0 -317
  51. package/dist/lib/config.d.ts.map +1 -1
  52. package/dist/lib/extract.d.ts +0 -57
  53. package/dist/lib/extract.d.ts.map +1 -1
  54. package/dist/lib/logger.d.ts +12 -6
  55. package/dist/lib/logger.d.ts.map +1 -1
  56. package/dist/mcp/index.d.ts.map +1 -1
  57. package/dist/mcp/index.js +22576 -28974
  58. package/dist/mcp/token-utils.d.ts +2 -2
  59. package/dist/mcp/token-utils.d.ts.map +1 -1
  60. package/dist/mcp/tools/cloud.d.ts +12 -0
  61. package/dist/mcp/tools/cloud.d.ts.map +1 -0
  62. package/dist/mcp/tools/code-tools.d.ts.map +1 -1
  63. package/dist/mcp/tools/machines.d.ts.map +1 -1
  64. package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
  65. package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
  66. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  67. package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
  68. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  69. package/dist/mcp/tools/task-rel-tools.d.ts.map +1 -1
  70. package/dist/mcp/tools/task-resources.d.ts.map +1 -1
  71. package/dist/mcp/tools/task-workflow-tools.d.ts.map +1 -1
  72. package/dist/mcp/tools/templates.d.ts.map +1 -1
  73. package/dist/sdk/client.d.ts +1 -1
  74. package/dist/sdk/client.d.ts.map +1 -1
  75. package/dist/sdk/types.d.ts +1 -26
  76. package/dist/sdk/types.d.ts.map +1 -1
  77. package/dist/server/index.js +474 -1882
  78. package/dist/server/routes.d.ts.map +1 -1
  79. package/dist/types/index.d.ts +1 -216
  80. package/dist/types/index.d.ts.map +1 -1
  81. package/package.json +6 -30
  82. package/dashboard/dist/assets/index-DJm6m6Yy.css +0 -1
  83. package/dist/capabilities.d.ts +0 -32
  84. package/dist/capabilities.d.ts.map +0 -1
  85. package/dist/cli/commands/agent-reliability-commands.d.ts +0 -3
  86. package/dist/cli/commands/agent-reliability-commands.d.ts.map +0 -1
  87. package/dist/cli/commands/audit-ledger-commands.d.ts +0 -3
  88. package/dist/cli/commands/audit-ledger-commands.d.ts.map +0 -1
  89. package/dist/cli/commands/capacity-commands.d.ts +0 -3
  90. package/dist/cli/commands/capacity-commands.d.ts.map +0 -1
  91. package/dist/cli/commands/environment-snapshots.d.ts +0 -3
  92. package/dist/cli/commands/environment-snapshots.d.ts.map +0 -1
  93. package/dist/cli/commands/knowledge-commands.d.ts +0 -3
  94. package/dist/cli/commands/knowledge-commands.d.ts.map +0 -1
  95. package/dist/cli/commands/local-snapshot-commands.d.ts +0 -3
  96. package/dist/cli/commands/local-snapshot-commands.d.ts.map +0 -1
  97. package/dist/cli/commands/onboarding-commands.d.ts +0 -3
  98. package/dist/cli/commands/onboarding-commands.d.ts.map +0 -1
  99. package/dist/cli/commands/release-compatibility-commands.d.ts +0 -3
  100. package/dist/cli/commands/release-compatibility-commands.d.ts.map +0 -1
  101. package/dist/cli/commands/retrospective-commands.d.ts +0 -3
  102. package/dist/cli/commands/retrospective-commands.d.ts.map +0 -1
  103. package/dist/cli/commands/review-queue-commands.d.ts +0 -3
  104. package/dist/cli/commands/review-queue-commands.d.ts.map +0 -1
  105. package/dist/cli/commands/risk-commands.d.ts +0 -3
  106. package/dist/cli/commands/risk-commands.d.ts.map +0 -1
  107. package/dist/cli/commands/roadmap-commands.d.ts +0 -3
  108. package/dist/cli/commands/roadmap-commands.d.ts.map +0 -1
  109. package/dist/cli/commands/sdk-fixture-commands.d.ts +0 -3
  110. package/dist/cli/commands/sdk-fixture-commands.d.ts.map +0 -1
  111. package/dist/cli-mcp-parity.d.ts +0 -41
  112. package/dist/cli-mcp-parity.d.ts.map +0 -1
  113. package/dist/contracts.d.ts +0 -81
  114. package/dist/contracts.d.ts.map +0 -1
  115. package/dist/contracts.js +0 -16690
  116. package/dist/db/boards.d.ts +0 -56
  117. package/dist/db/boards.d.ts.map +0 -1
  118. package/dist/db/calendar.d.ts +0 -52
  119. package/dist/db/calendar.d.ts.map +0 -1
  120. package/dist/db/inbox.d.ts +0 -47
  121. package/dist/db/inbox.d.ts.map +0 -1
  122. package/dist/db/project-knowledge.d.ts +0 -88
  123. package/dist/db/project-knowledge.d.ts.map +0 -1
  124. package/dist/db/project-risks.d.ts +0 -139
  125. package/dist/db/project-risks.d.ts.map +0 -1
  126. package/dist/db/retrospectives.d.ts +0 -98
  127. package/dist/db/retrospectives.d.ts.map +0 -1
  128. package/dist/db/task-runs.d.ts +0 -130
  129. package/dist/db/task-runs.d.ts.map +0 -1
  130. package/dist/json-contracts.d.ts +0 -56
  131. package/dist/json-contracts.d.ts.map +0 -1
  132. package/dist/lib/activity-timeline.d.ts +0 -43
  133. package/dist/lib/activity-timeline.d.ts.map +0 -1
  134. package/dist/lib/agent-replay-simulator.d.ts +0 -66
  135. package/dist/lib/agent-replay-simulator.d.ts.map +0 -1
  136. package/dist/lib/agent-run-dispatcher.d.ts +0 -62
  137. package/dist/lib/agent-run-dispatcher.d.ts.map +0 -1
  138. package/dist/lib/approval-gates.d.ts +0 -52
  139. package/dist/lib/approval-gates.d.ts.map +0 -1
  140. package/dist/lib/artifact-store.d.ts +0 -68
  141. package/dist/lib/artifact-store.d.ts.map +0 -1
  142. package/dist/lib/audit-ledger.d.ts +0 -59
  143. package/dist/lib/audit-ledger.d.ts.map +0 -1
  144. package/dist/lib/branch-work-plans.d.ts +0 -46
  145. package/dist/lib/branch-work-plans.d.ts.map +0 -1
  146. package/dist/lib/capacity-forecasts.d.ts +0 -70
  147. package/dist/lib/capacity-forecasts.d.ts.map +0 -1
  148. package/dist/lib/context-packs.d.ts +0 -163
  149. package/dist/lib/context-packs.d.ts.map +0 -1
  150. package/dist/lib/doctor.d.ts +0 -46
  151. package/dist/lib/doctor.d.ts.map +0 -1
  152. package/dist/lib/environment-snapshots.d.ts +0 -111
  153. package/dist/lib/environment-snapshots.d.ts.map +0 -1
  154. package/dist/lib/event-hooks.d.ts +0 -58
  155. package/dist/lib/event-hooks.d.ts.map +0 -1
  156. package/dist/lib/external-issue-importers.d.ts +0 -60
  157. package/dist/lib/external-issue-importers.d.ts.map +0 -1
  158. package/dist/lib/local-bridge.d.ts +0 -81
  159. package/dist/lib/local-bridge.d.ts.map +0 -1
  160. package/dist/lib/local-encryption.d.ts +0 -94
  161. package/dist/lib/local-encryption.d.ts.map +0 -1
  162. package/dist/lib/local-extensions.d.ts +0 -75
  163. package/dist/lib/local-extensions.d.ts.map +0 -1
  164. package/dist/lib/local-fields.d.ts +0 -33
  165. package/dist/lib/local-fields.d.ts.map +0 -1
  166. package/dist/lib/local-notifications.d.ts +0 -55
  167. package/dist/lib/local-notifications.d.ts.map +0 -1
  168. package/dist/lib/local-snapshots.d.ts +0 -66
  169. package/dist/lib/local-snapshots.d.ts.map +0 -1
  170. package/dist/lib/mention-resolver.d.ts +0 -43
  171. package/dist/lib/mention-resolver.d.ts.map +0 -1
  172. package/dist/lib/natural-language-intake.d.ts +0 -56
  173. package/dist/lib/natural-language-intake.d.ts.map +0 -1
  174. package/dist/lib/onboarding-fixtures.d.ts +0 -31
  175. package/dist/lib/onboarding-fixtures.d.ts.map +0 -1
  176. package/dist/lib/policy-packs.d.ts +0 -87
  177. package/dist/lib/policy-packs.d.ts.map +0 -1
  178. package/dist/lib/project-bootstrap.d.ts +0 -35
  179. package/dist/lib/project-bootstrap.d.ts.map +0 -1
  180. package/dist/lib/public-release-gate.d.ts +0 -57
  181. package/dist/lib/public-release-gate.d.ts.map +0 -1
  182. package/dist/lib/redaction.d.ts +0 -12
  183. package/dist/lib/redaction.d.ts.map +0 -1
  184. package/dist/lib/release-compatibility.d.ts +0 -59
  185. package/dist/lib/release-compatibility.d.ts.map +0 -1
  186. package/dist/lib/release-notes.d.ts +0 -81
  187. package/dist/lib/release-notes.d.ts.map +0 -1
  188. package/dist/lib/retention-cleanup.d.ts +0 -63
  189. package/dist/lib/retention-cleanup.d.ts.map +0 -1
  190. package/dist/lib/review-queues.d.ts +0 -98
  191. package/dist/lib/review-queues.d.ts.map +0 -1
  192. package/dist/lib/roadmaps.d.ts +0 -133
  193. package/dist/lib/roadmaps.d.ts.map +0 -1
  194. package/dist/lib/runner-sandbox.d.ts +0 -50
  195. package/dist/lib/runner-sandbox.d.ts.map +0 -1
  196. package/dist/lib/saved-search-views.d.ts +0 -60
  197. package/dist/lib/saved-search-views.d.ts.map +0 -1
  198. package/dist/lib/sdk-integration-fixtures.d.ts +0 -65
  199. package/dist/lib/sdk-integration-fixtures.d.ts.map +0 -1
  200. package/dist/lib/task-contracts.d.ts +0 -75
  201. package/dist/lib/task-contracts.d.ts.map +0 -1
  202. package/dist/lib/task-dedupe.d.ts +0 -45
  203. package/dist/lib/task-dedupe.d.ts.map +0 -1
  204. package/dist/lib/terminal-notifications.d.ts +0 -53
  205. package/dist/lib/terminal-notifications.d.ts.map +0 -1
  206. package/dist/lib/todos-md.d.ts +0 -21
  207. package/dist/lib/todos-md.d.ts.map +0 -1
  208. package/dist/lib/verification-providers.d.ts +0 -54
  209. package/dist/lib/verification-providers.d.ts.map +0 -1
  210. package/dist/lib/workflow-prompts.d.ts +0 -38
  211. package/dist/lib/workflow-prompts.d.ts.map +0 -1
  212. package/dist/lib/workspace-trust.d.ts +0 -38
  213. package/dist/lib/workspace-trust.d.ts.map +0 -1
  214. package/dist/mcp/tools/environment-snapshots.d.ts +0 -8
  215. package/dist/mcp/tools/environment-snapshots.d.ts.map +0 -1
  216. package/dist/mcp/tools/workflow-prompts.d.ts +0 -3
  217. package/dist/mcp/tools/workflow-prompts.d.ts.map +0 -1
  218. package/dist/mcp.d.ts +0 -42
  219. package/dist/mcp.d.ts.map +0 -1
  220. package/dist/mcp.js +0 -542
  221. package/dist/registry.d.ts +0 -35
  222. package/dist/registry.d.ts.map +0 -1
  223. package/dist/registry.js +0 -16882
  224. package/dist/release-provenance.json +0 -7
  225. package/dist/sdk/index.js +0 -635
  226. package/dist/storage/index.d.ts +0 -4
  227. package/dist/storage/index.d.ts.map +0 -1
  228. package/dist/storage/interfaces.d.ts +0 -185
  229. package/dist/storage/interfaces.d.ts.map +0 -1
  230. package/dist/storage/local-sqlite.d.ts +0 -7
  231. package/dist/storage/local-sqlite.d.ts.map +0 -1
  232. package/dist/storage.d.ts +0 -4
  233. package/dist/storage.d.ts.map +0 -1
  234. package/dist/storage.js +0 -8237
  235. package/dist/test/no-network.d.ts +0 -7
  236. package/dist/test/no-network.d.ts.map +0 -1
@@ -846,300 +846,6 @@ var init_migrations = __esm(() => {
846
846
  CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix);
847
847
  CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(revoked_at, expires_at);
848
848
  INSERT OR IGNORE INTO _migrations (id) VALUES (50);
849
- `,
850
- `
851
- CREATE TABLE IF NOT EXISTS task_git_refs (
852
- id TEXT PRIMARY KEY,
853
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
854
- ref_type TEXT NOT NULL CHECK(ref_type IN ('branch', 'pull_request')),
855
- name TEXT NOT NULL,
856
- url TEXT,
857
- provider TEXT,
858
- metadata TEXT DEFAULT '{}',
859
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
860
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
861
- UNIQUE(task_id, ref_type, name)
862
- );
863
- CREATE INDEX IF NOT EXISTS idx_task_git_refs_task ON task_git_refs(task_id);
864
- CREATE INDEX IF NOT EXISTS idx_task_git_refs_lookup ON task_git_refs(ref_type, name);
865
- CREATE INDEX IF NOT EXISTS idx_task_git_refs_url ON task_git_refs(url);
866
-
867
- CREATE TABLE IF NOT EXISTS task_verifications (
868
- id TEXT PRIMARY KEY,
869
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
870
- command TEXT NOT NULL,
871
- status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
872
- output_summary TEXT,
873
- artifact_path TEXT,
874
- agent_id TEXT,
875
- run_at TEXT NOT NULL DEFAULT (datetime('now')),
876
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
877
- );
878
- CREATE INDEX IF NOT EXISTS idx_task_verifications_task ON task_verifications(task_id);
879
- CREATE INDEX IF NOT EXISTS idx_task_verifications_status ON task_verifications(status);
880
- INSERT OR IGNORE INTO _migrations (id) VALUES (51);
881
- `,
882
- `
883
- CREATE TABLE IF NOT EXISTS task_runs (
884
- id TEXT PRIMARY KEY,
885
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
886
- agent_id TEXT,
887
- title TEXT,
888
- status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('running', 'completed', 'failed', 'cancelled')),
889
- summary TEXT,
890
- metadata TEXT DEFAULT '{}',
891
- started_at TEXT NOT NULL DEFAULT (datetime('now')),
892
- completed_at TEXT,
893
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
894
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
895
- );
896
- CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id);
897
- CREATE INDEX IF NOT EXISTS idx_task_runs_agent ON task_runs(agent_id);
898
- CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(status);
899
- CREATE INDEX IF NOT EXISTS idx_task_runs_started ON task_runs(started_at);
900
-
901
- CREATE TABLE IF NOT EXISTS task_run_events (
902
- id TEXT PRIMARY KEY,
903
- run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
904
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
905
- event_type TEXT NOT NULL CHECK(event_type IN ('started', 'progress', 'claim', 'comment', 'command', 'file', 'artifact', 'completed', 'failed', 'cancelled')),
906
- message TEXT,
907
- data TEXT DEFAULT '{}',
908
- agent_id TEXT,
909
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
910
- );
911
- CREATE INDEX IF NOT EXISTS idx_task_run_events_run ON task_run_events(run_id);
912
- CREATE INDEX IF NOT EXISTS idx_task_run_events_task ON task_run_events(task_id);
913
- CREATE INDEX IF NOT EXISTS idx_task_run_events_type ON task_run_events(event_type);
914
-
915
- CREATE TABLE IF NOT EXISTS task_run_commands (
916
- id TEXT PRIMARY KEY,
917
- run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
918
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
919
- command TEXT NOT NULL,
920
- status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
921
- exit_code INTEGER,
922
- output_summary TEXT,
923
- artifact_path TEXT,
924
- agent_id TEXT,
925
- started_at TEXT,
926
- completed_at TEXT,
927
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
928
- );
929
- CREATE INDEX IF NOT EXISTS idx_task_run_commands_run ON task_run_commands(run_id);
930
- CREATE INDEX IF NOT EXISTS idx_task_run_commands_task ON task_run_commands(task_id);
931
- CREATE INDEX IF NOT EXISTS idx_task_run_commands_status ON task_run_commands(status);
932
-
933
- CREATE TABLE IF NOT EXISTS task_run_artifacts (
934
- id TEXT PRIMARY KEY,
935
- run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
936
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
937
- path TEXT NOT NULL,
938
- artifact_type TEXT,
939
- description TEXT,
940
- size_bytes INTEGER,
941
- sha256 TEXT,
942
- metadata TEXT DEFAULT '{}',
943
- agent_id TEXT,
944
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
945
- );
946
- CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id);
947
- CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id);
948
- CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path);
949
- INSERT OR IGNORE INTO _migrations (id) VALUES (52);
950
- `,
951
- `
952
- CREATE TABLE IF NOT EXISTS inbox_items (
953
- id TEXT PRIMARY KEY,
954
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
955
- source_type TEXT NOT NULL CHECK(source_type IN ('pasted_error', 'ci_log', 'git_context', 'github_issue', 'file', 'other')),
956
- source_name TEXT,
957
- source_url TEXT,
958
- title TEXT NOT NULL,
959
- body TEXT,
960
- fingerprint TEXT NOT NULL UNIQUE,
961
- status TEXT NOT NULL DEFAULT 'triaged' CHECK(status IN ('new', 'triaged', 'ignored')),
962
- metadata TEXT DEFAULT '{}',
963
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
964
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
965
- );
966
- CREATE INDEX IF NOT EXISTS idx_inbox_items_task ON inbox_items(task_id);
967
- CREATE INDEX IF NOT EXISTS idx_inbox_items_source ON inbox_items(source_type, source_name);
968
- CREATE INDEX IF NOT EXISTS idx_inbox_items_status ON inbox_items(status);
969
- INSERT OR IGNORE INTO _migrations (id) VALUES (53);
970
- `,
971
- `
972
- ALTER TABLE handoffs ADD COLUMN session_id TEXT;
973
- ALTER TABLE handoffs ADD COLUMN task_ids TEXT;
974
- ALTER TABLE handoffs ADD COLUMN relevant_files TEXT;
975
- ALTER TABLE handoffs ADD COLUMN run_ids TEXT;
976
- CREATE TABLE IF NOT EXISTS handoff_acknowledgements (
977
- handoff_id TEXT NOT NULL REFERENCES handoffs(id) ON DELETE CASCADE,
978
- agent_id TEXT NOT NULL,
979
- acknowledged_at TEXT NOT NULL DEFAULT (datetime('now')),
980
- PRIMARY KEY (handoff_id, agent_id)
981
- );
982
- CREATE INDEX IF NOT EXISTS idx_handoff_acks_agent ON handoff_acknowledgements(agent_id, acknowledged_at);
983
- INSERT OR IGNORE INTO _migrations (id) VALUES (54);
984
- `,
985
- `
986
- CREATE TABLE IF NOT EXISTS saved_search_views (
987
- id TEXT PRIMARY KEY,
988
- name TEXT NOT NULL UNIQUE,
989
- description TEXT,
990
- scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('all', 'tasks', 'projects', 'plans', 'runs', 'comments')),
991
- filters TEXT NOT NULL DEFAULT '{}',
992
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
993
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
994
- );
995
- CREATE INDEX IF NOT EXISTS idx_saved_search_views_scope ON saved_search_views(scope);
996
- INSERT OR IGNORE INTO _migrations (id) VALUES (55);
997
- `,
998
- `
999
- ALTER TABLE task_time_logs ADD COLUMN run_id TEXT;
1000
- ALTER TABLE task_time_logs ADD COLUMN focus_session_id TEXT;
1001
- CREATE INDEX IF NOT EXISTS idx_task_time_logs_run ON task_time_logs(run_id);
1002
- CREATE INDEX IF NOT EXISTS idx_task_time_logs_focus_session ON task_time_logs(focus_session_id);
1003
- CREATE TABLE IF NOT EXISTS focus_sessions (
1004
- id TEXT PRIMARY KEY,
1005
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1006
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1007
- run_id TEXT,
1008
- agent_id TEXT,
1009
- title TEXT,
1010
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'paused', 'completed', 'cancelled')),
1011
- started_at TEXT NOT NULL,
1012
- last_resumed_at TEXT,
1013
- paused_at TEXT,
1014
- ended_at TEXT,
1015
- actual_minutes INTEGER NOT NULL DEFAULT 0,
1016
- idle_after_minutes INTEGER,
1017
- notes TEXT,
1018
- metadata TEXT DEFAULT '{}',
1019
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1020
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1021
- );
1022
- CREATE INDEX IF NOT EXISTS idx_focus_sessions_task ON focus_sessions(task_id);
1023
- CREATE INDEX IF NOT EXISTS idx_focus_sessions_plan ON focus_sessions(plan_id);
1024
- CREATE INDEX IF NOT EXISTS idx_focus_sessions_run ON focus_sessions(run_id);
1025
- CREATE INDEX IF NOT EXISTS idx_focus_sessions_agent ON focus_sessions(agent_id);
1026
- CREATE INDEX IF NOT EXISTS idx_focus_sessions_status ON focus_sessions(status);
1027
- INSERT OR IGNORE INTO _migrations (id) VALUES (56);
1028
- `,
1029
- `
1030
- CREATE TABLE IF NOT EXISTS task_boards (
1031
- id TEXT PRIMARY KEY,
1032
- name TEXT NOT NULL UNIQUE,
1033
- scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('tasks', 'plans')),
1034
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1035
- task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL,
1036
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1037
- agent_id TEXT,
1038
- lanes TEXT NOT NULL DEFAULT '[]',
1039
- filters TEXT NOT NULL DEFAULT '{}',
1040
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1041
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1042
- );
1043
- CREATE INDEX IF NOT EXISTS idx_task_boards_scope ON task_boards(scope);
1044
- CREATE INDEX IF NOT EXISTS idx_task_boards_project ON task_boards(project_id);
1045
- CREATE INDEX IF NOT EXISTS idx_task_boards_plan ON task_boards(plan_id);
1046
- INSERT OR IGNORE INTO _migrations (id) VALUES (57);
1047
- `,
1048
- `
1049
- CREATE TABLE IF NOT EXISTS local_calendar_items (
1050
- id TEXT PRIMARY KEY,
1051
- kind TEXT NOT NULL CHECK(kind IN ('task_due', 'task_sla', 'task_reminder', 'milestone', 'work_block', 'run', 'imported')),
1052
- title TEXT NOT NULL,
1053
- description TEXT,
1054
- starts_at TEXT NOT NULL,
1055
- ends_at TEXT,
1056
- timezone TEXT,
1057
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1058
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1059
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1060
- run_id TEXT,
1061
- recurrence_rule TEXT,
1062
- metadata TEXT DEFAULT '{}',
1063
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1064
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1065
- );
1066
- CREATE INDEX IF NOT EXISTS idx_local_calendar_items_time ON local_calendar_items(starts_at, ends_at);
1067
- CREATE INDEX IF NOT EXISTS idx_local_calendar_items_task ON local_calendar_items(task_id);
1068
- CREATE INDEX IF NOT EXISTS idx_local_calendar_items_project ON local_calendar_items(project_id);
1069
- CREATE INDEX IF NOT EXISTS idx_local_calendar_items_kind ON local_calendar_items(kind);
1070
- INSERT OR IGNORE INTO _migrations (id) VALUES (58);
1071
- `,
1072
- `
1073
- CREATE TABLE IF NOT EXISTS project_knowledge_records (
1074
- id TEXT PRIMARY KEY,
1075
- record_type TEXT NOT NULL CHECK(record_type IN ('decision','architecture_note','tradeoff','context_snapshot')),
1076
- title TEXT NOT NULL,
1077
- content TEXT,
1078
- decision TEXT,
1079
- rationale TEXT,
1080
- alternatives TEXT DEFAULT '[]',
1081
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1082
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1083
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1084
- agent_id TEXT,
1085
- snapshot_id TEXT REFERENCES context_snapshots(id) ON DELETE SET NULL,
1086
- tags TEXT DEFAULT '[]',
1087
- metadata TEXT DEFAULT '{}',
1088
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1089
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1090
- );
1091
- CREATE INDEX IF NOT EXISTS idx_project_knowledge_type ON project_knowledge_records(record_type);
1092
- CREATE INDEX IF NOT EXISTS idx_project_knowledge_project ON project_knowledge_records(project_id);
1093
- CREATE INDEX IF NOT EXISTS idx_project_knowledge_task ON project_knowledge_records(task_id);
1094
- CREATE INDEX IF NOT EXISTS idx_project_knowledge_plan ON project_knowledge_records(plan_id);
1095
- CREATE INDEX IF NOT EXISTS idx_project_knowledge_agent ON project_knowledge_records(agent_id);
1096
- CREATE INDEX IF NOT EXISTS idx_project_knowledge_snapshot ON project_knowledge_records(snapshot_id);
1097
- INSERT OR IGNORE INTO _migrations (id) VALUES (59);
1098
- `,
1099
- `
1100
- CREATE TABLE IF NOT EXISTS project_risks (
1101
- id TEXT PRIMARY KEY,
1102
- title TEXT NOT NULL,
1103
- description TEXT,
1104
- status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','mitigating','resolved','accepted')),
1105
- severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low','medium','high','critical')),
1106
- probability TEXT NOT NULL DEFAULT 'medium' CHECK(probability IN ('low','medium','high')),
1107
- owner TEXT,
1108
- mitigation TEXT,
1109
- due_at TEXT,
1110
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1111
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1112
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1113
- tags TEXT DEFAULT '[]',
1114
- metadata TEXT DEFAULT '{}',
1115
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1116
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1117
- closed_at TEXT
1118
- );
1119
- CREATE INDEX IF NOT EXISTS idx_project_risks_status ON project_risks(status);
1120
- CREATE INDEX IF NOT EXISTS idx_project_risks_severity ON project_risks(severity);
1121
- CREATE INDEX IF NOT EXISTS idx_project_risks_project ON project_risks(project_id);
1122
- CREATE INDEX IF NOT EXISTS idx_project_risks_plan ON project_risks(plan_id);
1123
- CREATE INDEX IF NOT EXISTS idx_project_risks_task ON project_risks(task_id);
1124
- CREATE INDEX IF NOT EXISTS idx_project_risks_due ON project_risks(due_at);
1125
- INSERT OR IGNORE INTO _migrations (id) VALUES (60);
1126
- `,
1127
- `
1128
- CREATE TABLE IF NOT EXISTS local_retrospectives (
1129
- id TEXT PRIMARY KEY,
1130
- title TEXT NOT NULL,
1131
- scope TEXT NOT NULL CHECK(scope IN ('project','plan')),
1132
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1133
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1134
- agent_id TEXT,
1135
- report_json TEXT NOT NULL,
1136
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1137
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1138
- );
1139
- CREATE INDEX IF NOT EXISTS idx_local_retrospectives_project ON local_retrospectives(project_id);
1140
- CREATE INDEX IF NOT EXISTS idx_local_retrospectives_plan ON local_retrospectives(plan_id);
1141
- CREATE INDEX IF NOT EXISTS idx_local_retrospectives_agent ON local_retrospectives(agent_id);
1142
- INSERT OR IGNORE INTO _migrations (id) VALUES (61);
1143
849
  `
1144
850
  ];
1145
851
  });
@@ -1224,17 +930,6 @@ function ensureSchema(db) {
1224
930
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1225
931
  tag TEXT NOT NULL, PRIMARY KEY (task_id, tag)
1226
932
  )`);
1227
- ensureTable("task_dependencies", `
1228
- CREATE TABLE task_dependencies (
1229
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1230
- depends_on TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1231
- external_project_id TEXT,
1232
- external_task_id TEXT,
1233
- PRIMARY KEY (task_id, depends_on),
1234
- CHECK (task_id != depends_on)
1235
- )`);
1236
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_dependencies_task ON task_dependencies(task_id)");
1237
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_dependencies_depends_on ON task_dependencies(depends_on)");
1238
933
  ensureTable("task_history", `
1239
934
  CREATE TABLE task_history (
1240
935
  id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
@@ -1292,124 +987,6 @@ function ensureSchema(db) {
1292
987
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
1293
988
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1294
989
  )`);
1295
- ensureTable("handoffs", `
1296
- CREATE TABLE handoffs (
1297
- id TEXT PRIMARY KEY,
1298
- agent_id TEXT,
1299
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1300
- session_id TEXT,
1301
- summary TEXT NOT NULL,
1302
- completed TEXT,
1303
- in_progress TEXT,
1304
- blockers TEXT,
1305
- next_steps TEXT,
1306
- task_ids TEXT,
1307
- relevant_files TEXT,
1308
- run_ids TEXT,
1309
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
1310
- )`);
1311
- ensureTable("handoff_acknowledgements", `
1312
- CREATE TABLE handoff_acknowledgements (
1313
- handoff_id TEXT NOT NULL REFERENCES handoffs(id) ON DELETE CASCADE,
1314
- agent_id TEXT NOT NULL,
1315
- acknowledged_at TEXT NOT NULL DEFAULT (datetime('now')),
1316
- PRIMARY KEY (handoff_id, agent_id)
1317
- )`);
1318
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_handoff_acks_agent ON handoff_acknowledgements(agent_id, acknowledged_at)");
1319
- ensureTable("saved_search_views", `
1320
- CREATE TABLE saved_search_views (
1321
- id TEXT PRIMARY KEY,
1322
- name TEXT NOT NULL UNIQUE,
1323
- description TEXT,
1324
- scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('all', 'tasks', 'projects', 'plans', 'runs', 'comments')),
1325
- filters TEXT NOT NULL DEFAULT '{}',
1326
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1327
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1328
- )`);
1329
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_saved_search_views_scope ON saved_search_views(scope)");
1330
- ensureTable("context_snapshots", `
1331
- CREATE TABLE context_snapshots (
1332
- id TEXT PRIMARY KEY,
1333
- agent_id TEXT,
1334
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1335
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1336
- snapshot_type TEXT NOT NULL CHECK(snapshot_type IN ('interrupt','complete','handoff','checkpoint')),
1337
- plan_summary TEXT,
1338
- files_open TEXT DEFAULT '[]',
1339
- attempts TEXT DEFAULT '[]',
1340
- blockers TEXT DEFAULT '[]',
1341
- next_steps TEXT,
1342
- metadata TEXT DEFAULT '{}',
1343
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
1344
- )`);
1345
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_snapshots_agent ON context_snapshots(agent_id)");
1346
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_snapshots_task ON context_snapshots(task_id)");
1347
- ensureTable("project_knowledge_records", `
1348
- CREATE TABLE project_knowledge_records (
1349
- id TEXT PRIMARY KEY,
1350
- record_type TEXT NOT NULL CHECK(record_type IN ('decision','architecture_note','tradeoff','context_snapshot')),
1351
- title TEXT NOT NULL,
1352
- content TEXT,
1353
- decision TEXT,
1354
- rationale TEXT,
1355
- alternatives TEXT DEFAULT '[]',
1356
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1357
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1358
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1359
- agent_id TEXT,
1360
- snapshot_id TEXT REFERENCES context_snapshots(id) ON DELETE SET NULL,
1361
- tags TEXT DEFAULT '[]',
1362
- metadata TEXT DEFAULT '{}',
1363
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1364
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1365
- )`);
1366
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_knowledge_type ON project_knowledge_records(record_type)");
1367
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_knowledge_project ON project_knowledge_records(project_id)");
1368
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_knowledge_task ON project_knowledge_records(task_id)");
1369
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_knowledge_plan ON project_knowledge_records(plan_id)");
1370
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_knowledge_agent ON project_knowledge_records(agent_id)");
1371
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_knowledge_snapshot ON project_knowledge_records(snapshot_id)");
1372
- ensureTable("project_risks", `
1373
- CREATE TABLE project_risks (
1374
- id TEXT PRIMARY KEY,
1375
- title TEXT NOT NULL,
1376
- description TEXT,
1377
- status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','mitigating','resolved','accepted')),
1378
- severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low','medium','high','critical')),
1379
- probability TEXT NOT NULL DEFAULT 'medium' CHECK(probability IN ('low','medium','high')),
1380
- owner TEXT,
1381
- mitigation TEXT,
1382
- due_at TEXT,
1383
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1384
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1385
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1386
- tags TEXT DEFAULT '[]',
1387
- metadata TEXT DEFAULT '{}',
1388
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1389
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1390
- closed_at TEXT
1391
- )`);
1392
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_risks_status ON project_risks(status)");
1393
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_risks_severity ON project_risks(severity)");
1394
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_risks_project ON project_risks(project_id)");
1395
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_risks_plan ON project_risks(plan_id)");
1396
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_risks_task ON project_risks(task_id)");
1397
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_risks_due ON project_risks(due_at)");
1398
- ensureTable("local_retrospectives", `
1399
- CREATE TABLE local_retrospectives (
1400
- id TEXT PRIMARY KEY,
1401
- title TEXT NOT NULL,
1402
- scope TEXT NOT NULL CHECK(scope IN ('project','plan')),
1403
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1404
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1405
- agent_id TEXT,
1406
- report_json TEXT NOT NULL,
1407
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1408
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1409
- )`);
1410
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_retrospectives_project ON local_retrospectives(project_id)");
1411
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_retrospectives_plan ON local_retrospectives(plan_id)");
1412
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_retrospectives_agent ON local_retrospectives(agent_id)");
1413
990
  ensureTable("task_relationships", `
1414
991
  CREATE TABLE task_relationships (
1415
992
  id TEXT PRIMARY KEY,
@@ -1421,121 +998,6 @@ function ensureSchema(db) {
1421
998
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
1422
999
  CHECK (source_task_id != target_task_id)
1423
1000
  )`);
1424
- ensureTable("task_git_refs", `
1425
- CREATE TABLE task_git_refs (
1426
- id TEXT PRIMARY KEY,
1427
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1428
- ref_type TEXT NOT NULL CHECK(ref_type IN ('branch', 'pull_request')),
1429
- name TEXT NOT NULL,
1430
- url TEXT,
1431
- provider TEXT,
1432
- metadata TEXT DEFAULT '{}',
1433
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1434
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1435
- UNIQUE(task_id, ref_type, name)
1436
- )`);
1437
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_task ON task_git_refs(task_id)");
1438
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_lookup ON task_git_refs(ref_type, name)");
1439
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_url ON task_git_refs(url)");
1440
- ensureTable("task_verifications", `
1441
- CREATE TABLE task_verifications (
1442
- id TEXT PRIMARY KEY,
1443
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1444
- command TEXT NOT NULL,
1445
- status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
1446
- output_summary TEXT,
1447
- artifact_path TEXT,
1448
- agent_id TEXT,
1449
- run_at TEXT NOT NULL DEFAULT (datetime('now')),
1450
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
1451
- )`);
1452
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_verifications_task ON task_verifications(task_id)");
1453
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_verifications_status ON task_verifications(status)");
1454
- ensureTable("task_runs", `
1455
- CREATE TABLE task_runs (
1456
- id TEXT PRIMARY KEY,
1457
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1458
- agent_id TEXT,
1459
- title TEXT,
1460
- status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('running', 'completed', 'failed', 'cancelled')),
1461
- summary TEXT,
1462
- metadata TEXT DEFAULT '{}',
1463
- started_at TEXT NOT NULL DEFAULT (datetime('now')),
1464
- completed_at TEXT,
1465
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1466
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1467
- )`);
1468
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id)");
1469
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_runs_agent ON task_runs(agent_id)");
1470
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(status)");
1471
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_runs_started ON task_runs(started_at)");
1472
- ensureTable("task_run_events", `
1473
- CREATE TABLE task_run_events (
1474
- id TEXT PRIMARY KEY,
1475
- run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
1476
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1477
- event_type TEXT NOT NULL CHECK(event_type IN ('started', 'progress', 'claim', 'comment', 'command', 'file', 'artifact', 'completed', 'failed', 'cancelled')),
1478
- message TEXT,
1479
- data TEXT DEFAULT '{}',
1480
- agent_id TEXT,
1481
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
1482
- )`);
1483
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_events_run ON task_run_events(run_id)");
1484
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_events_task ON task_run_events(task_id)");
1485
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_events_type ON task_run_events(event_type)");
1486
- ensureTable("task_run_commands", `
1487
- CREATE TABLE task_run_commands (
1488
- id TEXT PRIMARY KEY,
1489
- run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
1490
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1491
- command TEXT NOT NULL,
1492
- status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
1493
- exit_code INTEGER,
1494
- output_summary TEXT,
1495
- artifact_path TEXT,
1496
- agent_id TEXT,
1497
- started_at TEXT,
1498
- completed_at TEXT,
1499
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
1500
- )`);
1501
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_commands_run ON task_run_commands(run_id)");
1502
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_commands_task ON task_run_commands(task_id)");
1503
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_commands_status ON task_run_commands(status)");
1504
- ensureTable("task_run_artifacts", `
1505
- CREATE TABLE task_run_artifacts (
1506
- id TEXT PRIMARY KEY,
1507
- run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
1508
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1509
- path TEXT NOT NULL,
1510
- artifact_type TEXT,
1511
- description TEXT,
1512
- size_bytes INTEGER,
1513
- sha256 TEXT,
1514
- metadata TEXT DEFAULT '{}',
1515
- agent_id TEXT,
1516
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
1517
- )`);
1518
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id)");
1519
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id)");
1520
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path)");
1521
- ensureTable("inbox_items", `
1522
- CREATE TABLE inbox_items (
1523
- id TEXT PRIMARY KEY,
1524
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1525
- source_type TEXT NOT NULL CHECK(source_type IN ('pasted_error', 'ci_log', 'git_context', 'github_issue', 'file', 'other')),
1526
- source_name TEXT,
1527
- source_url TEXT,
1528
- title TEXT NOT NULL,
1529
- body TEXT,
1530
- fingerprint TEXT NOT NULL UNIQUE,
1531
- status TEXT NOT NULL DEFAULT 'triaged' CHECK(status IN ('new', 'triaged', 'ignored')),
1532
- metadata TEXT DEFAULT '{}',
1533
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1534
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1535
- )`);
1536
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_inbox_items_task ON inbox_items(task_id)");
1537
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_inbox_items_source ON inbox_items(source_type, source_name)");
1538
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_inbox_items_status ON inbox_items(status)");
1539
1001
  ensureTable("kg_edges", `
1540
1002
  CREATE TABLE kg_edges (
1541
1003
  id TEXT PRIMARY KEY,
@@ -1697,10 +1159,6 @@ function ensureSchema(db) {
1697
1159
  ensureColumn("orgs", "synced_at", "TEXT");
1698
1160
  ensureColumn("handoffs", "machine_id", "TEXT");
1699
1161
  ensureColumn("handoffs", "synced_at", "TEXT");
1700
- ensureColumn("handoffs", "session_id", "TEXT");
1701
- ensureColumn("handoffs", "task_ids", "TEXT");
1702
- ensureColumn("handoffs", "relevant_files", "TEXT");
1703
- ensureColumn("handoffs", "run_ids", "TEXT");
1704
1162
  ensureColumn("task_checklists", "machine_id", "TEXT");
1705
1163
  ensureColumn("project_sources", "machine_id", "TEXT");
1706
1164
  ensureColumn("project_sources", "synced_at", "TEXT");
@@ -1746,8 +1204,6 @@ function ensureSchema(db) {
1746
1204
  CREATE TABLE task_time_logs (
1747
1205
  id TEXT PRIMARY KEY,
1748
1206
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1749
- run_id TEXT,
1750
- focus_session_id TEXT,
1751
1207
  agent_id TEXT,
1752
1208
  started_at TEXT,
1753
1209
  ended_at TEXT,
@@ -1755,77 +1211,9 @@ function ensureSchema(db) {
1755
1211
  notes TEXT,
1756
1212
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
1757
1213
  )`);
1758
- ensureColumn("task_time_logs", "run_id", "TEXT");
1759
- ensureColumn("task_time_logs", "focus_session_id", "TEXT");
1760
1214
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_task ON task_time_logs(task_id)");
1761
1215
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_agent ON task_time_logs(agent_id)");
1762
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_run ON task_time_logs(run_id)");
1763
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_focus_session ON task_time_logs(focus_session_id)");
1764
1216
  ensureColumn("tasks", "actual_minutes", "INTEGER");
1765
- ensureTable("focus_sessions", `
1766
- CREATE TABLE focus_sessions (
1767
- id TEXT PRIMARY KEY,
1768
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1769
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1770
- run_id TEXT,
1771
- agent_id TEXT,
1772
- title TEXT,
1773
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'paused', 'completed', 'cancelled')),
1774
- started_at TEXT NOT NULL,
1775
- last_resumed_at TEXT,
1776
- paused_at TEXT,
1777
- ended_at TEXT,
1778
- actual_minutes INTEGER NOT NULL DEFAULT 0,
1779
- idle_after_minutes INTEGER,
1780
- notes TEXT,
1781
- metadata TEXT DEFAULT '{}',
1782
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1783
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1784
- )`);
1785
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_task ON focus_sessions(task_id)");
1786
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_plan ON focus_sessions(plan_id)");
1787
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_run ON focus_sessions(run_id)");
1788
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_agent ON focus_sessions(agent_id)");
1789
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_status ON focus_sessions(status)");
1790
- ensureTable("task_boards", `
1791
- CREATE TABLE task_boards (
1792
- id TEXT PRIMARY KEY,
1793
- name TEXT NOT NULL UNIQUE,
1794
- scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('tasks', 'plans')),
1795
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1796
- task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL,
1797
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1798
- agent_id TEXT,
1799
- lanes TEXT NOT NULL DEFAULT '[]',
1800
- filters TEXT NOT NULL DEFAULT '{}',
1801
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1802
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1803
- )`);
1804
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_boards_scope ON task_boards(scope)");
1805
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_boards_project ON task_boards(project_id)");
1806
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_boards_plan ON task_boards(plan_id)");
1807
- ensureTable("local_calendar_items", `
1808
- CREATE TABLE local_calendar_items (
1809
- id TEXT PRIMARY KEY,
1810
- kind TEXT NOT NULL CHECK(kind IN ('task_due', 'task_sla', 'task_reminder', 'milestone', 'work_block', 'run', 'imported')),
1811
- title TEXT NOT NULL,
1812
- description TEXT,
1813
- starts_at TEXT NOT NULL,
1814
- ends_at TEXT,
1815
- timezone TEXT,
1816
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1817
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1818
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1819
- run_id TEXT,
1820
- recurrence_rule TEXT,
1821
- metadata TEXT DEFAULT '{}',
1822
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1823
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1824
- )`);
1825
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_time ON local_calendar_items(starts_at, ends_at)");
1826
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_task ON local_calendar_items(task_id)");
1827
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_project ON local_calendar_items(project_id)");
1828
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_kind ON local_calendar_items(kind)");
1829
1217
  ensureTable("task_watchers", `
1830
1218
  CREATE TABLE task_watchers (
1831
1219
  id TEXT PRIMARY KEY,
@@ -1905,22 +1293,12 @@ var init_schema = __esm(() => {
1905
1293
  });
1906
1294
 
1907
1295
  // src/db/machines.ts
1908
- import { hostname as osHostname, platform as osPlatform, arch as osArch } from "os";
1909
- function parseMetadata(value) {
1910
- if (!value)
1911
- return {};
1912
- try {
1913
- const parsed = JSON.parse(value);
1914
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1915
- } catch {
1916
- return {};
1917
- }
1918
- }
1296
+ import { hostname as osHostname, platform as osPlatform } from "os";
1919
1297
  function rowToMachine(row) {
1920
1298
  return {
1921
1299
  ...row,
1922
1300
  is_primary: !!row.is_primary,
1923
- metadata: parseMetadata(row.metadata)
1301
+ metadata: row.metadata ? JSON.parse(row.metadata) : {}
1924
1302
  };
1925
1303
  }
1926
1304
  function getOrCreateLocalMachine(db) {
@@ -1985,7 +1363,6 @@ __export(exports_database, {
1985
1363
  now: () => now,
1986
1364
  lockExpiryCutoff: () => lockExpiryCutoff,
1987
1365
  isLockExpired: () => isLockExpired,
1988
- getDatabasePath: () => getDatabasePath,
1989
1366
  getDatabase: () => getDatabase,
1990
1367
  closeDatabase: () => closeDatabase,
1991
1368
  clearExpiredLocks: () => clearExpiredLocks,
@@ -2047,9 +1424,6 @@ function getDbPath() {
2047
1424
  }
2048
1425
  return newPath;
2049
1426
  }
2050
- function getDatabasePath() {
2051
- return getDbPath();
2052
- }
2053
1427
  function ensureDir(filePath) {
2054
1428
  if (isInMemoryDb(filePath))
2055
1429
  return;
@@ -2074,564 +1448,72 @@ function getDatabase(dbPath) {
2074
1448
  }
2075
1449
  function closeDatabase() {
2076
1450
  if (_db) {
2077
- _db.close();
2078
- _db = null;
2079
- }
2080
- }
2081
- function resetDatabase() {
2082
- _db = null;
2083
- }
2084
- function now() {
2085
- return new Date().toISOString();
2086
- }
2087
- function uuid() {
2088
- return crypto.randomUUID();
2089
- }
2090
- function isLockExpired(lockedAt, nowMs = Date.now()) {
2091
- if (!lockedAt)
2092
- return true;
2093
- const lockTime = new Date(lockedAt).getTime();
2094
- const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
2095
- return nowMs - lockTime > expiryMs;
2096
- }
2097
- function lockExpiryCutoff(nowMs = Date.now()) {
2098
- const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
2099
- return new Date(nowMs - expiryMs).toISOString();
2100
- }
2101
- function clearExpiredLocks(db) {
2102
- const cutoff = lockExpiryCutoff();
2103
- db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
2104
- }
2105
- function resolvePartialId(db, table, partialId) {
2106
- if (!ALLOWED_TABLES.has(table)) {
2107
- throw new Error(`Invalid table name: ${table}`);
2108
- }
2109
- if (partialId.length >= 36) {
2110
- const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
2111
- return row?.id ?? null;
2112
- }
2113
- const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
2114
- if (rows.length === 1) {
2115
- return rows[0].id;
2116
- }
2117
- if (rows.length > 1) {
2118
- return null;
2119
- }
2120
- if (table === "tasks") {
2121
- const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
2122
- if (shortIdRows.length === 1) {
2123
- return shortIdRows[0].id;
2124
- }
2125
- }
2126
- if (table === "task_lists") {
2127
- const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
2128
- if (slugRow)
2129
- return slugRow.id;
2130
- }
2131
- if (table === "projects") {
2132
- const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
2133
- if (nameRow)
2134
- return nameRow.id;
2135
- }
2136
- return null;
2137
- }
2138
- var LOCK_EXPIRY_MINUTES = 30, _db = null, ALLOWED_TABLES;
2139
- var init_database = __esm(() => {
2140
- init_schema();
2141
- init_machines();
2142
- ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
2143
- });
2144
-
2145
- // src/lib/recurrence.ts
2146
- function parseRecurrenceRule(rule) {
2147
- const normalized = rule.trim().toLowerCase();
2148
- if (normalized === "every weekday" || normalized === "every weekdays") {
2149
- return { type: "specific_days", days: [1, 2, 3, 4, 5] };
2150
- }
2151
- if (normalized === "every day" || normalized === "daily") {
2152
- return { type: "interval", interval: 1, unit: "day" };
2153
- }
2154
- if (normalized === "every week" || normalized === "weekly") {
2155
- return { type: "interval", interval: 1, unit: "week" };
2156
- }
2157
- if (normalized === "every month" || normalized === "monthly") {
2158
- return { type: "interval", interval: 1, unit: "month" };
2159
- }
2160
- const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
2161
- if (intervalMatch) {
2162
- return {
2163
- type: "interval",
2164
- interval: parseInt(intervalMatch[1], 10),
2165
- unit: intervalMatch[2]
2166
- };
2167
- }
2168
- const daysMatch = normalized.match(/^every\s+(.+)$/);
2169
- if (daysMatch) {
2170
- const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
2171
- const days = [];
2172
- for (const part of dayParts) {
2173
- const dayNum = DAY_NAMES[part];
2174
- if (dayNum !== undefined) {
2175
- days.push(dayNum);
2176
- }
2177
- }
2178
- if (days.length > 0) {
2179
- return { type: "specific_days", days: days.sort((a, b) => a - b) };
2180
- }
2181
- }
2182
- throw new Error(`Invalid recurrence rule: "${rule}". Supported formats: "every day", "every weekday", "every week", "every 2 weeks", "every month", "every N days/weeks/months", "every monday", "every mon,wed,fri"`);
2183
- }
2184
- function isValidRecurrenceRule(rule) {
2185
- try {
2186
- parseRecurrenceRule(rule);
2187
- return true;
2188
- } catch {
2189
- return false;
2190
- }
2191
- }
2192
- function nextOccurrence(rule, from) {
2193
- const parsed = parseRecurrenceRule(rule);
2194
- const base = from || new Date;
2195
- if (parsed.type === "interval") {
2196
- const next = new Date(base);
2197
- if (parsed.unit === "day") {
2198
- next.setDate(next.getDate() + parsed.interval);
2199
- } else if (parsed.unit === "week") {
2200
- next.setDate(next.getDate() + parsed.interval * 7);
2201
- } else if (parsed.unit === "month") {
2202
- next.setMonth(next.getMonth() + parsed.interval);
2203
- }
2204
- return next.toISOString();
2205
- }
2206
- if (parsed.type === "specific_days") {
2207
- const currentDay = base.getDay();
2208
- const days = parsed.days;
2209
- let daysToAdd = Infinity;
2210
- for (const day of days) {
2211
- let diff = day - currentDay;
2212
- if (diff <= 0)
2213
- diff += 7;
2214
- if (diff < daysToAdd)
2215
- daysToAdd = diff;
2216
- }
2217
- const next = new Date(base);
2218
- next.setDate(next.getDate() + daysToAdd);
2219
- return next.toISOString();
2220
- }
2221
- throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
2222
- }
2223
- var DAY_NAMES;
2224
- var init_recurrence = __esm(() => {
2225
- DAY_NAMES = {
2226
- sunday: 0,
2227
- sun: 0,
2228
- monday: 1,
2229
- mon: 1,
2230
- tuesday: 2,
2231
- tue: 2,
2232
- wednesday: 3,
2233
- wed: 3,
2234
- thursday: 4,
2235
- thu: 4,
2236
- friday: 5,
2237
- fri: 5,
2238
- saturday: 6,
2239
- sat: 6
2240
- };
2241
- });
2242
-
2243
- // src/lib/doctor.ts
2244
- var exports_doctor = {};
2245
- __export(exports_doctor, {
2246
- runTodosDoctor: () => runTodosDoctor
2247
- });
2248
- import { chmodSync, copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, statSync as statSync2 } from "fs";
2249
- import { basename, dirname as dirname5, join as join4 } from "path";
2250
- function tableExists(db, table) {
2251
- return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
2252
- }
2253
- function quoteIdent(identifier) {
2254
- return `"${identifier.replace(/"/g, '""')}"`;
2255
- }
2256
- function getTableColumns(db, table) {
2257
- try {
2258
- const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
2259
- return new Set(rows.map((row) => row.name));
2260
- } catch {
2261
- return new Set;
2262
- }
2263
- }
2264
- function countQuery(db, sql) {
2265
- try {
2266
- const row = db.query(sql).get();
2267
- return row?.count ?? 0;
2268
- } catch {
2269
- return 0;
2270
- }
2271
- }
2272
- function getMigrationLevel(db) {
2273
- try {
2274
- const row = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
2275
- return row?.max_id ?? 0;
2276
- } catch {
2277
- return 0;
2278
- }
2279
- }
2280
- function addCheck(checks, check) {
2281
- checks.push(check);
2282
- }
2283
- function listUserTables(db) {
2284
- return db.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all().map((row) => row.name).sort();
2285
- }
2286
- function findCorruptJsonMetadata(db) {
2287
- const corrupt = [];
2288
- for (const table of listUserTables(db)) {
2289
- const columns = getTableColumns(db, table);
2290
- if (!columns.has("metadata"))
2291
- continue;
2292
- const rows = db.query(`SELECT rowid, metadata FROM ${quoteIdent(table)} WHERE metadata IS NOT NULL AND metadata != ''`).all();
2293
- for (const row of rows) {
2294
- try {
2295
- JSON.parse(row.metadata);
2296
- } catch {
2297
- corrupt.push({ table, column: "metadata", rowid: row.rowid });
2298
- }
2299
- }
2300
- }
2301
- return corrupt;
2302
- }
2303
- function getIndexColumns(db, indexName) {
2304
- return db.query(`PRAGMA index_info(${quoteIdent(indexName)})`).all().map((row) => row.name).filter(Boolean);
2305
- }
2306
- function findDuplicateIndexes(db) {
2307
- const duplicates = [];
2308
- for (const table of listUserTables(db)) {
2309
- const indexes = db.query(`PRAGMA index_list(${quoteIdent(table)})`).all();
2310
- const groups = new Map;
2311
- for (const index of indexes) {
2312
- if (index.origin === "pk" || index.name.startsWith("sqlite_autoindex"))
2313
- continue;
2314
- const columns = getIndexColumns(db, index.name);
2315
- if (columns.length === 0)
2316
- continue;
2317
- const key = `${index.unique}:${columns.join(",")}`;
2318
- const current = groups.get(key) ?? [];
2319
- current.push({ name: index.name, origin: index.origin, columns });
2320
- groups.set(key, current);
2321
- }
2322
- for (const group of groups.values()) {
2323
- if (group.length < 2)
2324
- continue;
2325
- const [kept, ...rest] = group;
2326
- for (const duplicate of rest) {
2327
- duplicates.push({ table, duplicate: duplicate.name, kept: kept.name, columns: duplicate.columns });
2328
- }
2329
- }
2330
- }
2331
- return duplicates;
2332
- }
2333
- function findMissingProjectRoots(db) {
2334
- if (!tableExists(db, "projects"))
2335
- return 0;
2336
- let missing = 0;
2337
- const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
2338
- for (const row of rows) {
2339
- if (/^[a-z]+:\/\//i.test(row.path))
2340
- continue;
2341
- if (!row.path.startsWith("/"))
2342
- continue;
2343
- if (!existsSync5(row.path))
2344
- missing++;
2345
- }
2346
- return missing;
2347
- }
2348
- function addTaskStateChecks(db, checks) {
2349
- if (!tableExists(db, "tasks"))
2350
- return;
2351
- const columns = getTableColumns(db, "tasks");
2352
- const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
2353
- if (columns.has("updated_at") && columns.has("status")) {
2354
- const staleTasks = countQuery(db, `SELECT COUNT(*) as count FROM tasks WHERE status = 'in_progress' AND updated_at < '${staleCutoff}'`);
2355
- if (staleTasks > 0) {
2356
- addCheck(checks, {
2357
- severity: "warn",
2358
- type: "stale_tasks",
2359
- message: `${staleTasks} tasks stuck in_progress for more than 30 minutes`,
2360
- count: staleTasks,
2361
- repairable: false
2362
- });
2363
- }
2364
- }
2365
- if (columns.has("recurrence_rule") && columns.has("status")) {
2366
- const dueAtSelect = columns.has("due_at") ? "due_at" : "NULL as due_at";
2367
- const recurring = db.query(`SELECT recurrence_rule, ${dueAtSelect} FROM tasks WHERE status IN ('pending', 'in_progress') AND recurrence_rule IS NOT NULL AND recurrence_rule != ''`).all();
2368
- const invalidRecurrence = recurring.filter((task) => !isValidRecurrenceRule(task.recurrence_rule));
2369
- if (invalidRecurrence.length > 0) {
2370
- addCheck(checks, {
2371
- severity: "error",
2372
- type: "invalid_recurrence",
2373
- message: `${invalidRecurrence.length} tasks have invalid recurrence rules`,
2374
- count: invalidRecurrence.length,
2375
- repairable: false
2376
- });
2377
- }
2378
- const nowIso = new Date().toISOString();
2379
- const overdueRecurring = recurring.filter((task) => task.due_at !== null && task.due_at < nowIso);
2380
- if (overdueRecurring.length > 0) {
2381
- addCheck(checks, {
2382
- severity: "warn",
2383
- type: "overdue_recurring",
2384
- message: `${overdueRecurring.length} recurring tasks are past due`,
2385
- count: overdueRecurring.length,
2386
- repairable: false
2387
- });
2388
- }
2389
- }
2390
- }
2391
- function databasePermissionsAreUnsafe(dbPath) {
2392
- if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
2393
- return false;
2394
- try {
2395
- return (statSync2(dbPath).mode & 63) !== 0;
2396
- } catch {
2397
- return false;
1451
+ _db.close();
1452
+ _db = null;
2398
1453
  }
2399
1454
  }
2400
- function createBackup(dbPath) {
2401
- if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
2402
- return;
2403
- if (!existsSync5(dbPath))
2404
- return;
2405
- const stamp = now().replace(/[:.]/g, "-");
2406
- const backupDir = join4(dirname5(dbPath), `${basename(dbPath)}.backup-${stamp}`);
2407
- const files = [];
2408
- mkdirSync4(backupDir, { recursive: true });
2409
- for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
2410
- if (!existsSync5(source))
2411
- continue;
2412
- const target = join4(backupDir, basename(source));
2413
- copyFileSync(source, target);
2414
- files.push(target);
2415
- }
2416
- return files.length > 0 ? { path: backupDir, files } : undefined;
1455
+ function resetDatabase() {
1456
+ _db = null;
2417
1457
  }
2418
- function pushRepair(repairs, type, message, applied, count) {
2419
- repairs.push({ type, message, applied, count });
1458
+ function now() {
1459
+ return new Date().toISOString();
2420
1460
  }
2421
- function summarize2(checks, repairs) {
2422
- return {
2423
- errors: checks.filter((check) => check.severity === "error").length,
2424
- warnings: checks.filter((check) => check.severity === "warn").length,
2425
- infos: checks.filter((check) => check.severity === "info").length,
2426
- repairable: checks.filter((check) => check.repairable).length,
2427
- applied: repairs.filter((repair) => repair.applied).length
2428
- };
1461
+ function uuid() {
1462
+ return crypto.randomUUID();
2429
1463
  }
2430
- function deleteOrphans(db, table, where) {
2431
- const before = countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
2432
- if (before > 0)
2433
- db.run(`DELETE FROM ${table} WHERE ${where}`);
2434
- return before;
2435
- }
2436
- function runTodosDoctor(options = {}) {
2437
- const db = options.db ?? getDatabase();
2438
- const dbPath = options.dbPath ?? getDatabasePath();
2439
- const apply = options.apply === true;
2440
- const checks = [];
2441
- const repairs = [];
2442
- const migrationCurrent = getMigrationLevel(db);
2443
- const migrationExpected = MIGRATIONS.length;
2444
- if (migrationCurrent < migrationExpected) {
2445
- addCheck(checks, {
2446
- severity: "error",
2447
- type: "migration_level",
2448
- message: `Migration level ${migrationCurrent}; expected ${migrationExpected}`,
2449
- repairable: true
2450
- });
2451
- } else {
2452
- addCheck(checks, {
2453
- severity: "info",
2454
- type: "migration_level",
2455
- message: `Schema at migration ${migrationCurrent}`
2456
- });
2457
- }
2458
- const missingTables = REQUIRED_TABLES.filter((table) => !tableExists(db, table));
2459
- if (missingTables.length > 0) {
2460
- addCheck(checks, {
2461
- severity: "error",
2462
- type: "missing_schema_tables",
2463
- message: `Missing schema tables: ${missingTables.join(", ")}`,
2464
- count: missingTables.length,
2465
- repairable: true
2466
- });
2467
- }
2468
- const orphanedParents = tableExists(db, "tasks") ? countQuery(db, "SELECT COUNT(*) as count FROM tasks t WHERE t.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = t.parent_id)") : 0;
2469
- if (orphanedParents > 0) {
2470
- addCheck(checks, {
2471
- severity: "error",
2472
- type: "orphaned_task_parents",
2473
- message: `${orphanedParents} tasks reference missing parent tasks`,
2474
- count: orphanedParents,
2475
- repairable: true
2476
- });
1464
+ function isLockExpired(lockedAt) {
1465
+ if (!lockedAt)
1466
+ return true;
1467
+ const lockTime = new Date(lockedAt).getTime();
1468
+ const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
1469
+ return Date.now() - lockTime > expiryMs;
1470
+ }
1471
+ function lockExpiryCutoff(nowMs = Date.now()) {
1472
+ const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
1473
+ return new Date(nowMs - expiryMs).toISOString();
1474
+ }
1475
+ function clearExpiredLocks(db) {
1476
+ const cutoff = lockExpiryCutoff();
1477
+ db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
1478
+ }
1479
+ function resolvePartialId(db, table, partialId) {
1480
+ if (!ALLOWED_TABLES.has(table)) {
1481
+ throw new Error(`Invalid table name: ${table}`);
2477
1482
  }
2478
- const orphanedDependencies = tableExists(db, "task_dependencies") && tableExists(db, "tasks") ? countQuery(db, "SELECT COUNT(*) as count FROM task_dependencies d WHERE NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.depends_on)") : 0;
2479
- if (orphanedDependencies > 0) {
2480
- addCheck(checks, {
2481
- severity: "error",
2482
- type: "orphaned_task_dependencies",
2483
- message: `${orphanedDependencies} dependency rows reference missing tasks`,
2484
- count: orphanedDependencies,
2485
- repairable: true
2486
- });
1483
+ if (partialId.length >= 36) {
1484
+ const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
1485
+ return row?.id ?? null;
2487
1486
  }
2488
- const orphanTables = [
2489
- ["task_comments", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_comments.task_id)"],
2490
- ["task_runs", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_runs.task_id)"],
2491
- ["task_run_events", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_run_events.task_id) OR NOT EXISTS (SELECT 1 FROM task_runs r WHERE r.id = task_run_events.run_id)"],
2492
- ["task_run_commands", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_run_commands.task_id) OR NOT EXISTS (SELECT 1 FROM task_runs r WHERE r.id = task_run_commands.run_id)"],
2493
- ["task_run_artifacts", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_run_artifacts.task_id) OR NOT EXISTS (SELECT 1 FROM task_runs r WHERE r.id = task_run_artifacts.run_id)"]
2494
- ];
2495
- let orphanedRows = 0;
2496
- for (const [table, where] of orphanTables) {
2497
- if (!tableExists(db, table))
2498
- continue;
2499
- orphanedRows += countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
2500
- }
2501
- if (orphanedRows > 0) {
2502
- addCheck(checks, {
2503
- severity: "error",
2504
- type: "orphaned_child_rows",
2505
- message: `${orphanedRows} child rows reference missing tasks or runs`,
2506
- count: orphanedRows,
2507
- repairable: true
2508
- });
1487
+ const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
1488
+ if (rows.length === 1) {
1489
+ return rows[0].id;
2509
1490
  }
2510
- addTaskStateChecks(db, checks);
2511
- const corruptJson = findCorruptJsonMetadata(db);
2512
- if (corruptJson.length > 0) {
2513
- addCheck(checks, {
2514
- severity: "error",
2515
- type: "corrupt_json_metadata",
2516
- message: `${corruptJson.length} metadata values are not valid JSON`,
2517
- count: corruptJson.length,
2518
- repairable: true
2519
- });
1491
+ if (rows.length > 1) {
1492
+ return null;
2520
1493
  }
2521
- const duplicateIndexes = findDuplicateIndexes(db);
2522
- if (duplicateIndexes.length > 0) {
2523
- addCheck(checks, {
2524
- severity: "warn",
2525
- type: "duplicate_indexes",
2526
- message: `${duplicateIndexes.length} duplicate index definitions found`,
2527
- count: duplicateIndexes.length,
2528
- repairable: true
2529
- });
1494
+ if (table === "tasks") {
1495
+ const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
1496
+ if (shortIdRows.length === 1) {
1497
+ return shortIdRows[0].id;
1498
+ }
2530
1499
  }
2531
- const missingProjectRoots = findMissingProjectRoots(db);
2532
- if (missingProjectRoots > 0) {
2533
- addCheck(checks, {
2534
- severity: "warn",
2535
- type: "missing_project_roots",
2536
- message: `${missingProjectRoots} project paths do not exist on this machine`,
2537
- count: missingProjectRoots,
2538
- repairable: false
2539
- });
1500
+ if (table === "task_lists") {
1501
+ const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
1502
+ if (slugRow)
1503
+ return slugRow.id;
2540
1504
  }
2541
- const unsafePermissions = databasePermissionsAreUnsafe(dbPath);
2542
- addCheck(checks, unsafePermissions ? {
2543
- severity: "warn",
2544
- type: "database_permissions",
2545
- message: "Database file is readable or writable by group/others",
2546
- repairable: true
2547
- } : {
2548
- severity: "info",
2549
- type: "database_permissions",
2550
- message: "Database file permissions are private"
2551
- });
2552
- let backup;
2553
- const hasRepairableIssue = checks.some((check) => check.repairable && check.severity !== "info");
2554
- if (apply && hasRepairableIssue) {
2555
- backup = createBackup(dbPath);
2556
- if (backup)
2557
- pushRepair(repairs, "backup_created", `Created backup at ${backup.path}`, true, backup.files.length);
2558
- else
2559
- pushRepair(repairs, "backup_created", "Backup skipped for in-memory or missing database path", false, 0);
2560
- if (migrationCurrent < migrationExpected || missingTables.length > 0) {
2561
- runMigrations(db);
2562
- ensureSchema(db);
2563
- pushRepair(repairs, "schema_repair", "Ran migration and schema safety net", true);
2564
- }
2565
- if (orphanedParents > 0) {
2566
- db.run("UPDATE tasks SET parent_id = NULL WHERE parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = tasks.parent_id)");
2567
- pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
2568
- }
2569
- if (orphanedDependencies > 0 && tableExists(db, "task_dependencies")) {
2570
- const count = deleteOrphans(db, "task_dependencies", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.depends_on)");
2571
- pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count);
2572
- }
2573
- for (const [table, where] of orphanTables) {
2574
- if (!tableExists(db, table))
2575
- continue;
2576
- const count = deleteOrphans(db, table, where);
2577
- if (count > 0)
2578
- pushRepair(repairs, "orphaned_child_rows", `Deleted orphaned rows from ${table}`, true, count);
2579
- }
2580
- if (corruptJson.length > 0) {
2581
- for (const cell of corruptJson) {
2582
- db.run(`UPDATE ${quoteIdent(cell.table)} SET ${quoteIdent(cell.column)} = '{}' WHERE rowid = ?`, [cell.rowid]);
2583
- }
2584
- pushRepair(repairs, "corrupt_json_metadata", "Reset invalid metadata JSON values to {}", true, corruptJson.length);
2585
- }
2586
- if (duplicateIndexes.length > 0) {
2587
- let dropped = 0;
2588
- for (const duplicate of duplicateIndexes) {
2589
- db.run(`DROP INDEX IF EXISTS ${quoteIdent(duplicate.duplicate)}`);
2590
- dropped++;
2591
- }
2592
- pushRepair(repairs, "duplicate_indexes", "Dropped duplicate non-primary indexes", true, dropped);
2593
- }
2594
- if (unsafePermissions) {
2595
- try {
2596
- chmodSync(dbPath, 384);
2597
- pushRepair(repairs, "database_permissions", "Changed database file mode to 0600", true);
2598
- } catch (error) {
2599
- pushRepair(repairs, "database_permissions", error instanceof Error ? error.message : "Failed to repair database permissions", false);
2600
- }
2601
- }
1505
+ if (table === "projects") {
1506
+ const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
1507
+ if (nameRow)
1508
+ return nameRow.id;
2602
1509
  }
2603
- const finalChecks = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }).checks : checks;
2604
- const summary = summarize2(finalChecks, repairs);
2605
- return {
2606
- ok: !finalChecks.some((check) => check.severity === "error"),
2607
- dry_run: !apply,
2608
- database_path: dbPath,
2609
- migration: { current: getMigrationLevel(db), expected: migrationExpected },
2610
- backup,
2611
- checks: finalChecks,
2612
- repairs,
2613
- summary
2614
- };
1510
+ return null;
2615
1511
  }
2616
- var REQUIRED_TABLES;
2617
- var init_doctor = __esm(() => {
2618
- init_database();
2619
- init_migrations();
1512
+ var LOCK_EXPIRY_MINUTES = 30, _db = null, ALLOWED_TABLES;
1513
+ var init_database = __esm(() => {
2620
1514
  init_schema();
2621
- init_recurrence();
2622
- REQUIRED_TABLES = [
2623
- "_migrations",
2624
- "projects",
2625
- "tasks",
2626
- "plans",
2627
- "agents",
2628
- "task_dependencies",
2629
- "task_comments",
2630
- "task_runs",
2631
- "task_run_events",
2632
- "task_run_commands",
2633
- "task_run_artifacts"
2634
- ];
1515
+ init_machines();
1516
+ ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates"]);
2635
1517
  });
2636
1518
 
2637
1519
  // src/lib/package-version.ts
@@ -2659,8 +1541,8 @@ function getPackageVersion(fromUrl = import.meta.url) {
2659
1541
 
2660
1542
  // src/server/serve.ts
2661
1543
  init_database();
2662
- import { existsSync as existsSync6 } from "fs";
2663
- import { join as join6, dirname as dirname6, extname } from "path";
1544
+ import { existsSync as existsSync5 } from "fs";
1545
+ import { join as join5, dirname as dirname3, extname } from "path";
2664
1546
  import { fileURLToPath as fileURLToPath2 } from "url";
2665
1547
 
2666
1548
  // src/db/api-keys.ts
@@ -2771,7 +1653,7 @@ init_database();
2771
1653
 
2772
1654
  // src/lib/config.ts
2773
1655
  import { existsSync as existsSync4 } from "fs";
2774
- import { dirname as dirname3, join as join3 } from "path";
1656
+ import { join as join3 } from "path";
2775
1657
 
2776
1658
  // src/lib/sync-utils.ts
2777
1659
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync } from "fs";
@@ -2924,494 +1806,6 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
2924
1806
  }
2925
1807
  }
2926
1808
 
2927
- // src/lib/event-hooks.ts
2928
- import { createHash as createHash2, randomUUID } from "crypto";
2929
- import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
2930
- import { dirname as dirname4, resolve as resolve4 } from "path";
2931
- import { createConnection } from "net";
2932
-
2933
- // src/lib/redaction.ts
2934
- var DEFAULT_SECRET_PATTERNS = [
2935
- { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
2936
- { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
2937
- { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
2938
- { name: "env-secret-assignment", regex: /\b([A-Za-z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD)[A-Za-z0-9_]*)\s*=\s*['"]?[^'"\s]{8,}/gi, replacement: "$1=[REDACTED]" },
2939
- { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
2940
- ];
2941
- var DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
2942
- function unique(values) {
2943
- return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
2944
- }
2945
- function cloneRegex(regex) {
2946
- return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
2947
- }
2948
- function customPatterns() {
2949
- return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
2950
- try {
2951
- return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
2952
- } catch {
2953
- return [];
2954
- }
2955
- });
2956
- }
2957
- function secretPatterns() {
2958
- return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
2959
- }
2960
- function isSecretKey(key) {
2961
- if (DEFAULT_SECRET_KEY_PATTERN.test(key))
2962
- return true;
2963
- return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
2964
- }
2965
- function redactEvidenceText(value) {
2966
- let redacted = value;
2967
- for (const pattern of secretPatterns()) {
2968
- const regex = cloneRegex(pattern.regex);
2969
- const replacement = pattern.replacement ?? "[REDACTED]";
2970
- redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
2971
- }
2972
- return redacted;
2973
- }
2974
- function redactValue(value) {
2975
- if (typeof value === "string")
2976
- return redactEvidenceText(value);
2977
- if (Array.isArray(value))
2978
- return value.map(redactValue);
2979
- if (value && typeof value === "object") {
2980
- const redacted = {};
2981
- for (const [key, child] of Object.entries(value)) {
2982
- if (isSecretKey(key)) {
2983
- redacted[key] = "[REDACTED]";
2984
- } else {
2985
- redacted[key] = redactValue(child);
2986
- }
2987
- }
2988
- return redacted;
2989
- }
2990
- return value;
2991
- }
2992
-
2993
- // src/lib/runner-sandbox.ts
2994
- import { relative as relative2, resolve as resolve3 } from "path";
2995
-
2996
- // src/lib/workspace-trust.ts
2997
- import { relative, resolve as resolve2 } from "path";
2998
- var DEFAULT_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
2999
- var DEFAULT_ENV_REDACTIONS = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
3000
- var PRESET_DEFAULTS = {
3001
- restricted: {
3002
- trusted: false,
3003
- preset: "restricted",
3004
- command_allowlist: ["todos"],
3005
- command_denylist: DEFAULT_DENYLIST,
3006
- tool_permissions: ["read"],
3007
- write_scopes: [],
3008
- env_redactions: DEFAULT_ENV_REDACTIONS,
3009
- require_prompt_for_unsafe: true
3010
- },
3011
- readonly: {
3012
- trusted: false,
3013
- preset: "readonly",
3014
- command_allowlist: ["todos", "git status", "git diff", "bun test"],
3015
- command_denylist: DEFAULT_DENYLIST,
3016
- tool_permissions: ["read", "list", "search"],
3017
- write_scopes: [],
3018
- env_redactions: DEFAULT_ENV_REDACTIONS,
3019
- require_prompt_for_unsafe: true
3020
- },
3021
- standard: {
3022
- trusted: true,
3023
- preset: "standard",
3024
- command_allowlist: ["todos", "git", "bun", "rg"],
3025
- command_denylist: DEFAULT_DENYLIST,
3026
- tool_permissions: ["read", "write", "test", "mcp"],
3027
- write_scopes: ["."],
3028
- env_redactions: DEFAULT_ENV_REDACTIONS,
3029
- require_prompt_for_unsafe: true
3030
- },
3031
- trusted: {
3032
- trusted: true,
3033
- preset: "trusted",
3034
- command_allowlist: ["*"],
3035
- command_denylist: DEFAULT_DENYLIST,
3036
- tool_permissions: ["*"],
3037
- write_scopes: ["."],
3038
- env_redactions: DEFAULT_ENV_REDACTIONS,
3039
- require_prompt_for_unsafe: false
3040
- }
3041
- };
3042
- function normalizePath(path) {
3043
- return resolve2(path);
3044
- }
3045
- function unique2(values) {
3046
- return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
3047
- }
3048
- function defaultProfile(root, preset) {
3049
- return {
3050
- root,
3051
- ...PRESET_DEFAULTS[preset]
3052
- };
3053
- }
3054
- function configuredProfiles(config = loadConfig()) {
3055
- return Object.values(config.workspace_trust || {}).map((profile) => ({ ...profile, root: normalizePath(profile.root) })).sort((a, b) => b.root.length - a.root.length);
3056
- }
3057
- function isPathInside(root, path) {
3058
- const rel = relative(root, path);
3059
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/") && !/^[A-Za-z]:/.test(rel);
3060
- }
3061
- function matchesPattern(value, pattern) {
3062
- if (pattern === "*")
3063
- return true;
3064
- if (pattern.includes("*")) {
3065
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
3066
- return new RegExp(`^${escaped}$`, "i").test(value);
3067
- }
3068
- return value === pattern || value.startsWith(`${pattern} `) || value.includes(pattern);
3069
- }
3070
- function profileFor(path) {
3071
- const resolved = normalizePath(path);
3072
- for (const profile of configuredProfiles()) {
3073
- if (isPathInside(profile.root, resolved))
3074
- return { profile, matchedRoot: profile.root };
3075
- }
3076
- return { profile: defaultProfile(resolved, "restricted"), matchedRoot: null };
3077
- }
3078
- function getWorkspaceTrustStatus(path = process.cwd()) {
3079
- const root = normalizePath(path);
3080
- const { profile, matchedRoot } = profileFor(root);
3081
- return {
3082
- root,
3083
- trusted: profile.trusted,
3084
- matched_root: matchedRoot,
3085
- profile
3086
- };
3087
- }
3088
- function writeAllowed(profile, root, writePath) {
3089
- const target = normalizePath(writePath.startsWith("/") ? writePath : `${root}/${writePath}`);
3090
- return profile.write_scopes.some((scope) => {
3091
- const scopeRoot = normalizePath(scope.startsWith("/") ? scope : `${root}/${scope}`);
3092
- return isPathInside(scopeRoot, target);
3093
- });
3094
- }
3095
- function redactedEnvKeys(profile, env) {
3096
- if (!env)
3097
- return [];
3098
- const patterns = unique2([...DEFAULT_ENV_REDACTIONS, ...profile.env_redactions]).map((item) => item.toUpperCase());
3099
- return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
3100
- }
3101
- function checkWorkspacePermission(input = {}) {
3102
- const status = getWorkspaceTrustStatus(input.path || process.cwd());
3103
- const reasons = [];
3104
- const profile = status.profile;
3105
- if (!status.matched_root)
3106
- reasons.push("workspace is not trusted");
3107
- if (input.command) {
3108
- if (profile.command_denylist.some((pattern) => matchesPattern(input.command, pattern))) {
3109
- reasons.push("command matches denylist");
3110
- } else if (!profile.command_allowlist.some((pattern) => matchesPattern(input.command, pattern))) {
3111
- reasons.push("command is not in allowlist");
3112
- }
3113
- }
3114
- if (input.tool && !profile.tool_permissions.some((permission) => matchesPattern(input.tool, permission))) {
3115
- reasons.push("tool permission is not allowed");
3116
- }
3117
- if (input.write_path && !writeAllowed(profile, status.matched_root || status.root, input.write_path)) {
3118
- reasons.push("write path is outside allowed scopes");
3119
- }
3120
- const redacted = redactedEnvKeys(profile, input.env);
3121
- const allowed = reasons.length === 0;
3122
- return {
3123
- allowed,
3124
- requires_prompt: !allowed && profile.require_prompt_for_unsafe,
3125
- reasons,
3126
- status,
3127
- redacted_env_keys: redacted
3128
- };
3129
- }
3130
-
3131
- // src/lib/runner-sandbox.ts
3132
- var DEFAULT_COMMAND_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
3133
- var DEFAULT_ENV_REDACTIONS2 = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
3134
- function normalizePath2(path) {
3135
- return resolve3(path);
3136
- }
3137
- function unique3(values) {
3138
- return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
3139
- }
3140
- function configuredProfiles2(config = loadConfig()) {
3141
- return Object.values(config.runner_sandboxes || {}).map((profile) => ({
3142
- ...profile,
3143
- root: normalizePath2(profile.root),
3144
- cwd_boundary: normalizePath2(profile.cwd_boundary || profile.root)
3145
- })).sort((a, b) => a.name.localeCompare(b.name));
3146
- }
3147
- function isPathInside2(root, path) {
3148
- const rel = relative2(root, path);
3149
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/") && !/^[A-Za-z]:/.test(rel);
3150
- }
3151
- function matchesPattern2(value, pattern) {
3152
- if (pattern === "*")
3153
- return true;
3154
- if (pattern.includes("*")) {
3155
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
3156
- return new RegExp(`^${escaped}$`, "i").test(value);
3157
- }
3158
- return value === pattern || value.startsWith(`${pattern} `) || value.includes(pattern);
3159
- }
3160
- function resolveFromRoot(root, path) {
3161
- return normalizePath2(path.startsWith("/") ? path : `${root}/${path}`);
3162
- }
3163
- function defaultProfile2(name, root) {
3164
- const normalizedRoot = normalizePath2(root);
3165
- return {
3166
- name,
3167
- root: normalizedRoot,
3168
- command_allowlist: ["todos", "git", "bun"],
3169
- command_denylist: DEFAULT_COMMAND_DENYLIST,
3170
- cwd_boundary: normalizedRoot,
3171
- write_scopes: ["."],
3172
- env_allowlist: ["PATH", "HOME", "SHELL", "TMPDIR", "TEMP", "TMP", "CI", "NODE_ENV", "BUN_ENV"],
3173
- env_redactions: DEFAULT_ENV_REDACTIONS2,
3174
- network_policy: "none",
3175
- require_approval: true,
3176
- audit_evidence: true
3177
- };
3178
- }
3179
- function profileByName(name, path) {
3180
- const profiles = configuredProfiles2();
3181
- if (name) {
3182
- const found = profiles.find((profile) => profile.name === name);
3183
- if (found)
3184
- return found;
3185
- return defaultProfile2(name, path);
3186
- }
3187
- const resolved = normalizePath2(path);
3188
- return profiles.find((profile) => isPathInside2(profile.root, resolved)) || defaultProfile2("default", resolved);
3189
- }
3190
- function redactedEnvKeys2(profile, env) {
3191
- if (!env)
3192
- return [];
3193
- const patterns = unique3([...DEFAULT_ENV_REDACTIONS2, ...profile.env_redactions]).map((item) => item.toUpperCase());
3194
- return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
3195
- }
3196
- function omittedEnvKeys(profile, env) {
3197
- if (!env)
3198
- return [];
3199
- if (profile.env_allowlist.includes("*"))
3200
- return [];
3201
- return Object.keys(env).filter((key) => !profile.env_allowlist.some((pattern) => matchesPattern2(key, pattern)));
3202
- }
3203
- function resolveFromCwd(cwd, path) {
3204
- return normalizePath2(path.startsWith("/") ? path : `${cwd}/${path}`);
3205
- }
3206
- function writeAllowed2(profile, cwd, writePath) {
3207
- const target = resolveFromCwd(cwd, writePath);
3208
- return profile.write_scopes.some((scope) => isPathInside2(resolveFromRoot(profile.root, scope), target));
3209
- }
3210
- function checkRunnerSandbox(input = {}) {
3211
- const path = normalizePath2(input.path || input.cwd || process.cwd());
3212
- const profile = profileByName(input.name, path);
3213
- const cwd = resolveFromRoot(profile.root, input.cwd || profile.root);
3214
- const reasons = [];
3215
- const writePaths = input.write_paths || [];
3216
- const resolvedWritePaths = writePaths.map((writePath) => resolveFromCwd(cwd, writePath));
3217
- if (!isPathInside2(profile.cwd_boundary, cwd))
3218
- reasons.push("cwd is outside sandbox boundary");
3219
- if (input.command) {
3220
- if (profile.command_denylist.some((pattern) => matchesPattern2(input.command, pattern))) {
3221
- reasons.push("command matches sandbox denylist");
3222
- } else if (!profile.command_allowlist.some((pattern) => matchesPattern2(input.command, pattern))) {
3223
- reasons.push("command is not in sandbox allowlist");
3224
- }
3225
- }
3226
- for (const writePath of writePaths) {
3227
- if (!writeAllowed2(profile, cwd, writePath)) {
3228
- reasons.push(`write path is outside sandbox scopes: ${writePath}`);
3229
- }
3230
- }
3231
- if (input.network && profile.network_policy === "none") {
3232
- reasons.push("network access is disabled by sandbox policy");
3233
- }
3234
- const trustChecks = [
3235
- checkWorkspacePermission({ path: profile.root, command: input.command, env: input.env }),
3236
- ...resolvedWritePaths.map((writePath) => checkWorkspacePermission({ path: profile.root, write_path: writePath }))
3237
- ];
3238
- for (const trust of trustChecks) {
3239
- for (const reason of trust.reasons)
3240
- reasons.push(`workspace trust: ${reason}`);
3241
- }
3242
- const redacted = redactedEnvKeys2(profile, input.env);
3243
- const omitted = omittedEnvKeys(profile, input.env);
3244
- const effective = Object.keys(input.env || {}).filter((key) => !omitted.includes(key));
3245
- const uniqueReasons = unique3(reasons);
3246
- const allowed = uniqueReasons.length === 0;
3247
- return {
3248
- allowed,
3249
- requires_approval: !allowed && profile.require_approval,
3250
- reasons: uniqueReasons,
3251
- profile,
3252
- redacted_env_keys: redacted,
3253
- omitted_env_keys: omitted,
3254
- effective_env_keys: effective,
3255
- audit_evidence: profile.audit_evidence ? {
3256
- sandbox: profile.name,
3257
- root: profile.root,
3258
- cwd,
3259
- command: input.command,
3260
- write_paths: writePaths,
3261
- network_requested: Boolean(input.network),
3262
- network_policy: profile.network_policy,
3263
- allowed,
3264
- reasons: uniqueReasons
3265
- } : null
3266
- };
3267
- }
3268
-
3269
- // src/lib/event-hooks.ts
3270
- var VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
3271
- function clampAttempts(value) {
3272
- if (!Number.isFinite(value))
3273
- return 1;
3274
- return Math.min(5, Math.max(1, Math.trunc(value)));
3275
- }
3276
- function eventMatches(hook, eventType) {
3277
- return hook.enabled !== false && (hook.events.includes("*") || hook.events.includes(eventType));
3278
- }
3279
- function canonicalEvent(input) {
3280
- return JSON.stringify(input);
3281
- }
3282
- function buildEnvelope(type, payload, timestamp = new Date().toISOString()) {
3283
- const base = {
3284
- id: randomUUID(),
3285
- type,
3286
- timestamp,
3287
- payload: redactValue(payload ?? {}),
3288
- source: { package: "@hasna/todos", local_only: true }
3289
- };
3290
- const digest = createHash2("sha256").update(canonicalEvent(base)).digest("hex");
3291
- return { ...base, integrity: { algorithm: "sha256", digest } };
3292
- }
3293
- function summarize(value) {
3294
- const redacted = redactEvidenceText(value.trim());
3295
- if (!redacted)
3296
- return;
3297
- return redacted.length > 1000 ? `${redacted.slice(0, 997)}...` : redacted;
3298
- }
3299
- function sleep(ms) {
3300
- return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
3301
- }
3302
- async function writeSocket(socketPath, line) {
3303
- await new Promise((resolveWrite, rejectWrite) => {
3304
- const socket = createConnection(socketPath);
3305
- const timeout = setTimeout(() => {
3306
- socket.destroy();
3307
- rejectWrite(new Error(`socket write timed out: ${socketPath}`));
3308
- }, 1000);
3309
- socket.on("error", (error) => {
3310
- clearTimeout(timeout);
3311
- rejectWrite(error);
3312
- });
3313
- socket.on("connect", () => {
3314
- socket.end(line, () => {
3315
- clearTimeout(timeout);
3316
- resolveWrite();
3317
- });
3318
- });
3319
- });
3320
- }
3321
- async function deliverScript(hook, envelope) {
3322
- const command = hook.command;
3323
- const cwd = hook.cwd || process.cwd();
3324
- if (hook.sandbox) {
3325
- const check = checkRunnerSandbox({ name: hook.sandbox, cwd, command, env: hook.env });
3326
- if (!check.allowed)
3327
- throw new Error(check.reasons.join("; "));
3328
- }
3329
- const proc = Bun.spawn(["bash", "-lc", command], {
3330
- cwd,
3331
- env: {
3332
- ...process.env,
3333
- ...hook.env || {},
3334
- TODOS_EVENT_JSON: JSON.stringify(envelope),
3335
- TODOS_EVENT_ID: envelope.id,
3336
- TODOS_EVENT_TYPE: envelope.type,
3337
- TODOS_EVENT_INTEGRITY: envelope.integrity.digest,
3338
- TODOS_HOOK_NAME: hook.name
3339
- },
3340
- stdout: "pipe",
3341
- stderr: "pipe"
3342
- });
3343
- const [stdout, stderr, exitCode] = await Promise.all([
3344
- new Response(proc.stdout).text(),
3345
- new Response(proc.stderr).text(),
3346
- proc.exited
3347
- ]);
3348
- return { exitCode, output: summarize([stdout, stderr].filter(Boolean).join(`
3349
- `)) };
3350
- }
3351
- async function deliverHook(hook, envelope) {
3352
- const line = `${JSON.stringify(envelope)}
3353
- `;
3354
- const maxAttempts = clampAttempts(hook.retry?.attempts ?? 1);
3355
- const backoffMs = Math.max(0, hook.retry?.backoff_ms ?? 0);
3356
- let lastError;
3357
- let output;
3358
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
3359
- try {
3360
- if (hook.target === "stdout") {
3361
- output = line.trim();
3362
- } else if (hook.target === "file") {
3363
- const filePath = resolve4(hook.file_path);
3364
- mkdirSync3(dirname4(filePath), { recursive: true });
3365
- appendFileSync(filePath, line);
3366
- } else if (hook.target === "socket") {
3367
- await writeSocket(hook.socket_path, line);
3368
- } else {
3369
- const result = await deliverScript(hook, envelope);
3370
- output = result.output;
3371
- if (result.exitCode !== 0)
3372
- throw new Error(`script exited ${result.exitCode}${output ? `: ${output}` : ""}`);
3373
- }
3374
- return {
3375
- hook: hook.name,
3376
- event_id: envelope.id,
3377
- event_type: envelope.type,
3378
- target: hook.target,
3379
- status: "delivered",
3380
- attempts: attempt,
3381
- integrity: envelope.integrity,
3382
- output_summary: output
3383
- };
3384
- } catch (error) {
3385
- lastError = error instanceof Error ? error.message : String(error);
3386
- if (attempt < maxAttempts && backoffMs > 0)
3387
- await sleep(backoffMs);
3388
- }
3389
- }
3390
- return {
3391
- hook: hook.name,
3392
- event_id: envelope.id,
3393
- event_type: envelope.type,
3394
- target: hook.target,
3395
- status: "failed",
3396
- attempts: maxAttempts,
3397
- integrity: envelope.integrity,
3398
- error: redactEvidenceText(lastError || "delivery failed")
3399
- };
3400
- }
3401
- function listLocalEventHooks() {
3402
- return Object.values(loadConfig().local_event_hooks || {}).sort((a, b) => a.name.localeCompare(b.name));
3403
- }
3404
- async function emitLocalEventHooks(input) {
3405
- const hooks = (input.hooks || listLocalEventHooks()).filter((hook) => eventMatches(hook, input.type));
3406
- if (hooks.length === 0)
3407
- return [];
3408
- const envelope = buildEnvelope(input.type, input.payload, input.timestamp);
3409
- return Promise.all(hooks.map((hook) => deliverHook(hook, envelope)));
3410
- }
3411
- function emitLocalEventHooksQuiet(input) {
3412
- emitLocalEventHooks(input).catch(() => {});
3413
- }
3414
-
3415
1809
  // src/db/audit.ts
3416
1810
  init_database();
3417
1811
  function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
@@ -3679,8 +2073,8 @@ function createTask(input, db) {
3679
2073
  let id = uuid();
3680
2074
  for (let attempt = 0;attempt < 3; attempt++) {
3681
2075
  try {
3682
- d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, 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)
3683
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2076
+ d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, confidence, retry_count, max_retries, retry_after, 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)
2077
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3684
2078
  id,
3685
2079
  null,
3686
2080
  input.project_id || null,
@@ -3702,7 +2096,6 @@ function createTask(input, db) {
3702
2096
  timestamp,
3703
2097
  input.due_at || null,
3704
2098
  input.estimated_minutes || null,
3705
- input.sla_minutes ?? null,
3706
2099
  input.confidence ?? null,
3707
2100
  input.retry_count ?? 0,
3708
2101
  input.max_retries ?? 3,
@@ -3982,10 +2375,6 @@ function updateTask(id, input, db) {
3982
2375
  sets.push("estimated_minutes = ?");
3983
2376
  params.push(input.estimated_minutes);
3984
2377
  }
3985
- if (input.sla_minutes !== undefined) {
3986
- sets.push("sla_minutes = ?");
3987
- params.push(input.sla_minutes);
3988
- }
3989
2378
  if (input.actual_minutes !== undefined) {
3990
2379
  sets.push("actual_minutes = ?");
3991
2380
  params.push(input.actual_minutes);
@@ -4050,14 +2439,9 @@ function updateTask(id, input, db) {
4050
2439
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
4051
2440
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
4052
2441
  dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
4053
- emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id, assigned_to: input.assigned_to, title: task.title } });
4054
2442
  }
4055
2443
  if (input.status !== undefined && input.status !== task.status) {
4056
2444
  dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
4057
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload: { id, old_status: task.status, new_status: input.status, title: task.title } });
4058
- }
4059
- if (input.approved_by !== undefined) {
4060
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
4061
2445
  }
4062
2446
  return {
4063
2447
  ...task,
@@ -4067,7 +2451,6 @@ function updateTask(id, input, db) {
4067
2451
  version: task.version + 1,
4068
2452
  updated_at: timestamp,
4069
2453
  completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : task.completed_at,
4070
- sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
4071
2454
  actual_minutes: input.actual_minutes ?? task.actual_minutes,
4072
2455
  confidence: input.confidence !== undefined ? input.confidence : task.confidence,
4073
2456
  retry_count: input.retry_count ?? task.retry_count,
@@ -4083,9 +2466,95 @@ function deleteTask(id, db) {
4083
2466
  const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
4084
2467
  return result.changes > 0;
4085
2468
  }
4086
- // src/db/task-lifecycle.ts
4087
- init_database();
4088
- init_recurrence();
2469
+ // src/db/task-lifecycle.ts
2470
+ init_database();
2471
+
2472
+ // src/lib/recurrence.ts
2473
+ var DAY_NAMES = {
2474
+ sunday: 0,
2475
+ sun: 0,
2476
+ monday: 1,
2477
+ mon: 1,
2478
+ tuesday: 2,
2479
+ tue: 2,
2480
+ wednesday: 3,
2481
+ wed: 3,
2482
+ thursday: 4,
2483
+ thu: 4,
2484
+ friday: 5,
2485
+ fri: 5,
2486
+ saturday: 6,
2487
+ sat: 6
2488
+ };
2489
+ function parseRecurrenceRule(rule) {
2490
+ const normalized = rule.trim().toLowerCase();
2491
+ if (normalized === "every weekday" || normalized === "every weekdays") {
2492
+ return { type: "specific_days", days: [1, 2, 3, 4, 5] };
2493
+ }
2494
+ if (normalized === "every day" || normalized === "daily") {
2495
+ return { type: "interval", interval: 1, unit: "day" };
2496
+ }
2497
+ if (normalized === "every week" || normalized === "weekly") {
2498
+ return { type: "interval", interval: 1, unit: "week" };
2499
+ }
2500
+ if (normalized === "every month" || normalized === "monthly") {
2501
+ return { type: "interval", interval: 1, unit: "month" };
2502
+ }
2503
+ const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
2504
+ if (intervalMatch) {
2505
+ return {
2506
+ type: "interval",
2507
+ interval: parseInt(intervalMatch[1], 10),
2508
+ unit: intervalMatch[2]
2509
+ };
2510
+ }
2511
+ const daysMatch = normalized.match(/^every\s+(.+)$/);
2512
+ if (daysMatch) {
2513
+ const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
2514
+ const days = [];
2515
+ for (const part of dayParts) {
2516
+ const dayNum = DAY_NAMES[part];
2517
+ if (dayNum !== undefined) {
2518
+ days.push(dayNum);
2519
+ }
2520
+ }
2521
+ if (days.length > 0) {
2522
+ return { type: "specific_days", days: days.sort((a, b) => a - b) };
2523
+ }
2524
+ }
2525
+ throw new Error(`Invalid recurrence rule: "${rule}". Supported formats: "every day", "every weekday", "every week", "every 2 weeks", "every month", "every N days/weeks/months", "every monday", "every mon,wed,fri"`);
2526
+ }
2527
+ function nextOccurrence(rule, from) {
2528
+ const parsed = parseRecurrenceRule(rule);
2529
+ const base = from || new Date;
2530
+ if (parsed.type === "interval") {
2531
+ const next = new Date(base);
2532
+ if (parsed.unit === "day") {
2533
+ next.setDate(next.getDate() + parsed.interval);
2534
+ } else if (parsed.unit === "week") {
2535
+ next.setDate(next.getDate() + parsed.interval * 7);
2536
+ } else if (parsed.unit === "month") {
2537
+ next.setMonth(next.getMonth() + parsed.interval);
2538
+ }
2539
+ return next.toISOString();
2540
+ }
2541
+ if (parsed.type === "specific_days") {
2542
+ const currentDay = base.getDay();
2543
+ const days = parsed.days;
2544
+ let daysToAdd = Infinity;
2545
+ for (const day of days) {
2546
+ let diff = day - currentDay;
2547
+ if (diff <= 0)
2548
+ diff += 7;
2549
+ if (diff < daysToAdd)
2550
+ daysToAdd = diff;
2551
+ }
2552
+ const next = new Date(base);
2553
+ next.setDate(next.getDate() + daysToAdd);
2554
+ return next.toISOString();
2555
+ }
2556
+ throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
2557
+ }
4089
2558
 
4090
2559
  // src/db/templates.ts
4091
2560
  init_database();
@@ -4212,13 +2681,6 @@ function getTaskDependencies(taskId, db) {
4212
2681
 
4213
2682
  // src/db/task-lifecycle.ts
4214
2683
  var MAX_SPAWN_DEPTH = 10;
4215
- function assertStartable(task, agentId) {
4216
- if (task.status === "pending")
4217
- return;
4218
- if (task.status === "in_progress")
4219
- return;
4220
- throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
4221
- }
4222
2684
  function getBlockingDeps(id, db) {
4223
2685
  const d = db || getDatabase();
4224
2686
  const deps = getTaskDependencies(id, d);
@@ -4237,38 +2699,22 @@ function startTask(id, agentId, db) {
4237
2699
  const task = getTask(id, d);
4238
2700
  if (!task)
4239
2701
  throw new TaskNotFoundError(id);
4240
- assertStartable(task, agentId);
4241
2702
  const blocking = getBlockingDeps(id, d);
4242
2703
  if (blocking.length > 0) {
4243
2704
  const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
4244
- emitLocalEventHooksQuiet({
4245
- type: "task.blocked",
4246
- payload: {
4247
- id,
4248
- agent_id: agentId,
4249
- title: task.title,
4250
- blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
4251
- }
4252
- });
4253
2705
  throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
4254
2706
  }
4255
2707
  const cutoff = lockExpiryCutoff();
4256
2708
  const timestamp = now();
4257
2709
  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 = ?
4258
- WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
2710
+ WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
4259
2711
  if (result.changes === 0) {
4260
- const current = getTask(id, d);
4261
- if (!current)
4262
- throw new TaskNotFoundError(id);
4263
- assertStartable(current, agentId);
4264
- if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
4265
- throw new LockError(id, current.locked_by);
2712
+ if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
2713
+ throw new LockError(id, task.locked_by);
4266
2714
  }
4267
- throw new Error(`Task ${id} could not be started because it changed during claim`);
4268
2715
  }
4269
2716
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
4270
2717
  dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
4271
- emitLocalEventHooksQuiet({ type: "task.started", payload: { id, agent_id: agentId, title: task.title } });
4272
2718
  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 };
4273
2719
  }
4274
2720
  function completeTask(id, agentId, db, options) {
@@ -4306,10 +2752,9 @@ function completeTask(id, agentId, db, options) {
4306
2752
  tx();
4307
2753
  logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
4308
2754
  dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
4309
- emitLocalEventHooksQuiet({ type: "task.completed", payload: { id, agent_id: agentId, title: task.title, completed_at: timestamp } });
4310
2755
  let spawnedTask = null;
4311
2756
  if (task.recurrence_rule && !options?.skip_recurrence) {
4312
- spawnedTask = spawnNextRecurrence(task, d, timestamp);
2757
+ spawnedTask = spawnNextRecurrence(task, d);
4313
2758
  }
4314
2759
  let spawnedFromTemplate = null;
4315
2760
  if (task.spawns_template_id) {
@@ -4348,7 +2793,6 @@ function completeTask(id, agentId, db, options) {
4348
2793
  meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
4349
2794
  for (const dep of unblockedDeps) {
4350
2795
  dispatchWebhook("task.unblocked", { id: dep.id, unblocked_by: id, title: dep.title }, d).catch(() => {});
4351
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload: { id: dep.id, unblocked_by: id, title: dep.title } });
4352
2796
  }
4353
2797
  }
4354
2798
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
@@ -4461,7 +2905,6 @@ function failTask(id, agentId, reason, options, db) {
4461
2905
  WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
4462
2906
  logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
4463
2907
  dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
4464
- emitLocalEventHooksQuiet({ type: "task.failed", payload: { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title } });
4465
2908
  const failedTask = {
4466
2909
  ...task,
4467
2910
  status: "failed",
@@ -4506,31 +2949,28 @@ function failTask(id, agentId, reason, options, db) {
4506
2949
  }
4507
2950
  return { task: failedTask, retryTask };
4508
2951
  }
4509
- function getStaleTasks(staleQuery = 30, filters, db) {
2952
+ function getStaleTasks(staleMinutes = 30, filters, db) {
4510
2953
  const d = db || getDatabase();
4511
- const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
4512
- const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
4513
2954
  const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
4514
2955
  const conditions = [
4515
2956
  "status = 'in_progress'",
4516
2957
  "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
4517
2958
  ];
4518
2959
  const params = [cutoff, cutoff];
4519
- if (effectiveFilters?.project_id) {
2960
+ if (filters?.project_id) {
4520
2961
  conditions.push("project_id = ?");
4521
- params.push(effectiveFilters.project_id);
2962
+ params.push(filters.project_id);
4522
2963
  }
4523
- if (effectiveFilters?.task_list_id) {
2964
+ if (filters?.task_list_id) {
4524
2965
  conditions.push("task_list_id = ?");
4525
- params.push(effectiveFilters.task_list_id);
2966
+ params.push(filters.task_list_id);
4526
2967
  }
4527
2968
  const where = conditions.join(" AND ");
4528
2969
  const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
4529
2970
  return rows.map(rowToTask);
4530
2971
  }
4531
- function spawnNextRecurrence(completedTask, db, completedAt) {
4532
- const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
4533
- const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
2972
+ function spawnNextRecurrence(completedTask, db) {
2973
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
4534
2974
  let title = completedTask.title;
4535
2975
  if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
4536
2976
  title = title.slice(completedTask.short_id.length + 2);
@@ -4547,7 +2987,6 @@ function spawnNextRecurrence(completedTask, db, completedAt) {
4547
2987
  tags: completedTask.tags,
4548
2988
  metadata: completedTask.metadata,
4549
2989
  estimated_minutes: completedTask.estimated_minutes ?? undefined,
4550
- sla_minutes: completedTask.sla_minutes ?? undefined,
4551
2990
  recurrence_rule: completedTask.recurrence_rule,
4552
2991
  recurrence_parent_id: recurrenceParentId,
4553
2992
  due_at: dueAt
@@ -4639,130 +3078,6 @@ function getTaskStats(filters, db) {
4639
3078
  }
4640
3079
  // src/db/task-relations.ts
4641
3080
  init_database();
4642
- // src/db/boards.ts
4643
- init_database();
4644
-
4645
- // src/db/plans.ts
4646
- init_database();
4647
- function createPlan(input, db) {
4648
- const d = db || getDatabase();
4649
- const id = uuid();
4650
- const timestamp = now();
4651
- d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at)
4652
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
4653
- id,
4654
- input.project_id || null,
4655
- input.task_list_id || null,
4656
- input.agent_id || null,
4657
- input.name,
4658
- input.description || null,
4659
- input.status || "active",
4660
- timestamp,
4661
- timestamp
4662
- ]);
4663
- return getPlan(id, d);
4664
- }
4665
- function getPlan(id, db) {
4666
- const d = db || getDatabase();
4667
- const row = d.query("SELECT * FROM plans WHERE id = ?").get(id);
4668
- return row;
4669
- }
4670
- function listPlans(projectId, db) {
4671
- const d = db || getDatabase();
4672
- if (projectId) {
4673
- return d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
4674
- }
4675
- return d.query("SELECT * FROM plans ORDER BY created_at DESC").all();
4676
- }
4677
- function updatePlan(id, input, db) {
4678
- const d = db || getDatabase();
4679
- const plan = getPlan(id, d);
4680
- if (!plan)
4681
- throw new PlanNotFoundError(id);
4682
- const sets = ["updated_at = ?"];
4683
- const params = [now()];
4684
- if (input.name !== undefined) {
4685
- sets.push("name = ?");
4686
- params.push(input.name);
4687
- }
4688
- if (input.description !== undefined) {
4689
- sets.push("description = ?");
4690
- params.push(input.description);
4691
- }
4692
- if (input.status !== undefined) {
4693
- sets.push("status = ?");
4694
- params.push(input.status);
4695
- }
4696
- if (input.task_list_id !== undefined) {
4697
- sets.push("task_list_id = ?");
4698
- params.push(input.task_list_id);
4699
- }
4700
- if (input.agent_id !== undefined) {
4701
- sets.push("agent_id = ?");
4702
- params.push(input.agent_id);
4703
- }
4704
- params.push(id);
4705
- d.run(`UPDATE plans SET ${sets.join(", ")} WHERE id = ?`, params);
4706
- const updated = getPlan(id, d);
4707
- emitLocalEventHooksQuiet({
4708
- type: "plan.updated",
4709
- payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id }
4710
- });
4711
- return updated;
4712
- }
4713
- function deletePlan(id, db) {
4714
- const d = db || getDatabase();
4715
- const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
4716
- return result.changes > 0;
4717
- }
4718
- // src/db/calendar.ts
4719
- init_database();
4720
-
4721
- // src/lib/artifact-store.ts
4722
- init_database();
4723
-
4724
- // src/db/comments.ts
4725
- init_database();
4726
- function addComment(input, db) {
4727
- const d = db || getDatabase();
4728
- if (!getTask(input.task_id, d)) {
4729
- throw new TaskNotFoundError(input.task_id);
4730
- }
4731
- const id = uuid();
4732
- const timestamp = now();
4733
- d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, type, progress_pct, created_at)
4734
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
4735
- id,
4736
- input.task_id,
4737
- input.agent_id || null,
4738
- input.session_id || null,
4739
- redactEvidenceText(input.content),
4740
- input.type || "comment",
4741
- input.progress_pct ?? null,
4742
- timestamp
4743
- ]);
4744
- return getComment(id, d);
4745
- }
4746
- function logProgress(taskId, message, pct, agentId, db) {
4747
- return addComment({ task_id: taskId, content: message, type: "progress", progress_pct: pct, agent_id: agentId }, db);
4748
- }
4749
- function getComment(id, db) {
4750
- const d = db || getDatabase();
4751
- return d.query("SELECT * FROM task_comments WHERE id = ?").get(id);
4752
- }
4753
- function listComments(taskId, db) {
4754
- const d = db || getDatabase();
4755
- return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
4756
- }
4757
-
4758
- // src/db/task-runs.ts
4759
- init_database();
4760
-
4761
- // src/db/task-files.ts
4762
- init_database();
4763
-
4764
- // src/db/task-commits.ts
4765
- init_database();
4766
3081
  // src/db/agents.ts
4767
3082
  init_database();
4768
3083
 
@@ -4838,10 +3153,63 @@ var NICE_AGENT_NAMES = [
4838
3153
  "vesper",
4839
3154
  "zephyr"
4840
3155
  ];
3156
+ var EXTENDED_AGENT_NAMES = [
3157
+ "agrippa",
3158
+ "antonius",
3159
+ "aurelian",
3160
+ "aurelius",
3161
+ "camillus",
3162
+ "cassius",
3163
+ "celer",
3164
+ "cincinnatus",
3165
+ "corvus",
3166
+ "drusus",
3167
+ "fabius",
3168
+ "faustus",
3169
+ "flaccus",
3170
+ "gallus",
3171
+ "gaius",
3172
+ "horatius",
3173
+ "lucius",
3174
+ "lucullus",
3175
+ "marius",
3176
+ "marcellus",
3177
+ "maximus",
3178
+ "nerva",
3179
+ "pompey",
3180
+ "quintus",
3181
+ "regulus",
3182
+ "romulus",
3183
+ "scipio",
3184
+ "seneca",
3185
+ "sertorius",
3186
+ "sulla",
3187
+ "tacitus",
3188
+ "varro",
3189
+ "vitruvius",
3190
+ "plato",
3191
+ "socrates",
3192
+ "aristotle",
3193
+ "heraclitus",
3194
+ "democritus",
3195
+ "pythagoras",
3196
+ "hipparchus",
3197
+ "euclid",
3198
+ "archimedes",
3199
+ "zeno",
3200
+ "anaximander",
3201
+ "epictetus",
3202
+ "aeschylus",
3203
+ "sophocles",
3204
+ "euripides",
3205
+ "xenophon",
3206
+ "diogenes"
3207
+ ];
4841
3208
  var PREFERRED_AGENT_NAMES = [
4842
3209
  ...ROMAN_AGENT_NAMES,
4843
3210
  ...GREEK_AGENT_NAMES,
4844
- ...NICE_AGENT_NAMES
3211
+ ...NICE_AGENT_NAMES,
3212
+ ...EXTENDED_AGENT_NAMES
4845
3213
  ];
4846
3214
  var RESERVED_GENERIC_NAMES = new Set([
4847
3215
  "agent",
@@ -4859,12 +3227,48 @@ var RESERVED_GENERIC_NAMES = new Set([
4859
3227
  ]);
4860
3228
  var NUMERIC_SUFFIX_RE = /[-_]\d+$/;
4861
3229
  var ONE_WORD_NAME_RE = /^[a-z]+$/;
3230
+ var ROMAN_NUMERAL_SUFFIXES = new Set([
3231
+ "i",
3232
+ "ii",
3233
+ "iii",
3234
+ "iv",
3235
+ "v",
3236
+ "vi",
3237
+ "vii",
3238
+ "viii",
3239
+ "ix",
3240
+ "x",
3241
+ "xi",
3242
+ "xii",
3243
+ "xiii",
3244
+ "xiv",
3245
+ "xv",
3246
+ "xvi",
3247
+ "xvii",
3248
+ "xviii",
3249
+ "xix",
3250
+ "xx"
3251
+ ]);
3252
+ var PREFERRED_NAMES_BY_LENGTH = [...PREFERRED_AGENT_NAMES].sort((a, b) => b.length - a.length);
4862
3253
  function normalizeAgentNameInput(name) {
4863
3254
  return name.trim().toLowerCase();
4864
3255
  }
4865
3256
  function hasGeneratedNumericSuffix(name) {
4866
3257
  return NUMERIC_SUFFIX_RE.test(normalizeAgentNameInput(name));
4867
3258
  }
3259
+ function generatedAgentNameBase(name) {
3260
+ const normalized = normalizeAgentNameInput(name);
3261
+ for (const base of PREFERRED_NAMES_BY_LENGTH) {
3262
+ if (normalized === base || !normalized.startsWith(base))
3263
+ continue;
3264
+ const suffix = normalized.slice(base.length);
3265
+ const separatedRoman = suffix.match(/^[-_](.+)$/)?.[1];
3266
+ if (ROMAN_NUMERAL_SUFFIXES.has(suffix) || separatedRoman && ROMAN_NUMERAL_SUFFIXES.has(separatedRoman)) {
3267
+ return base;
3268
+ }
3269
+ }
3270
+ return null;
3271
+ }
4868
3272
  function isGenericAgentName(name) {
4869
3273
  const normalized = normalizeAgentNameInput(name);
4870
3274
  if (RESERVED_GENERIC_NAMES.has(normalized))
@@ -4879,29 +3283,93 @@ function isGenericAgentName(name) {
4879
3283
  }
4880
3284
  return false;
4881
3285
  }
4882
- function alphabeticSuffix(index) {
4883
- const letters = "abcdefghijklmnopqrstuvwxyz";
4884
- let value = index;
4885
- let suffix = "";
4886
- do {
4887
- suffix = letters[value % letters.length] + suffix;
4888
- value = Math.floor(value / letters.length) - 1;
4889
- } while (value >= 0);
4890
- return suffix;
3286
+ var FALLBACK_PREFIXES = [
3287
+ "arv",
3288
+ "bel",
3289
+ "cyr",
3290
+ "dax",
3291
+ "elun",
3292
+ "feno",
3293
+ "gavor",
3294
+ "hiro",
3295
+ "ivar",
3296
+ "jaro",
3297
+ "kavo",
3298
+ "lumo",
3299
+ "myr",
3300
+ "navo",
3301
+ "prax",
3302
+ "quor",
3303
+ "riven",
3304
+ "sovan",
3305
+ "tavor",
3306
+ "ulmor",
3307
+ "vexo",
3308
+ "wiro",
3309
+ "yaro",
3310
+ "zel"
3311
+ ];
3312
+ var FALLBACK_STEMS = [
3313
+ "al",
3314
+ "ber",
3315
+ "cor",
3316
+ "dren",
3317
+ "el",
3318
+ "far",
3319
+ "gor",
3320
+ "hal",
3321
+ "ion",
3322
+ "jor",
3323
+ "kel",
3324
+ "lor",
3325
+ "mor",
3326
+ "nel",
3327
+ "or",
3328
+ "per",
3329
+ "quil",
3330
+ "ron",
3331
+ "ser",
3332
+ "tor",
3333
+ "um",
3334
+ "ver",
3335
+ "wyn",
3336
+ "xil"
3337
+ ];
3338
+ var FALLBACK_ENDINGS = [
3339
+ "a",
3340
+ "en",
3341
+ "ia",
3342
+ "is",
3343
+ "on",
3344
+ "or",
3345
+ "um",
3346
+ "us",
3347
+ "yn",
3348
+ "ar",
3349
+ "el",
3350
+ "ir"
3351
+ ];
3352
+ function generatedFallbackAgentName(index) {
3353
+ const perPrefix = FALLBACK_STEMS.length * FALLBACK_ENDINGS.length;
3354
+ const total = FALLBACK_PREFIXES.length * perPrefix;
3355
+ if (index >= total)
3356
+ return null;
3357
+ const prefix = FALLBACK_PREFIXES[Math.floor(index / perPrefix)];
3358
+ const rest = index % perPrefix;
3359
+ const stem = FALLBACK_STEMS[Math.floor(rest / FALLBACK_ENDINGS.length)];
3360
+ const ending = FALLBACK_ENDINGS[rest % FALLBACK_ENDINGS.length];
3361
+ return `${prefix}${stem}${ending}`;
4891
3362
  }
4892
3363
  function suggestAgentNames(existingNames = []) {
4893
3364
  const existing = new Set([...existingNames].map(normalizeAgentNameInput));
4894
3365
  const suggestions = PREFERRED_AGENT_NAMES.filter((name) => !existing.has(name));
4895
- for (let suffixIndex = 0;suggestions.length < 20 && suffixIndex < 1000; suffixIndex++) {
4896
- const suffix = alphabeticSuffix(suffixIndex);
4897
- for (const base of PREFERRED_AGENT_NAMES) {
4898
- const candidate = `${base}${suffix}`;
4899
- if (existing.has(candidate) || suggestions.includes(candidate))
4900
- continue;
4901
- suggestions.push(candidate);
4902
- if (suggestions.length >= 20)
4903
- break;
4904
- }
3366
+ for (let index = 0;suggestions.length < 20; index++) {
3367
+ const candidate = generatedFallbackAgentName(index);
3368
+ if (!candidate)
3369
+ break;
3370
+ if (existing.has(candidate) || suggestions.includes(candidate))
3371
+ continue;
3372
+ suggestions.push(candidate);
4905
3373
  }
4906
3374
  return suggestions;
4907
3375
  }
@@ -4923,11 +3391,21 @@ function validateAgentName(name, existingNames = []) {
4923
3391
  if (hasGeneratedNumericSuffix(normalized)) {
4924
3392
  throw new InvalidAgentNameError(name, "numbered suffix names are not allowed; pick a distinct human-readable name", suggestions);
4925
3393
  }
3394
+ if (generatedAgentNameBase(normalized)) {
3395
+ throw new InvalidAgentNameError(name, "generated suffix variants like caesarii or valeria-ii are reserved; use the base name or a distinct name", suggestions);
3396
+ }
4926
3397
  if (!ONE_WORD_NAME_RE.test(normalized)) {
4927
3398
  throw new InvalidAgentNameError(name, "use one word made of letters only, preferably a Roman or Greek name", suggestions);
4928
3399
  }
4929
3400
  return normalized;
4930
3401
  }
3402
+ var MERGE_DEDUP_REFERENCE_COLUMNS = new Set([
3403
+ "agent_budgets.agent_id",
3404
+ "file_locks.agent_id",
3405
+ "project_agent_roles.agent_id",
3406
+ "resource_locks.agent_id",
3407
+ "task_watchers.agent_id"
3408
+ ]);
4931
3409
 
4932
3410
  // src/db/agents.ts
4933
3411
  function getActiveWindowMs() {
@@ -5164,6 +3642,78 @@ function getOrgChart(db) {
5164
3642
  return buildTree(null);
5165
3643
  }
5166
3644
 
3645
+ // src/db/plans.ts
3646
+ init_database();
3647
+ function createPlan(input, db) {
3648
+ const d = db || getDatabase();
3649
+ const id = uuid();
3650
+ const timestamp = now();
3651
+ d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at)
3652
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3653
+ id,
3654
+ input.project_id || null,
3655
+ input.task_list_id || null,
3656
+ input.agent_id || null,
3657
+ input.name,
3658
+ input.description || null,
3659
+ input.status || "active",
3660
+ timestamp,
3661
+ timestamp
3662
+ ]);
3663
+ return getPlan(id, d);
3664
+ }
3665
+ function getPlan(id, db) {
3666
+ const d = db || getDatabase();
3667
+ const row = d.query("SELECT * FROM plans WHERE id = ?").get(id);
3668
+ return row;
3669
+ }
3670
+ function listPlans(projectId, db) {
3671
+ const d = db || getDatabase();
3672
+ if (projectId) {
3673
+ return d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
3674
+ }
3675
+ return d.query("SELECT * FROM plans ORDER BY created_at DESC").all();
3676
+ }
3677
+ function updatePlan(id, input, db) {
3678
+ const d = db || getDatabase();
3679
+ const plan = getPlan(id, d);
3680
+ if (!plan)
3681
+ throw new PlanNotFoundError(id);
3682
+ const sets = ["updated_at = ?"];
3683
+ const params = [now()];
3684
+ if (input.name !== undefined) {
3685
+ sets.push("name = ?");
3686
+ params.push(input.name);
3687
+ }
3688
+ if (input.description !== undefined) {
3689
+ sets.push("description = ?");
3690
+ params.push(input.description);
3691
+ }
3692
+ if (input.status !== undefined) {
3693
+ sets.push("status = ?");
3694
+ params.push(input.status);
3695
+ }
3696
+ if (input.task_list_id !== undefined) {
3697
+ sets.push("task_list_id = ?");
3698
+ params.push(input.task_list_id);
3699
+ }
3700
+ if (input.agent_id !== undefined) {
3701
+ sets.push("agent_id = ?");
3702
+ params.push(input.agent_id);
3703
+ }
3704
+ params.push(id);
3705
+ d.run(`UPDATE plans SET ${sets.join(", ")} WHERE id = ?`, params);
3706
+ return getPlan(id, d);
3707
+ }
3708
+ function deletePlan(id, db) {
3709
+ const d = db || getDatabase();
3710
+ const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
3711
+ return result.changes > 0;
3712
+ }
3713
+
3714
+ // src/server/routes.ts
3715
+ init_database();
3716
+
5167
3717
  // src/db/orgs.ts
5168
3718
  init_database();
5169
3719
  function rowToOrg(row) {
@@ -5213,8 +3763,42 @@ function deleteOrg(id, db) {
5213
3763
  return d.run("DELETE FROM orgs WHERE id = ?", [id]).changes > 0;
5214
3764
  }
5215
3765
 
3766
+ // src/db/comments.ts
3767
+ init_database();
3768
+ function addComment(input, db) {
3769
+ const d = db || getDatabase();
3770
+ if (!getTask(input.task_id, d)) {
3771
+ throw new TaskNotFoundError(input.task_id);
3772
+ }
3773
+ const id = uuid();
3774
+ const timestamp = now();
3775
+ d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, type, progress_pct, created_at)
3776
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
3777
+ id,
3778
+ input.task_id,
3779
+ input.agent_id || null,
3780
+ input.session_id || null,
3781
+ input.content,
3782
+ input.type || "comment",
3783
+ input.progress_pct ?? null,
3784
+ timestamp
3785
+ ]);
3786
+ return getComment(id, d);
3787
+ }
3788
+ function logProgress(taskId, message, pct, agentId, db) {
3789
+ return addComment({ task_id: taskId, content: message, type: "progress", progress_pct: pct, agent_id: agentId }, db);
3790
+ }
3791
+ function getComment(id, db) {
3792
+ const d = db || getDatabase();
3793
+ return d.query("SELECT * FROM task_comments WHERE id = ?").get(id);
3794
+ }
3795
+ function listComments(taskId, db) {
3796
+ const d = db || getDatabase();
3797
+ return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
3798
+ }
3799
+
5216
3800
  // src/server/routes.ts
5217
- import { join as join5, resolve as resolve5, sep } from "path";
3801
+ import { join as join4, resolve as resolve2, sep } from "path";
5218
3802
  function parseFieldsParam(url) {
5219
3803
  const fieldsParam = url.searchParams.get("fields");
5220
3804
  return fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
@@ -5785,8 +4369,16 @@ async function handleBulkDeleteProjects(req, _ctx, json2) {
5785
4369
  }
5786
4370
  }
5787
4371
  function handleDoctor(_ctx, json2) {
5788
- const { runTodosDoctor: runTodosDoctor2 } = (init_doctor(), __toCommonJS(exports_doctor));
5789
- return json2(runTodosDoctor2({ apply: false }));
4372
+ const issues = [];
4373
+ const staleItems = getStaleTasks(30);
4374
+ if (staleItems.length > 0)
4375
+ issues.push({ severity: "warn", type: "stale_tasks", message: `${staleItems.length} tasks stuck in_progress >30min`, count: staleItems.length });
4376
+ const withParent = getDatabase().query("SELECT COUNT(*) as c FROM tasks t WHERE t.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = t.parent_id)").get();
4377
+ if (withParent.c > 0)
4378
+ issues.push({ severity: "error", type: "orphaned_parents", message: `${withParent.c} tasks reference non-existent parent IDs`, count: withParent.c });
4379
+ if (issues.length === 0)
4380
+ issues.push({ severity: "info", type: "healthy", message: "No issues found" });
4381
+ return json2({ ok: !issues.some((i) => i.severity === "error"), issues });
5790
4382
  }
5791
4383
  function handleReport(_req, url, _ctx, json2) {
5792
4384
  const days = parseInt(url.searchParams.get("days") || "7", 10);
@@ -5914,9 +4506,9 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
5914
4506
  if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
5915
4507
  return null;
5916
4508
  if (path !== "/") {
5917
- const filePath = join5(ctx.dashboardDir, path);
5918
- const resolvedFile = resolve5(filePath);
5919
- const resolvedBase = resolve5(ctx.dashboardDir);
4509
+ const filePath = join4(ctx.dashboardDir, path);
4510
+ const resolvedFile = resolve2(filePath);
4511
+ const resolvedBase = resolve2(ctx.dashboardDir);
5920
4512
  if (!resolvedFile.startsWith(resolvedBase + sep) && resolvedFile !== resolvedBase) {
5921
4513
  return json2({ error: "Forbidden" }, 403);
5922
4514
  }
@@ -5924,7 +4516,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
5924
4516
  if (res2)
5925
4517
  return res2;
5926
4518
  }
5927
- const indexPath = join5(ctx.dashboardDir, "index.html");
4519
+ const indexPath = join4(ctx.dashboardDir, "index.html");
5928
4520
  const res = serveStaticFile2(indexPath);
5929
4521
  if (res)
5930
4522
  return res;
@@ -5935,21 +4527,21 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
5935
4527
  function resolveDashboardDir() {
5936
4528
  const candidates = [];
5937
4529
  try {
5938
- const scriptDir = dirname6(fileURLToPath2(import.meta.url));
5939
- candidates.push(join6(scriptDir, "..", "dashboard", "dist"));
5940
- candidates.push(join6(scriptDir, "..", "..", "dashboard", "dist"));
4530
+ const scriptDir = dirname3(fileURLToPath2(import.meta.url));
4531
+ candidates.push(join5(scriptDir, "..", "dashboard", "dist"));
4532
+ candidates.push(join5(scriptDir, "..", "..", "dashboard", "dist"));
5941
4533
  } catch {}
5942
4534
  if (process.argv[1]) {
5943
- const mainDir = dirname6(process.argv[1]);
5944
- candidates.push(join6(mainDir, "..", "dashboard", "dist"));
5945
- candidates.push(join6(mainDir, "..", "..", "dashboard", "dist"));
4535
+ const mainDir = dirname3(process.argv[1]);
4536
+ candidates.push(join5(mainDir, "..", "dashboard", "dist"));
4537
+ candidates.push(join5(mainDir, "..", "..", "dashboard", "dist"));
5946
4538
  }
5947
- candidates.push(join6(process.cwd(), "dashboard", "dist"));
4539
+ candidates.push(join5(process.cwd(), "dashboard", "dist"));
5948
4540
  for (const candidate of candidates) {
5949
- if (existsSync6(candidate))
4541
+ if (existsSync5(candidate))
5950
4542
  return candidate;
5951
4543
  }
5952
- return join6(process.cwd(), "dashboard", "dist");
4544
+ return join5(process.cwd(), "dashboard", "dist");
5953
4545
  }
5954
4546
  var MIME_TYPES = {
5955
4547
  ".html": "text/html; charset=utf-8",
@@ -6021,7 +4613,7 @@ function json(data, status = 200, headers) {
6021
4613
  });
6022
4614
  }
6023
4615
  function serveStaticFile(filePath) {
6024
- if (!existsSync6(filePath))
4616
+ if (!existsSync5(filePath))
6025
4617
  return null;
6026
4618
  const ext = extname(filePath);
6027
4619
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -6100,7 +4692,7 @@ data: ${data}
6100
4692
  filteredSseClients.delete(client);
6101
4693
  }
6102
4694
  const dashboardDir = resolveDashboardDir();
6103
- const dashboardExists = existsSync6(dashboardDir);
4695
+ const dashboardExists = existsSync5(dashboardDir);
6104
4696
  if (!dashboardExists) {
6105
4697
  console.error(`
6106
4698
  Dashboard not found at: ${dashboardDir}`);