@hasna/todos 0.11.46 → 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 (258) hide show
  1. package/README.md +11 -1219
  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/components/Dashboard.d.ts.map +1 -1
  16. package/dist/cli/index.js +28793 -46092
  17. package/dist/db/agent-metrics.d.ts +0 -101
  18. package/dist/db/agent-metrics.d.ts.map +1 -1
  19. package/dist/db/agent-names.d.ts +3 -0
  20. package/dist/db/agent-names.d.ts.map +1 -1
  21. package/dist/db/builtin-templates.d.ts +0 -17
  22. package/dist/db/builtin-templates.d.ts.map +1 -1
  23. package/dist/db/comments.d.ts.map +1 -1
  24. package/dist/db/database.d.ts +1 -2
  25. package/dist/db/database.d.ts.map +1 -1
  26. package/dist/db/handoffs.d.ts +1 -52
  27. package/dist/db/handoffs.d.ts.map +1 -1
  28. package/dist/db/machines.d.ts +6 -19
  29. package/dist/db/machines.d.ts.map +1 -1
  30. package/dist/db/migrations.d.ts.map +1 -1
  31. package/dist/db/pg-migrate.d.ts +14 -0
  32. package/dist/db/pg-migrate.d.ts.map +1 -0
  33. package/dist/db/pg-migrations.d.ts +8 -0
  34. package/dist/db/pg-migrations.d.ts.map +1 -0
  35. package/dist/db/plans.d.ts.map +1 -1
  36. package/dist/db/schema.d.ts.map +1 -1
  37. package/dist/db/task-commits.d.ts +0 -51
  38. package/dist/db/task-commits.d.ts.map +1 -1
  39. package/dist/db/task-crud.d.ts.map +1 -1
  40. package/dist/db/task-lifecycle.d.ts +1 -16
  41. package/dist/db/task-lifecycle.d.ts.map +1 -1
  42. package/dist/db/task-relations.d.ts +9 -69
  43. package/dist/db/task-relations.d.ts.map +1 -1
  44. package/dist/db/tasks.d.ts +4 -8
  45. package/dist/db/tasks.d.ts.map +1 -1
  46. package/dist/index.d.ts +14 -133
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +14567 -25774
  49. package/dist/lib/auto-assign.d.ts +5 -3
  50. package/dist/lib/auto-assign.d.ts.map +1 -1
  51. package/dist/lib/config.d.ts +0 -355
  52. package/dist/lib/config.d.ts.map +1 -1
  53. package/dist/lib/extract.d.ts +0 -57
  54. package/dist/lib/extract.d.ts.map +1 -1
  55. package/dist/lib/logger.d.ts +12 -6
  56. package/dist/lib/logger.d.ts.map +1 -1
  57. package/dist/mcp/index.d.ts.map +1 -1
  58. package/dist/mcp/index.js +22566 -30764
  59. package/dist/mcp/token-utils.d.ts +2 -2
  60. package/dist/mcp/token-utils.d.ts.map +1 -1
  61. package/dist/mcp/tools/cloud.d.ts +12 -0
  62. package/dist/mcp/tools/cloud.d.ts.map +1 -0
  63. package/dist/mcp/tools/code-tools.d.ts.map +1 -1
  64. package/dist/mcp/tools/machines.d.ts.map +1 -1
  65. package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
  66. package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
  67. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  68. package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
  69. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  70. package/dist/mcp/tools/task-rel-tools.d.ts.map +1 -1
  71. package/dist/mcp/tools/task-resources.d.ts.map +1 -1
  72. package/dist/mcp/tools/task-workflow-tools.d.ts.map +1 -1
  73. package/dist/mcp/tools/templates.d.ts.map +1 -1
  74. package/dist/sdk/client.d.ts +1 -1
  75. package/dist/sdk/client.d.ts.map +1 -1
  76. package/dist/sdk/types.d.ts +1 -26
  77. package/dist/sdk/types.d.ts.map +1 -1
  78. package/dist/server/index.js +334 -1874
  79. package/dist/server/routes.d.ts.map +1 -1
  80. package/dist/types/index.d.ts +1 -216
  81. package/dist/types/index.d.ts.map +1 -1
  82. package/package.json +6 -30
  83. package/dashboard/dist/assets/index-DJm6m6Yy.css +0 -1
  84. package/dist/capabilities.d.ts +0 -32
  85. package/dist/capabilities.d.ts.map +0 -1
  86. package/dist/cli/commands/agent-reliability-commands.d.ts +0 -3
  87. package/dist/cli/commands/agent-reliability-commands.d.ts.map +0 -1
  88. package/dist/cli/commands/audit-ledger-commands.d.ts +0 -3
  89. package/dist/cli/commands/audit-ledger-commands.d.ts.map +0 -1
  90. package/dist/cli/commands/capacity-commands.d.ts +0 -3
  91. package/dist/cli/commands/capacity-commands.d.ts.map +0 -1
  92. package/dist/cli/commands/environment-snapshots.d.ts +0 -3
  93. package/dist/cli/commands/environment-snapshots.d.ts.map +0 -1
  94. package/dist/cli/commands/help-commands.d.ts +0 -3
  95. package/dist/cli/commands/help-commands.d.ts.map +0 -1
  96. package/dist/cli/commands/knowledge-commands.d.ts +0 -3
  97. package/dist/cli/commands/knowledge-commands.d.ts.map +0 -1
  98. package/dist/cli/commands/local-backup-commands.d.ts +0 -3
  99. package/dist/cli/commands/local-backup-commands.d.ts.map +0 -1
  100. package/dist/cli/commands/local-snapshot-commands.d.ts +0 -3
  101. package/dist/cli/commands/local-snapshot-commands.d.ts.map +0 -1
  102. package/dist/cli/commands/onboarding-commands.d.ts +0 -3
  103. package/dist/cli/commands/onboarding-commands.d.ts.map +0 -1
  104. package/dist/cli/commands/release-compatibility-commands.d.ts +0 -3
  105. package/dist/cli/commands/release-compatibility-commands.d.ts.map +0 -1
  106. package/dist/cli/commands/retrospective-commands.d.ts +0 -3
  107. package/dist/cli/commands/retrospective-commands.d.ts.map +0 -1
  108. package/dist/cli/commands/review-queue-commands.d.ts +0 -3
  109. package/dist/cli/commands/review-queue-commands.d.ts.map +0 -1
  110. package/dist/cli/commands/risk-commands.d.ts +0 -3
  111. package/dist/cli/commands/risk-commands.d.ts.map +0 -1
  112. package/dist/cli/commands/roadmap-commands.d.ts +0 -3
  113. package/dist/cli/commands/roadmap-commands.d.ts.map +0 -1
  114. package/dist/cli/commands/scale-hardening-commands.d.ts +0 -3
  115. package/dist/cli/commands/scale-hardening-commands.d.ts.map +0 -1
  116. package/dist/cli/commands/sdk-fixture-commands.d.ts +0 -3
  117. package/dist/cli/commands/sdk-fixture-commands.d.ts.map +0 -1
  118. package/dist/cli/commands/usage-ledger-commands.d.ts +0 -3
  119. package/dist/cli/commands/usage-ledger-commands.d.ts.map +0 -1
  120. package/dist/cli-mcp-parity.d.ts +0 -41
  121. package/dist/cli-mcp-parity.d.ts.map +0 -1
  122. package/dist/contracts.d.ts +0 -87
  123. package/dist/contracts.d.ts.map +0 -1
  124. package/dist/contracts.js +0 -18172
  125. package/dist/db/boards.d.ts +0 -56
  126. package/dist/db/boards.d.ts.map +0 -1
  127. package/dist/db/calendar.d.ts +0 -52
  128. package/dist/db/calendar.d.ts.map +0 -1
  129. package/dist/db/inbox.d.ts +0 -47
  130. package/dist/db/inbox.d.ts.map +0 -1
  131. package/dist/db/project-knowledge.d.ts +0 -88
  132. package/dist/db/project-knowledge.d.ts.map +0 -1
  133. package/dist/db/project-risks.d.ts +0 -139
  134. package/dist/db/project-risks.d.ts.map +0 -1
  135. package/dist/db/retrospectives.d.ts +0 -98
  136. package/dist/db/retrospectives.d.ts.map +0 -1
  137. package/dist/db/task-runs.d.ts +0 -133
  138. package/dist/db/task-runs.d.ts.map +0 -1
  139. package/dist/json-contracts.d.ts +0 -56
  140. package/dist/json-contracts.d.ts.map +0 -1
  141. package/dist/lib/activity-timeline.d.ts +0 -43
  142. package/dist/lib/activity-timeline.d.ts.map +0 -1
  143. package/dist/lib/agent-replay-simulator.d.ts +0 -66
  144. package/dist/lib/agent-replay-simulator.d.ts.map +0 -1
  145. package/dist/lib/agent-run-dispatcher.d.ts +0 -62
  146. package/dist/lib/agent-run-dispatcher.d.ts.map +0 -1
  147. package/dist/lib/approval-gates.d.ts +0 -52
  148. package/dist/lib/approval-gates.d.ts.map +0 -1
  149. package/dist/lib/artifact-store.d.ts +0 -68
  150. package/dist/lib/artifact-store.d.ts.map +0 -1
  151. package/dist/lib/audit-ledger.d.ts +0 -59
  152. package/dist/lib/audit-ledger.d.ts.map +0 -1
  153. package/dist/lib/branch-work-plans.d.ts +0 -46
  154. package/dist/lib/branch-work-plans.d.ts.map +0 -1
  155. package/dist/lib/capacity-forecasts.d.ts +0 -70
  156. package/dist/lib/capacity-forecasts.d.ts.map +0 -1
  157. package/dist/lib/cli-help.d.ts +0 -38
  158. package/dist/lib/cli-help.d.ts.map +0 -1
  159. package/dist/lib/context-packs.d.ts +0 -163
  160. package/dist/lib/context-packs.d.ts.map +0 -1
  161. package/dist/lib/doctor.d.ts +0 -46
  162. package/dist/lib/doctor.d.ts.map +0 -1
  163. package/dist/lib/environment-snapshots.d.ts +0 -111
  164. package/dist/lib/environment-snapshots.d.ts.map +0 -1
  165. package/dist/lib/event-hooks.d.ts +0 -58
  166. package/dist/lib/event-hooks.d.ts.map +0 -1
  167. package/dist/lib/external-issue-importers.d.ts +0 -60
  168. package/dist/lib/external-issue-importers.d.ts.map +0 -1
  169. package/dist/lib/local-backups.d.ts +0 -129
  170. package/dist/lib/local-backups.d.ts.map +0 -1
  171. package/dist/lib/local-bridge.d.ts +0 -81
  172. package/dist/lib/local-bridge.d.ts.map +0 -1
  173. package/dist/lib/local-encryption.d.ts +0 -94
  174. package/dist/lib/local-encryption.d.ts.map +0 -1
  175. package/dist/lib/local-extensions.d.ts +0 -92
  176. package/dist/lib/local-extensions.d.ts.map +0 -1
  177. package/dist/lib/local-fields.d.ts +0 -33
  178. package/dist/lib/local-fields.d.ts.map +0 -1
  179. package/dist/lib/local-notifications.d.ts +0 -55
  180. package/dist/lib/local-notifications.d.ts.map +0 -1
  181. package/dist/lib/local-reports.d.ts +0 -149
  182. package/dist/lib/local-reports.d.ts.map +0 -1
  183. package/dist/lib/local-snapshots.d.ts +0 -66
  184. package/dist/lib/local-snapshots.d.ts.map +0 -1
  185. package/dist/lib/mention-resolver.d.ts +0 -43
  186. package/dist/lib/mention-resolver.d.ts.map +0 -1
  187. package/dist/lib/natural-language-intake.d.ts +0 -56
  188. package/dist/lib/natural-language-intake.d.ts.map +0 -1
  189. package/dist/lib/onboarding-fixtures.d.ts +0 -31
  190. package/dist/lib/onboarding-fixtures.d.ts.map +0 -1
  191. package/dist/lib/policy-packs.d.ts +0 -87
  192. package/dist/lib/policy-packs.d.ts.map +0 -1
  193. package/dist/lib/project-bootstrap.d.ts +0 -35
  194. package/dist/lib/project-bootstrap.d.ts.map +0 -1
  195. package/dist/lib/public-release-gate.d.ts +0 -57
  196. package/dist/lib/public-release-gate.d.ts.map +0 -1
  197. package/dist/lib/redaction.d.ts +0 -12
  198. package/dist/lib/redaction.d.ts.map +0 -1
  199. package/dist/lib/release-compatibility.d.ts +0 -59
  200. package/dist/lib/release-compatibility.d.ts.map +0 -1
  201. package/dist/lib/release-notes.d.ts +0 -81
  202. package/dist/lib/release-notes.d.ts.map +0 -1
  203. package/dist/lib/retention-cleanup.d.ts +0 -63
  204. package/dist/lib/retention-cleanup.d.ts.map +0 -1
  205. package/dist/lib/review-queues.d.ts +0 -98
  206. package/dist/lib/review-queues.d.ts.map +0 -1
  207. package/dist/lib/roadmaps.d.ts +0 -133
  208. package/dist/lib/roadmaps.d.ts.map +0 -1
  209. package/dist/lib/runner-sandbox.d.ts +0 -50
  210. package/dist/lib/runner-sandbox.d.ts.map +0 -1
  211. package/dist/lib/saved-search-views.d.ts +0 -60
  212. package/dist/lib/saved-search-views.d.ts.map +0 -1
  213. package/dist/lib/scale-hardening.d.ts +0 -74
  214. package/dist/lib/scale-hardening.d.ts.map +0 -1
  215. package/dist/lib/sdk-integration-fixtures.d.ts +0 -65
  216. package/dist/lib/sdk-integration-fixtures.d.ts.map +0 -1
  217. package/dist/lib/task-contracts.d.ts +0 -75
  218. package/dist/lib/task-contracts.d.ts.map +0 -1
  219. package/dist/lib/task-dedupe.d.ts +0 -45
  220. package/dist/lib/task-dedupe.d.ts.map +0 -1
  221. package/dist/lib/terminal-notifications.d.ts +0 -53
  222. package/dist/lib/terminal-notifications.d.ts.map +0 -1
  223. package/dist/lib/todos-md.d.ts +0 -21
  224. package/dist/lib/todos-md.d.ts.map +0 -1
  225. package/dist/lib/tui-dashboard.d.ts +0 -49
  226. package/dist/lib/tui-dashboard.d.ts.map +0 -1
  227. package/dist/lib/usage-ledger.d.ts +0 -82
  228. package/dist/lib/usage-ledger.d.ts.map +0 -1
  229. package/dist/lib/verification-providers.d.ts +0 -54
  230. package/dist/lib/verification-providers.d.ts.map +0 -1
  231. package/dist/lib/workflow-prompts.d.ts +0 -38
  232. package/dist/lib/workflow-prompts.d.ts.map +0 -1
  233. package/dist/lib/workflow-states.d.ts +0 -70
  234. package/dist/lib/workflow-states.d.ts.map +0 -1
  235. package/dist/lib/workspace-trust.d.ts +0 -38
  236. package/dist/lib/workspace-trust.d.ts.map +0 -1
  237. package/dist/mcp/tools/environment-snapshots.d.ts +0 -8
  238. package/dist/mcp/tools/environment-snapshots.d.ts.map +0 -1
  239. package/dist/mcp/tools/workflow-prompts.d.ts +0 -3
  240. package/dist/mcp/tools/workflow-prompts.d.ts.map +0 -1
  241. package/dist/mcp.d.ts +0 -42
  242. package/dist/mcp.d.ts.map +0 -1
  243. package/dist/mcp.js +0 -554
  244. package/dist/registry.d.ts +0 -35
  245. package/dist/registry.d.ts.map +0 -1
  246. package/dist/registry.js +0 -18345
  247. package/dist/sdk/index.js +0 -635
  248. package/dist/storage/index.d.ts +0 -4
  249. package/dist/storage/index.d.ts.map +0 -1
  250. package/dist/storage/interfaces.d.ts +0 -185
  251. package/dist/storage/interfaces.d.ts.map +0 -1
  252. package/dist/storage/local-sqlite.d.ts +0 -7
  253. package/dist/storage/local-sqlite.d.ts.map +0 -1
  254. package/dist/storage.d.ts +0 -4
  255. package/dist/storage.d.ts.map +0 -1
  256. package/dist/storage.js +0 -8380
  257. package/dist/test/no-network.d.ts +0 -7
  258. 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");
@@ -1711,10 +1169,6 @@ function ensureSchema(db) {
1711
1169
  ensureColumn("dispatches", "machine_id", "TEXT");
1712
1170
  ensureColumn("dispatches", "synced_at", "TEXT");
1713
1171
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
1714
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id)");
1715
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)");
1716
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_archived_at ON tasks(archived_at)");
1717
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_updated_at ON tasks(updated_at)");
1718
1172
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id)");
1719
1173
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at)");
1720
1174
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL");
@@ -1750,8 +1204,6 @@ function ensureSchema(db) {
1750
1204
  CREATE TABLE task_time_logs (
1751
1205
  id TEXT PRIMARY KEY,
1752
1206
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1753
- run_id TEXT,
1754
- focus_session_id TEXT,
1755
1207
  agent_id TEXT,
1756
1208
  started_at TEXT,
1757
1209
  ended_at TEXT,
@@ -1759,77 +1211,9 @@ function ensureSchema(db) {
1759
1211
  notes TEXT,
1760
1212
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
1761
1213
  )`);
1762
- ensureColumn("task_time_logs", "run_id", "TEXT");
1763
- ensureColumn("task_time_logs", "focus_session_id", "TEXT");
1764
1214
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_task ON task_time_logs(task_id)");
1765
1215
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_agent ON task_time_logs(agent_id)");
1766
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_run ON task_time_logs(run_id)");
1767
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_focus_session ON task_time_logs(focus_session_id)");
1768
1216
  ensureColumn("tasks", "actual_minutes", "INTEGER");
1769
- ensureTable("focus_sessions", `
1770
- CREATE TABLE focus_sessions (
1771
- id TEXT PRIMARY KEY,
1772
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1773
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1774
- run_id TEXT,
1775
- agent_id TEXT,
1776
- title TEXT,
1777
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'paused', 'completed', 'cancelled')),
1778
- started_at TEXT NOT NULL,
1779
- last_resumed_at TEXT,
1780
- paused_at TEXT,
1781
- ended_at TEXT,
1782
- actual_minutes INTEGER NOT NULL DEFAULT 0,
1783
- idle_after_minutes INTEGER,
1784
- notes TEXT,
1785
- metadata TEXT DEFAULT '{}',
1786
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1787
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1788
- )`);
1789
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_task ON focus_sessions(task_id)");
1790
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_plan ON focus_sessions(plan_id)");
1791
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_run ON focus_sessions(run_id)");
1792
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_agent ON focus_sessions(agent_id)");
1793
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_status ON focus_sessions(status)");
1794
- ensureTable("task_boards", `
1795
- CREATE TABLE task_boards (
1796
- id TEXT PRIMARY KEY,
1797
- name TEXT NOT NULL UNIQUE,
1798
- scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('tasks', 'plans')),
1799
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1800
- task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL,
1801
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1802
- agent_id TEXT,
1803
- lanes TEXT NOT NULL DEFAULT '[]',
1804
- filters TEXT NOT NULL DEFAULT '{}',
1805
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1806
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1807
- )`);
1808
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_boards_scope ON task_boards(scope)");
1809
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_boards_project ON task_boards(project_id)");
1810
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_boards_plan ON task_boards(plan_id)");
1811
- ensureTable("local_calendar_items", `
1812
- CREATE TABLE local_calendar_items (
1813
- id TEXT PRIMARY KEY,
1814
- kind TEXT NOT NULL CHECK(kind IN ('task_due', 'task_sla', 'task_reminder', 'milestone', 'work_block', 'run', 'imported')),
1815
- title TEXT NOT NULL,
1816
- description TEXT,
1817
- starts_at TEXT NOT NULL,
1818
- ends_at TEXT,
1819
- timezone TEXT,
1820
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1821
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1822
- plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1823
- run_id TEXT,
1824
- recurrence_rule TEXT,
1825
- metadata TEXT DEFAULT '{}',
1826
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1827
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1828
- )`);
1829
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_time ON local_calendar_items(starts_at, ends_at)");
1830
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_task ON local_calendar_items(task_id)");
1831
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_project ON local_calendar_items(project_id)");
1832
- ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_kind ON local_calendar_items(kind)");
1833
1217
  ensureTable("task_watchers", `
1834
1218
  CREATE TABLE task_watchers (
1835
1219
  id TEXT PRIMARY KEY,
@@ -1909,22 +1293,12 @@ var init_schema = __esm(() => {
1909
1293
  });
1910
1294
 
1911
1295
  // src/db/machines.ts
1912
- import { hostname as osHostname, platform as osPlatform, arch as osArch } from "os";
1913
- function parseMetadata(value) {
1914
- if (!value)
1915
- return {};
1916
- try {
1917
- const parsed = JSON.parse(value);
1918
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1919
- } catch {
1920
- return {};
1921
- }
1922
- }
1296
+ import { hostname as osHostname, platform as osPlatform } from "os";
1923
1297
  function rowToMachine(row) {
1924
1298
  return {
1925
1299
  ...row,
1926
1300
  is_primary: !!row.is_primary,
1927
- metadata: parseMetadata(row.metadata)
1301
+ metadata: row.metadata ? JSON.parse(row.metadata) : {}
1928
1302
  };
1929
1303
  }
1930
1304
  function getOrCreateLocalMachine(db) {
@@ -1989,7 +1363,6 @@ __export(exports_database, {
1989
1363
  now: () => now,
1990
1364
  lockExpiryCutoff: () => lockExpiryCutoff,
1991
1365
  isLockExpired: () => isLockExpired,
1992
- getDatabasePath: () => getDatabasePath,
1993
1366
  getDatabase: () => getDatabase,
1994
1367
  closeDatabase: () => closeDatabase,
1995
1368
  clearExpiredLocks: () => clearExpiredLocks,
@@ -2051,9 +1424,6 @@ function getDbPath() {
2051
1424
  }
2052
1425
  return newPath;
2053
1426
  }
2054
- function getDatabasePath() {
2055
- return getDbPath();
2056
- }
2057
1427
  function ensureDir(filePath) {
2058
1428
  if (isInMemoryDb(filePath))
2059
1429
  return;
@@ -2079,563 +1449,71 @@ function getDatabase(dbPath) {
2079
1449
  function closeDatabase() {
2080
1450
  if (_db) {
2081
1451
  _db.close();
2082
- _db = null;
2083
- }
2084
- }
2085
- function resetDatabase() {
2086
- _db = null;
2087
- }
2088
- function now() {
2089
- return new Date().toISOString();
2090
- }
2091
- function uuid() {
2092
- return crypto.randomUUID();
2093
- }
2094
- function isLockExpired(lockedAt, nowMs = Date.now()) {
2095
- if (!lockedAt)
2096
- return true;
2097
- const lockTime = new Date(lockedAt).getTime();
2098
- const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
2099
- return nowMs - lockTime > expiryMs;
2100
- }
2101
- function lockExpiryCutoff(nowMs = Date.now()) {
2102
- const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
2103
- return new Date(nowMs - expiryMs).toISOString();
2104
- }
2105
- function clearExpiredLocks(db) {
2106
- const cutoff = lockExpiryCutoff();
2107
- db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
2108
- }
2109
- function resolvePartialId(db, table, partialId) {
2110
- if (!ALLOWED_TABLES.has(table)) {
2111
- throw new Error(`Invalid table name: ${table}`);
2112
- }
2113
- if (partialId.length >= 36) {
2114
- const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
2115
- return row?.id ?? null;
2116
- }
2117
- const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
2118
- if (rows.length === 1) {
2119
- return rows[0].id;
2120
- }
2121
- if (rows.length > 1) {
2122
- return null;
2123
- }
2124
- if (table === "tasks") {
2125
- const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
2126
- if (shortIdRows.length === 1) {
2127
- return shortIdRows[0].id;
2128
- }
2129
- }
2130
- if (table === "task_lists") {
2131
- const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
2132
- if (slugRow)
2133
- return slugRow.id;
2134
- }
2135
- if (table === "projects") {
2136
- const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
2137
- if (nameRow)
2138
- return nameRow.id;
2139
- }
2140
- return null;
2141
- }
2142
- var LOCK_EXPIRY_MINUTES = 30, _db = null, ALLOWED_TABLES;
2143
- var init_database = __esm(() => {
2144
- init_schema();
2145
- init_machines();
2146
- ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
2147
- });
2148
-
2149
- // src/lib/recurrence.ts
2150
- function parseRecurrenceRule(rule) {
2151
- const normalized = rule.trim().toLowerCase();
2152
- if (normalized === "every weekday" || normalized === "every weekdays") {
2153
- return { type: "specific_days", days: [1, 2, 3, 4, 5] };
2154
- }
2155
- if (normalized === "every day" || normalized === "daily") {
2156
- return { type: "interval", interval: 1, unit: "day" };
2157
- }
2158
- if (normalized === "every week" || normalized === "weekly") {
2159
- return { type: "interval", interval: 1, unit: "week" };
2160
- }
2161
- if (normalized === "every month" || normalized === "monthly") {
2162
- return { type: "interval", interval: 1, unit: "month" };
2163
- }
2164
- const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
2165
- if (intervalMatch) {
2166
- return {
2167
- type: "interval",
2168
- interval: parseInt(intervalMatch[1], 10),
2169
- unit: intervalMatch[2]
2170
- };
2171
- }
2172
- const daysMatch = normalized.match(/^every\s+(.+)$/);
2173
- if (daysMatch) {
2174
- const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
2175
- const days = [];
2176
- for (const part of dayParts) {
2177
- const dayNum = DAY_NAMES[part];
2178
- if (dayNum !== undefined) {
2179
- days.push(dayNum);
2180
- }
2181
- }
2182
- if (days.length > 0) {
2183
- return { type: "specific_days", days: days.sort((a, b) => a - b) };
2184
- }
2185
- }
2186
- 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"`);
2187
- }
2188
- function isValidRecurrenceRule(rule) {
2189
- try {
2190
- parseRecurrenceRule(rule);
2191
- return true;
2192
- } catch {
2193
- return false;
2194
- }
2195
- }
2196
- function nextOccurrence(rule, from) {
2197
- const parsed = parseRecurrenceRule(rule);
2198
- const base = from || new Date;
2199
- if (parsed.type === "interval") {
2200
- const next = new Date(base);
2201
- if (parsed.unit === "day") {
2202
- next.setDate(next.getDate() + parsed.interval);
2203
- } else if (parsed.unit === "week") {
2204
- next.setDate(next.getDate() + parsed.interval * 7);
2205
- } else if (parsed.unit === "month") {
2206
- next.setMonth(next.getMonth() + parsed.interval);
2207
- }
2208
- return next.toISOString();
2209
- }
2210
- if (parsed.type === "specific_days") {
2211
- const currentDay = base.getDay();
2212
- const days = parsed.days;
2213
- let daysToAdd = Infinity;
2214
- for (const day of days) {
2215
- let diff = day - currentDay;
2216
- if (diff <= 0)
2217
- diff += 7;
2218
- if (diff < daysToAdd)
2219
- daysToAdd = diff;
2220
- }
2221
- const next = new Date(base);
2222
- next.setDate(next.getDate() + daysToAdd);
2223
- return next.toISOString();
2224
- }
2225
- throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
2226
- }
2227
- var DAY_NAMES;
2228
- var init_recurrence = __esm(() => {
2229
- DAY_NAMES = {
2230
- sunday: 0,
2231
- sun: 0,
2232
- monday: 1,
2233
- mon: 1,
2234
- tuesday: 2,
2235
- tue: 2,
2236
- wednesday: 3,
2237
- wed: 3,
2238
- thursday: 4,
2239
- thu: 4,
2240
- friday: 5,
2241
- fri: 5,
2242
- saturday: 6,
2243
- sat: 6
2244
- };
2245
- });
2246
-
2247
- // src/lib/doctor.ts
2248
- var exports_doctor = {};
2249
- __export(exports_doctor, {
2250
- runTodosDoctor: () => runTodosDoctor
2251
- });
2252
- import { chmodSync, copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, statSync as statSync2 } from "fs";
2253
- import { basename, dirname as dirname5, join as join4 } from "path";
2254
- function tableExists(db, table) {
2255
- return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
2256
- }
2257
- function quoteIdent(identifier) {
2258
- return `"${identifier.replace(/"/g, '""')}"`;
2259
- }
2260
- function getTableColumns(db, table) {
2261
- try {
2262
- const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
2263
- return new Set(rows.map((row) => row.name));
2264
- } catch {
2265
- return new Set;
2266
- }
2267
- }
2268
- function countQuery(db, sql) {
2269
- try {
2270
- const row = db.query(sql).get();
2271
- return row?.count ?? 0;
2272
- } catch {
2273
- return 0;
2274
- }
2275
- }
2276
- function getMigrationLevel(db) {
2277
- try {
2278
- const row = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
2279
- return row?.max_id ?? 0;
2280
- } catch {
2281
- return 0;
2282
- }
2283
- }
2284
- function addCheck(checks, check) {
2285
- checks.push(check);
2286
- }
2287
- function listUserTables(db) {
2288
- return db.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all().map((row) => row.name).sort();
2289
- }
2290
- function findCorruptJsonMetadata(db) {
2291
- const corrupt = [];
2292
- for (const table of listUserTables(db)) {
2293
- const columns = getTableColumns(db, table);
2294
- if (!columns.has("metadata"))
2295
- continue;
2296
- const rows = db.query(`SELECT rowid, metadata FROM ${quoteIdent(table)} WHERE metadata IS NOT NULL AND metadata != ''`).all();
2297
- for (const row of rows) {
2298
- try {
2299
- JSON.parse(row.metadata);
2300
- } catch {
2301
- corrupt.push({ table, column: "metadata", rowid: row.rowid });
2302
- }
2303
- }
2304
- }
2305
- return corrupt;
2306
- }
2307
- function getIndexColumns(db, indexName) {
2308
- return db.query(`PRAGMA index_info(${quoteIdent(indexName)})`).all().map((row) => row.name).filter(Boolean);
2309
- }
2310
- function findDuplicateIndexes(db) {
2311
- const duplicates = [];
2312
- for (const table of listUserTables(db)) {
2313
- const indexes = db.query(`PRAGMA index_list(${quoteIdent(table)})`).all();
2314
- const groups = new Map;
2315
- for (const index of indexes) {
2316
- if (index.origin === "pk" || index.name.startsWith("sqlite_autoindex"))
2317
- continue;
2318
- const columns = getIndexColumns(db, index.name);
2319
- if (columns.length === 0)
2320
- continue;
2321
- const key = `${index.unique}:${columns.join(",")}`;
2322
- const current = groups.get(key) ?? [];
2323
- current.push({ name: index.name, origin: index.origin, columns });
2324
- groups.set(key, current);
2325
- }
2326
- for (const group of groups.values()) {
2327
- if (group.length < 2)
2328
- continue;
2329
- const [kept, ...rest] = group;
2330
- for (const duplicate of rest) {
2331
- duplicates.push({ table, duplicate: duplicate.name, kept: kept.name, columns: duplicate.columns });
2332
- }
2333
- }
2334
- }
2335
- return duplicates;
2336
- }
2337
- function findMissingProjectRoots(db) {
2338
- if (!tableExists(db, "projects"))
2339
- return 0;
2340
- let missing = 0;
2341
- const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
2342
- for (const row of rows) {
2343
- if (/^[a-z]+:\/\//i.test(row.path))
2344
- continue;
2345
- if (!row.path.startsWith("/"))
2346
- continue;
2347
- if (!existsSync5(row.path))
2348
- missing++;
1452
+ _db = null;
2349
1453
  }
2350
- return missing;
2351
1454
  }
2352
- function addTaskStateChecks(db, checks) {
2353
- if (!tableExists(db, "tasks"))
2354
- return;
2355
- const columns = getTableColumns(db, "tasks");
2356
- const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
2357
- if (columns.has("updated_at") && columns.has("status")) {
2358
- const staleTasks = countQuery(db, `SELECT COUNT(*) as count FROM tasks WHERE status = 'in_progress' AND updated_at < '${staleCutoff}'`);
2359
- if (staleTasks > 0) {
2360
- addCheck(checks, {
2361
- severity: "warn",
2362
- type: "stale_tasks",
2363
- message: `${staleTasks} tasks stuck in_progress for more than 30 minutes`,
2364
- count: staleTasks,
2365
- repairable: false
2366
- });
2367
- }
2368
- }
2369
- if (columns.has("recurrence_rule") && columns.has("status")) {
2370
- const dueAtSelect = columns.has("due_at") ? "due_at" : "NULL as due_at";
2371
- 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();
2372
- const invalidRecurrence = recurring.filter((task) => !isValidRecurrenceRule(task.recurrence_rule));
2373
- if (invalidRecurrence.length > 0) {
2374
- addCheck(checks, {
2375
- severity: "error",
2376
- type: "invalid_recurrence",
2377
- message: `${invalidRecurrence.length} tasks have invalid recurrence rules`,
2378
- count: invalidRecurrence.length,
2379
- repairable: false
2380
- });
2381
- }
2382
- const nowIso = new Date().toISOString();
2383
- const overdueRecurring = recurring.filter((task) => task.due_at !== null && task.due_at < nowIso);
2384
- if (overdueRecurring.length > 0) {
2385
- addCheck(checks, {
2386
- severity: "warn",
2387
- type: "overdue_recurring",
2388
- message: `${overdueRecurring.length} recurring tasks are past due`,
2389
- count: overdueRecurring.length,
2390
- repairable: false
2391
- });
2392
- }
2393
- }
1455
+ function resetDatabase() {
1456
+ _db = null;
2394
1457
  }
2395
- function databasePermissionsAreUnsafe(dbPath) {
2396
- if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
2397
- return false;
2398
- try {
2399
- return (statSync2(dbPath).mode & 63) !== 0;
2400
- } catch {
2401
- return false;
2402
- }
1458
+ function now() {
1459
+ return new Date().toISOString();
2403
1460
  }
2404
- function createBackup(dbPath) {
2405
- if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
2406
- return;
2407
- if (!existsSync5(dbPath))
2408
- return;
2409
- const stamp = now().replace(/[:.]/g, "-");
2410
- const backupDir = join4(dirname5(dbPath), `${basename(dbPath)}.backup-${stamp}`);
2411
- const files = [];
2412
- mkdirSync4(backupDir, { recursive: true });
2413
- for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
2414
- if (!existsSync5(source))
2415
- continue;
2416
- const target = join4(backupDir, basename(source));
2417
- copyFileSync(source, target);
2418
- files.push(target);
2419
- }
2420
- return files.length > 0 ? { path: backupDir, files } : undefined;
1461
+ function uuid() {
1462
+ return crypto.randomUUID();
2421
1463
  }
2422
- function pushRepair(repairs, type, message, applied, count) {
2423
- repairs.push({ type, message, applied, count });
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;
2424
1470
  }
2425
- function summarize2(checks, repairs) {
2426
- return {
2427
- errors: checks.filter((check) => check.severity === "error").length,
2428
- warnings: checks.filter((check) => check.severity === "warn").length,
2429
- infos: checks.filter((check) => check.severity === "info").length,
2430
- repairable: checks.filter((check) => check.repairable).length,
2431
- applied: repairs.filter((repair) => repair.applied).length
2432
- };
1471
+ function lockExpiryCutoff(nowMs = Date.now()) {
1472
+ const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
1473
+ return new Date(nowMs - expiryMs).toISOString();
2433
1474
  }
2434
- function deleteOrphans(db, table, where) {
2435
- const before = countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
2436
- if (before > 0)
2437
- db.run(`DELETE FROM ${table} WHERE ${where}`);
2438
- return before;
2439
- }
2440
- function runTodosDoctor(options = {}) {
2441
- const db = options.db ?? getDatabase();
2442
- const dbPath = options.dbPath ?? getDatabasePath();
2443
- const apply = options.apply === true;
2444
- const checks = [];
2445
- const repairs = [];
2446
- const migrationCurrent = getMigrationLevel(db);
2447
- const migrationExpected = MIGRATIONS.length;
2448
- if (migrationCurrent < migrationExpected) {
2449
- addCheck(checks, {
2450
- severity: "error",
2451
- type: "migration_level",
2452
- message: `Migration level ${migrationCurrent}; expected ${migrationExpected}`,
2453
- repairable: true
2454
- });
2455
- } else {
2456
- addCheck(checks, {
2457
- severity: "info",
2458
- type: "migration_level",
2459
- message: `Schema at migration ${migrationCurrent}`
2460
- });
2461
- }
2462
- const missingTables = REQUIRED_TABLES.filter((table) => !tableExists(db, table));
2463
- if (missingTables.length > 0) {
2464
- addCheck(checks, {
2465
- severity: "error",
2466
- type: "missing_schema_tables",
2467
- message: `Missing schema tables: ${missingTables.join(", ")}`,
2468
- count: missingTables.length,
2469
- repairable: true
2470
- });
2471
- }
2472
- 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;
2473
- if (orphanedParents > 0) {
2474
- addCheck(checks, {
2475
- severity: "error",
2476
- type: "orphaned_task_parents",
2477
- message: `${orphanedParents} tasks reference missing parent tasks`,
2478
- count: orphanedParents,
2479
- repairable: true
2480
- });
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}`);
2481
1482
  }
2482
- 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;
2483
- if (orphanedDependencies > 0) {
2484
- addCheck(checks, {
2485
- severity: "error",
2486
- type: "orphaned_task_dependencies",
2487
- message: `${orphanedDependencies} dependency rows reference missing tasks`,
2488
- count: orphanedDependencies,
2489
- repairable: true
2490
- });
1483
+ if (partialId.length >= 36) {
1484
+ const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
1485
+ return row?.id ?? null;
2491
1486
  }
2492
- const orphanTables = [
2493
- ["task_comments", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_comments.task_id)"],
2494
- ["task_runs", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_runs.task_id)"],
2495
- ["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)"],
2496
- ["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)"],
2497
- ["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)"]
2498
- ];
2499
- let orphanedRows = 0;
2500
- for (const [table, where] of orphanTables) {
2501
- if (!tableExists(db, table))
2502
- continue;
2503
- orphanedRows += countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
2504
- }
2505
- if (orphanedRows > 0) {
2506
- addCheck(checks, {
2507
- severity: "error",
2508
- type: "orphaned_child_rows",
2509
- message: `${orphanedRows} child rows reference missing tasks or runs`,
2510
- count: orphanedRows,
2511
- repairable: true
2512
- });
1487
+ const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
1488
+ if (rows.length === 1) {
1489
+ return rows[0].id;
2513
1490
  }
2514
- addTaskStateChecks(db, checks);
2515
- const corruptJson = findCorruptJsonMetadata(db);
2516
- if (corruptJson.length > 0) {
2517
- addCheck(checks, {
2518
- severity: "error",
2519
- type: "corrupt_json_metadata",
2520
- message: `${corruptJson.length} metadata values are not valid JSON`,
2521
- count: corruptJson.length,
2522
- repairable: true
2523
- });
1491
+ if (rows.length > 1) {
1492
+ return null;
2524
1493
  }
2525
- const duplicateIndexes = findDuplicateIndexes(db);
2526
- if (duplicateIndexes.length > 0) {
2527
- addCheck(checks, {
2528
- severity: "warn",
2529
- type: "duplicate_indexes",
2530
- message: `${duplicateIndexes.length} duplicate index definitions found`,
2531
- count: duplicateIndexes.length,
2532
- repairable: true
2533
- });
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
+ }
2534
1499
  }
2535
- const missingProjectRoots = findMissingProjectRoots(db);
2536
- if (missingProjectRoots > 0) {
2537
- addCheck(checks, {
2538
- severity: "warn",
2539
- type: "missing_project_roots",
2540
- message: `${missingProjectRoots} project paths do not exist on this machine`,
2541
- count: missingProjectRoots,
2542
- repairable: false
2543
- });
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;
2544
1504
  }
2545
- const unsafePermissions = databasePermissionsAreUnsafe(dbPath);
2546
- addCheck(checks, unsafePermissions ? {
2547
- severity: "warn",
2548
- type: "database_permissions",
2549
- message: "Database file is readable or writable by group/others",
2550
- repairable: true
2551
- } : {
2552
- severity: "info",
2553
- type: "database_permissions",
2554
- message: "Database file permissions are private"
2555
- });
2556
- let backup;
2557
- const hasRepairableIssue = checks.some((check) => check.repairable && check.severity !== "info");
2558
- if (apply && hasRepairableIssue) {
2559
- backup = createBackup(dbPath);
2560
- if (backup)
2561
- pushRepair(repairs, "backup_created", `Created backup at ${backup.path}`, true, backup.files.length);
2562
- else
2563
- pushRepair(repairs, "backup_created", "Backup skipped for in-memory or missing database path", false, 0);
2564
- if (migrationCurrent < migrationExpected || missingTables.length > 0) {
2565
- runMigrations(db);
2566
- ensureSchema(db);
2567
- pushRepair(repairs, "schema_repair", "Ran migration and schema safety net", true);
2568
- }
2569
- if (orphanedParents > 0) {
2570
- 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)");
2571
- pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
2572
- }
2573
- if (orphanedDependencies > 0 && tableExists(db, "task_dependencies")) {
2574
- 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)");
2575
- pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count);
2576
- }
2577
- for (const [table, where] of orphanTables) {
2578
- if (!tableExists(db, table))
2579
- continue;
2580
- const count = deleteOrphans(db, table, where);
2581
- if (count > 0)
2582
- pushRepair(repairs, "orphaned_child_rows", `Deleted orphaned rows from ${table}`, true, count);
2583
- }
2584
- if (corruptJson.length > 0) {
2585
- for (const cell of corruptJson) {
2586
- db.run(`UPDATE ${quoteIdent(cell.table)} SET ${quoteIdent(cell.column)} = '{}' WHERE rowid = ?`, [cell.rowid]);
2587
- }
2588
- pushRepair(repairs, "corrupt_json_metadata", "Reset invalid metadata JSON values to {}", true, corruptJson.length);
2589
- }
2590
- if (duplicateIndexes.length > 0) {
2591
- let dropped = 0;
2592
- for (const duplicate of duplicateIndexes) {
2593
- db.run(`DROP INDEX IF EXISTS ${quoteIdent(duplicate.duplicate)}`);
2594
- dropped++;
2595
- }
2596
- pushRepair(repairs, "duplicate_indexes", "Dropped duplicate non-primary indexes", true, dropped);
2597
- }
2598
- if (unsafePermissions) {
2599
- try {
2600
- chmodSync(dbPath, 384);
2601
- pushRepair(repairs, "database_permissions", "Changed database file mode to 0600", true);
2602
- } catch (error) {
2603
- pushRepair(repairs, "database_permissions", error instanceof Error ? error.message : "Failed to repair database permissions", false);
2604
- }
2605
- }
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;
2606
1509
  }
2607
- const finalChecks = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }).checks : checks;
2608
- const summary = summarize2(finalChecks, repairs);
2609
- return {
2610
- ok: !finalChecks.some((check) => check.severity === "error"),
2611
- dry_run: !apply,
2612
- database_path: dbPath,
2613
- migration: { current: getMigrationLevel(db), expected: migrationExpected },
2614
- backup,
2615
- checks: finalChecks,
2616
- repairs,
2617
- summary
2618
- };
1510
+ return null;
2619
1511
  }
2620
- var REQUIRED_TABLES;
2621
- var init_doctor = __esm(() => {
2622
- init_database();
2623
- init_migrations();
1512
+ var LOCK_EXPIRY_MINUTES = 30, _db = null, ALLOWED_TABLES;
1513
+ var init_database = __esm(() => {
2624
1514
  init_schema();
2625
- init_recurrence();
2626
- REQUIRED_TABLES = [
2627
- "_migrations",
2628
- "projects",
2629
- "tasks",
2630
- "plans",
2631
- "agents",
2632
- "task_dependencies",
2633
- "task_comments",
2634
- "task_runs",
2635
- "task_run_events",
2636
- "task_run_commands",
2637
- "task_run_artifacts"
2638
- ];
1515
+ init_machines();
1516
+ ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates"]);
2639
1517
  });
2640
1518
 
2641
1519
  // src/lib/package-version.ts
@@ -2663,8 +1541,8 @@ function getPackageVersion(fromUrl = import.meta.url) {
2663
1541
 
2664
1542
  // src/server/serve.ts
2665
1543
  init_database();
2666
- import { existsSync as existsSync6 } from "fs";
2667
- 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";
2668
1546
  import { fileURLToPath as fileURLToPath2 } from "url";
2669
1547
 
2670
1548
  // src/db/api-keys.ts
@@ -2775,7 +1653,7 @@ init_database();
2775
1653
 
2776
1654
  // src/lib/config.ts
2777
1655
  import { existsSync as existsSync4 } from "fs";
2778
- import { dirname as dirname3, join as join3 } from "path";
1656
+ import { join as join3 } from "path";
2779
1657
 
2780
1658
  // src/lib/sync-utils.ts
2781
1659
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync } from "fs";
@@ -2928,505 +1806,6 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
2928
1806
  }
2929
1807
  }
2930
1808
 
2931
- // src/lib/event-hooks.ts
2932
- import { createHash as createHash2, randomUUID } from "crypto";
2933
- import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
2934
- import { dirname as dirname4, resolve as resolve4 } from "path";
2935
- import { createConnection } from "net";
2936
-
2937
- // src/lib/redaction.ts
2938
- var DEFAULT_SECRET_PATTERNS = [
2939
- { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
2940
- { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
2941
- { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
2942
- { 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]" },
2943
- { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
2944
- ];
2945
- var DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
2946
- var NON_SECRET_USAGE_KEYS = new Set([
2947
- "tokens",
2948
- "total_tokens",
2949
- "token_count",
2950
- "input_tokens",
2951
- "output_tokens",
2952
- "prompt_tokens",
2953
- "completion_tokens"
2954
- ]);
2955
- function unique(values) {
2956
- return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
2957
- }
2958
- function cloneRegex(regex) {
2959
- return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
2960
- }
2961
- function customPatterns() {
2962
- return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
2963
- try {
2964
- return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
2965
- } catch {
2966
- return [];
2967
- }
2968
- });
2969
- }
2970
- function secretPatterns() {
2971
- return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
2972
- }
2973
- function isSecretKey(key) {
2974
- if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
2975
- return false;
2976
- if (DEFAULT_SECRET_KEY_PATTERN.test(key))
2977
- return true;
2978
- return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
2979
- }
2980
- function redactEvidenceText(value) {
2981
- let redacted = value;
2982
- for (const pattern of secretPatterns()) {
2983
- const regex = cloneRegex(pattern.regex);
2984
- const replacement = pattern.replacement ?? "[REDACTED]";
2985
- redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
2986
- }
2987
- return redacted;
2988
- }
2989
- function redactValue(value) {
2990
- if (typeof value === "string")
2991
- return redactEvidenceText(value);
2992
- if (Array.isArray(value))
2993
- return value.map(redactValue);
2994
- if (value && typeof value === "object") {
2995
- const redacted = {};
2996
- for (const [key, child] of Object.entries(value)) {
2997
- if (isSecretKey(key)) {
2998
- redacted[key] = "[REDACTED]";
2999
- } else {
3000
- redacted[key] = redactValue(child);
3001
- }
3002
- }
3003
- return redacted;
3004
- }
3005
- return value;
3006
- }
3007
-
3008
- // src/lib/runner-sandbox.ts
3009
- import { relative as relative2, resolve as resolve3 } from "path";
3010
-
3011
- // src/lib/workspace-trust.ts
3012
- import { relative, resolve as resolve2 } from "path";
3013
- var DEFAULT_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
3014
- var DEFAULT_ENV_REDACTIONS = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
3015
- var PRESET_DEFAULTS = {
3016
- restricted: {
3017
- trusted: false,
3018
- preset: "restricted",
3019
- command_allowlist: ["todos"],
3020
- command_denylist: DEFAULT_DENYLIST,
3021
- tool_permissions: ["read"],
3022
- write_scopes: [],
3023
- env_redactions: DEFAULT_ENV_REDACTIONS,
3024
- require_prompt_for_unsafe: true
3025
- },
3026
- readonly: {
3027
- trusted: false,
3028
- preset: "readonly",
3029
- command_allowlist: ["todos", "git status", "git diff", "bun test"],
3030
- command_denylist: DEFAULT_DENYLIST,
3031
- tool_permissions: ["read", "list", "search"],
3032
- write_scopes: [],
3033
- env_redactions: DEFAULT_ENV_REDACTIONS,
3034
- require_prompt_for_unsafe: true
3035
- },
3036
- standard: {
3037
- trusted: true,
3038
- preset: "standard",
3039
- command_allowlist: ["todos", "git", "bun", "rg"],
3040
- command_denylist: DEFAULT_DENYLIST,
3041
- tool_permissions: ["read", "write", "test", "mcp"],
3042
- write_scopes: ["."],
3043
- env_redactions: DEFAULT_ENV_REDACTIONS,
3044
- require_prompt_for_unsafe: true
3045
- },
3046
- trusted: {
3047
- trusted: true,
3048
- preset: "trusted",
3049
- command_allowlist: ["*"],
3050
- command_denylist: DEFAULT_DENYLIST,
3051
- tool_permissions: ["*"],
3052
- write_scopes: ["."],
3053
- env_redactions: DEFAULT_ENV_REDACTIONS,
3054
- require_prompt_for_unsafe: false
3055
- }
3056
- };
3057
- function normalizePath(path) {
3058
- return resolve2(path);
3059
- }
3060
- function unique2(values) {
3061
- return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
3062
- }
3063
- function defaultProfile(root, preset) {
3064
- return {
3065
- root,
3066
- ...PRESET_DEFAULTS[preset]
3067
- };
3068
- }
3069
- function configuredProfiles(config = loadConfig()) {
3070
- return Object.values(config.workspace_trust || {}).map((profile) => ({ ...profile, root: normalizePath(profile.root) })).sort((a, b) => b.root.length - a.root.length);
3071
- }
3072
- function isPathInside(root, path) {
3073
- const rel = relative(root, path);
3074
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/") && !/^[A-Za-z]:/.test(rel);
3075
- }
3076
- function matchesPattern(value, pattern) {
3077
- if (pattern === "*")
3078
- return true;
3079
- if (pattern.includes("*")) {
3080
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
3081
- return new RegExp(`^${escaped}$`, "i").test(value);
3082
- }
3083
- return value === pattern || value.startsWith(`${pattern} `) || value.includes(pattern);
3084
- }
3085
- function profileFor(path) {
3086
- const resolved = normalizePath(path);
3087
- for (const profile of configuredProfiles()) {
3088
- if (isPathInside(profile.root, resolved))
3089
- return { profile, matchedRoot: profile.root };
3090
- }
3091
- return { profile: defaultProfile(resolved, "restricted"), matchedRoot: null };
3092
- }
3093
- function getWorkspaceTrustStatus(path = process.cwd()) {
3094
- const root = normalizePath(path);
3095
- const { profile, matchedRoot } = profileFor(root);
3096
- return {
3097
- root,
3098
- trusted: profile.trusted,
3099
- matched_root: matchedRoot,
3100
- profile
3101
- };
3102
- }
3103
- function writeAllowed(profile, root, writePath) {
3104
- const target = normalizePath(writePath.startsWith("/") ? writePath : `${root}/${writePath}`);
3105
- return profile.write_scopes.some((scope) => {
3106
- const scopeRoot = normalizePath(scope.startsWith("/") ? scope : `${root}/${scope}`);
3107
- return isPathInside(scopeRoot, target);
3108
- });
3109
- }
3110
- function redactedEnvKeys(profile, env) {
3111
- if (!env)
3112
- return [];
3113
- const patterns = unique2([...DEFAULT_ENV_REDACTIONS, ...profile.env_redactions]).map((item) => item.toUpperCase());
3114
- return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
3115
- }
3116
- function checkWorkspacePermission(input = {}) {
3117
- const status = getWorkspaceTrustStatus(input.path || process.cwd());
3118
- const reasons = [];
3119
- const profile = status.profile;
3120
- if (!status.matched_root)
3121
- reasons.push("workspace is not trusted");
3122
- if (input.command) {
3123
- if (profile.command_denylist.some((pattern) => matchesPattern(input.command, pattern))) {
3124
- reasons.push("command matches denylist");
3125
- } else if (!profile.command_allowlist.some((pattern) => matchesPattern(input.command, pattern))) {
3126
- reasons.push("command is not in allowlist");
3127
- }
3128
- }
3129
- if (input.tool && !profile.tool_permissions.some((permission) => matchesPattern(input.tool, permission))) {
3130
- reasons.push("tool permission is not allowed");
3131
- }
3132
- if (input.write_path && !writeAllowed(profile, status.matched_root || status.root, input.write_path)) {
3133
- reasons.push("write path is outside allowed scopes");
3134
- }
3135
- const redacted = redactedEnvKeys(profile, input.env);
3136
- const allowed = reasons.length === 0;
3137
- return {
3138
- allowed,
3139
- requires_prompt: !allowed && profile.require_prompt_for_unsafe,
3140
- reasons,
3141
- status,
3142
- redacted_env_keys: redacted
3143
- };
3144
- }
3145
-
3146
- // src/lib/runner-sandbox.ts
3147
- var DEFAULT_COMMAND_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
3148
- var DEFAULT_ENV_REDACTIONS2 = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
3149
- function normalizePath2(path) {
3150
- return resolve3(path);
3151
- }
3152
- function unique3(values) {
3153
- return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
3154
- }
3155
- function configuredProfiles2(config = loadConfig()) {
3156
- return Object.values(config.runner_sandboxes || {}).map((profile) => ({
3157
- ...profile,
3158
- root: normalizePath2(profile.root),
3159
- cwd_boundary: normalizePath2(profile.cwd_boundary || profile.root)
3160
- })).sort((a, b) => a.name.localeCompare(b.name));
3161
- }
3162
- function isPathInside2(root, path) {
3163
- const rel = relative2(root, path);
3164
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/") && !/^[A-Za-z]:/.test(rel);
3165
- }
3166
- function matchesPattern2(value, pattern) {
3167
- if (pattern === "*")
3168
- return true;
3169
- if (pattern.includes("*")) {
3170
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
3171
- return new RegExp(`^${escaped}$`, "i").test(value);
3172
- }
3173
- return value === pattern || value.startsWith(`${pattern} `) || value.includes(pattern);
3174
- }
3175
- function resolveFromRoot(root, path) {
3176
- return normalizePath2(path.startsWith("/") ? path : `${root}/${path}`);
3177
- }
3178
- function defaultProfile2(name, root) {
3179
- const normalizedRoot = normalizePath2(root);
3180
- return {
3181
- name,
3182
- root: normalizedRoot,
3183
- command_allowlist: ["todos", "git", "bun"],
3184
- command_denylist: DEFAULT_COMMAND_DENYLIST,
3185
- cwd_boundary: normalizedRoot,
3186
- write_scopes: ["."],
3187
- env_allowlist: ["PATH", "HOME", "SHELL", "TMPDIR", "TEMP", "TMP", "CI", "NODE_ENV", "BUN_ENV"],
3188
- env_redactions: DEFAULT_ENV_REDACTIONS2,
3189
- network_policy: "none",
3190
- require_approval: true,
3191
- audit_evidence: true
3192
- };
3193
- }
3194
- function profileByName(name, path) {
3195
- const profiles = configuredProfiles2();
3196
- if (name) {
3197
- const found = profiles.find((profile) => profile.name === name);
3198
- if (found)
3199
- return found;
3200
- return defaultProfile2(name, path);
3201
- }
3202
- const resolved = normalizePath2(path);
3203
- return profiles.find((profile) => isPathInside2(profile.root, resolved)) || defaultProfile2("default", resolved);
3204
- }
3205
- function redactedEnvKeys2(profile, env) {
3206
- if (!env)
3207
- return [];
3208
- const patterns = unique3([...DEFAULT_ENV_REDACTIONS2, ...profile.env_redactions]).map((item) => item.toUpperCase());
3209
- return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
3210
- }
3211
- function omittedEnvKeys(profile, env) {
3212
- if (!env)
3213
- return [];
3214
- if (profile.env_allowlist.includes("*"))
3215
- return [];
3216
- return Object.keys(env).filter((key) => !profile.env_allowlist.some((pattern) => matchesPattern2(key, pattern)));
3217
- }
3218
- function resolveFromCwd(cwd, path) {
3219
- return normalizePath2(path.startsWith("/") ? path : `${cwd}/${path}`);
3220
- }
3221
- function writeAllowed2(profile, cwd, writePath) {
3222
- const target = resolveFromCwd(cwd, writePath);
3223
- return profile.write_scopes.some((scope) => isPathInside2(resolveFromRoot(profile.root, scope), target));
3224
- }
3225
- function checkRunnerSandbox(input = {}) {
3226
- const path = normalizePath2(input.path || input.cwd || process.cwd());
3227
- const profile = profileByName(input.name, path);
3228
- const cwd = resolveFromRoot(profile.root, input.cwd || profile.root);
3229
- const reasons = [];
3230
- const writePaths = input.write_paths || [];
3231
- const resolvedWritePaths = writePaths.map((writePath) => resolveFromCwd(cwd, writePath));
3232
- if (!isPathInside2(profile.cwd_boundary, cwd))
3233
- reasons.push("cwd is outside sandbox boundary");
3234
- if (input.command) {
3235
- if (profile.command_denylist.some((pattern) => matchesPattern2(input.command, pattern))) {
3236
- reasons.push("command matches sandbox denylist");
3237
- } else if (!profile.command_allowlist.some((pattern) => matchesPattern2(input.command, pattern))) {
3238
- reasons.push("command is not in sandbox allowlist");
3239
- }
3240
- }
3241
- for (const writePath of writePaths) {
3242
- if (!writeAllowed2(profile, cwd, writePath)) {
3243
- reasons.push(`write path is outside sandbox scopes: ${writePath}`);
3244
- }
3245
- }
3246
- if (input.network && profile.network_policy === "none") {
3247
- reasons.push("network access is disabled by sandbox policy");
3248
- }
3249
- const trustChecks = [
3250
- checkWorkspacePermission({ path: profile.root, command: input.command, env: input.env }),
3251
- ...resolvedWritePaths.map((writePath) => checkWorkspacePermission({ path: profile.root, write_path: writePath }))
3252
- ];
3253
- for (const trust of trustChecks) {
3254
- for (const reason of trust.reasons)
3255
- reasons.push(`workspace trust: ${reason}`);
3256
- }
3257
- const redacted = redactedEnvKeys2(profile, input.env);
3258
- const omitted = omittedEnvKeys(profile, input.env);
3259
- const effective = Object.keys(input.env || {}).filter((key) => !omitted.includes(key));
3260
- const uniqueReasons = unique3(reasons);
3261
- const allowed = uniqueReasons.length === 0;
3262
- return {
3263
- allowed,
3264
- requires_approval: !allowed && profile.require_approval,
3265
- reasons: uniqueReasons,
3266
- profile,
3267
- redacted_env_keys: redacted,
3268
- omitted_env_keys: omitted,
3269
- effective_env_keys: effective,
3270
- audit_evidence: profile.audit_evidence ? {
3271
- sandbox: profile.name,
3272
- root: profile.root,
3273
- cwd,
3274
- command: input.command,
3275
- write_paths: writePaths,
3276
- network_requested: Boolean(input.network),
3277
- network_policy: profile.network_policy,
3278
- allowed,
3279
- reasons: uniqueReasons
3280
- } : null
3281
- };
3282
- }
3283
-
3284
- // src/lib/event-hooks.ts
3285
- var VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
3286
- function clampAttempts(value) {
3287
- if (!Number.isFinite(value))
3288
- return 1;
3289
- return Math.min(5, Math.max(1, Math.trunc(value)));
3290
- }
3291
- function eventMatches(hook, eventType) {
3292
- return hook.enabled !== false && (hook.events.includes("*") || hook.events.includes(eventType));
3293
- }
3294
- function canonicalEvent(input) {
3295
- return JSON.stringify(input);
3296
- }
3297
- function buildEnvelope(type, payload, timestamp = new Date().toISOString()) {
3298
- const base = {
3299
- id: randomUUID(),
3300
- type,
3301
- timestamp,
3302
- payload: redactValue(payload ?? {}),
3303
- source: { package: "@hasna/todos", local_only: true }
3304
- };
3305
- const digest = createHash2("sha256").update(canonicalEvent(base)).digest("hex");
3306
- return { ...base, integrity: { algorithm: "sha256", digest } };
3307
- }
3308
- function summarize(value) {
3309
- const redacted = redactEvidenceText(value.trim());
3310
- if (!redacted)
3311
- return;
3312
- return redacted.length > 1000 ? `${redacted.slice(0, 997)}...` : redacted;
3313
- }
3314
- function sleep(ms) {
3315
- return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
3316
- }
3317
- async function writeSocket(socketPath, line) {
3318
- await new Promise((resolveWrite, rejectWrite) => {
3319
- const socket = createConnection(socketPath);
3320
- const timeout = setTimeout(() => {
3321
- socket.destroy();
3322
- rejectWrite(new Error(`socket write timed out: ${socketPath}`));
3323
- }, 1000);
3324
- socket.on("error", (error) => {
3325
- clearTimeout(timeout);
3326
- rejectWrite(error);
3327
- });
3328
- socket.on("connect", () => {
3329
- socket.end(line, () => {
3330
- clearTimeout(timeout);
3331
- resolveWrite();
3332
- });
3333
- });
3334
- });
3335
- }
3336
- async function deliverScript(hook, envelope) {
3337
- const command = hook.command;
3338
- const cwd = hook.cwd || process.cwd();
3339
- if (hook.sandbox) {
3340
- const check = checkRunnerSandbox({ name: hook.sandbox, cwd, command, env: hook.env });
3341
- if (!check.allowed)
3342
- throw new Error(check.reasons.join("; "));
3343
- }
3344
- const proc = Bun.spawn(["bash", "-lc", command], {
3345
- cwd,
3346
- env: {
3347
- ...process.env,
3348
- ...hook.env || {},
3349
- TODOS_EVENT_JSON: JSON.stringify(envelope),
3350
- TODOS_EVENT_ID: envelope.id,
3351
- TODOS_EVENT_TYPE: envelope.type,
3352
- TODOS_EVENT_INTEGRITY: envelope.integrity.digest,
3353
- TODOS_HOOK_NAME: hook.name
3354
- },
3355
- stdout: "pipe",
3356
- stderr: "pipe"
3357
- });
3358
- const [stdout, stderr, exitCode] = await Promise.all([
3359
- new Response(proc.stdout).text(),
3360
- new Response(proc.stderr).text(),
3361
- proc.exited
3362
- ]);
3363
- return { exitCode, output: summarize([stdout, stderr].filter(Boolean).join(`
3364
- `)) };
3365
- }
3366
- async function deliverHook(hook, envelope) {
3367
- const line = `${JSON.stringify(envelope)}
3368
- `;
3369
- const maxAttempts = clampAttempts(hook.retry?.attempts ?? 1);
3370
- const backoffMs = Math.max(0, hook.retry?.backoff_ms ?? 0);
3371
- let lastError;
3372
- let output;
3373
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
3374
- try {
3375
- if (hook.target === "stdout") {
3376
- output = line.trim();
3377
- } else if (hook.target === "file") {
3378
- const filePath = resolve4(hook.file_path);
3379
- mkdirSync3(dirname4(filePath), { recursive: true });
3380
- appendFileSync(filePath, line);
3381
- } else if (hook.target === "socket") {
3382
- await writeSocket(hook.socket_path, line);
3383
- } else {
3384
- const result = await deliverScript(hook, envelope);
3385
- output = result.output;
3386
- if (result.exitCode !== 0)
3387
- throw new Error(`script exited ${result.exitCode}${output ? `: ${output}` : ""}`);
3388
- }
3389
- return {
3390
- hook: hook.name,
3391
- event_id: envelope.id,
3392
- event_type: envelope.type,
3393
- target: hook.target,
3394
- status: "delivered",
3395
- attempts: attempt,
3396
- integrity: envelope.integrity,
3397
- output_summary: output
3398
- };
3399
- } catch (error) {
3400
- lastError = error instanceof Error ? error.message : String(error);
3401
- if (attempt < maxAttempts && backoffMs > 0)
3402
- await sleep(backoffMs);
3403
- }
3404
- }
3405
- return {
3406
- hook: hook.name,
3407
- event_id: envelope.id,
3408
- event_type: envelope.type,
3409
- target: hook.target,
3410
- status: "failed",
3411
- attempts: maxAttempts,
3412
- integrity: envelope.integrity,
3413
- error: redactEvidenceText(lastError || "delivery failed")
3414
- };
3415
- }
3416
- function listLocalEventHooks() {
3417
- return Object.values(loadConfig().local_event_hooks || {}).sort((a, b) => a.name.localeCompare(b.name));
3418
- }
3419
- async function emitLocalEventHooks(input) {
3420
- const hooks = (input.hooks || listLocalEventHooks()).filter((hook) => eventMatches(hook, input.type));
3421
- if (hooks.length === 0)
3422
- return [];
3423
- const envelope = buildEnvelope(input.type, input.payload, input.timestamp);
3424
- return Promise.all(hooks.map((hook) => deliverHook(hook, envelope)));
3425
- }
3426
- function emitLocalEventHooksQuiet(input) {
3427
- emitLocalEventHooks(input).catch(() => {});
3428
- }
3429
-
3430
1809
  // src/db/audit.ts
3431
1810
  init_database();
3432
1811
  function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
@@ -3694,8 +2073,8 @@ function createTask(input, db) {
3694
2073
  let id = uuid();
3695
2074
  for (let attempt = 0;attempt < 3; attempt++) {
3696
2075
  try {
3697
- 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)
3698
- 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, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3699
2078
  id,
3700
2079
  null,
3701
2080
  input.project_id || null,
@@ -3717,7 +2096,6 @@ function createTask(input, db) {
3717
2096
  timestamp,
3718
2097
  input.due_at || null,
3719
2098
  input.estimated_minutes || null,
3720
- input.sla_minutes ?? null,
3721
2099
  input.confidence ?? null,
3722
2100
  input.retry_count ?? 0,
3723
2101
  input.max_retries ?? 3,
@@ -3997,10 +2375,6 @@ function updateTask(id, input, db) {
3997
2375
  sets.push("estimated_minutes = ?");
3998
2376
  params.push(input.estimated_minutes);
3999
2377
  }
4000
- if (input.sla_minutes !== undefined) {
4001
- sets.push("sla_minutes = ?");
4002
- params.push(input.sla_minutes);
4003
- }
4004
2378
  if (input.actual_minutes !== undefined) {
4005
2379
  sets.push("actual_minutes = ?");
4006
2380
  params.push(input.actual_minutes);
@@ -4065,14 +2439,9 @@ function updateTask(id, input, db) {
4065
2439
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
4066
2440
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
4067
2441
  dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
4068
- emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id, assigned_to: input.assigned_to, title: task.title } });
4069
2442
  }
4070
2443
  if (input.status !== undefined && input.status !== task.status) {
4071
2444
  dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
4072
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload: { id, old_status: task.status, new_status: input.status, title: task.title } });
4073
- }
4074
- if (input.approved_by !== undefined) {
4075
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
4076
2445
  }
4077
2446
  return {
4078
2447
  ...task,
@@ -4082,7 +2451,6 @@ function updateTask(id, input, db) {
4082
2451
  version: task.version + 1,
4083
2452
  updated_at: timestamp,
4084
2453
  completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : task.completed_at,
4085
- sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
4086
2454
  actual_minutes: input.actual_minutes ?? task.actual_minutes,
4087
2455
  confidence: input.confidence !== undefined ? input.confidence : task.confidence,
4088
2456
  retry_count: input.retry_count ?? task.retry_count,
@@ -4098,9 +2466,95 @@ function deleteTask(id, db) {
4098
2466
  const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
4099
2467
  return result.changes > 0;
4100
2468
  }
4101
- // src/db/task-lifecycle.ts
4102
- init_database();
4103
- 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
+ }
4104
2558
 
4105
2559
  // src/db/templates.ts
4106
2560
  init_database();
@@ -4227,13 +2681,6 @@ function getTaskDependencies(taskId, db) {
4227
2681
 
4228
2682
  // src/db/task-lifecycle.ts
4229
2683
  var MAX_SPAWN_DEPTH = 10;
4230
- function assertStartable(task, agentId) {
4231
- if (task.status === "pending")
4232
- return;
4233
- if (task.status === "in_progress")
4234
- return;
4235
- throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
4236
- }
4237
2684
  function getBlockingDeps(id, db) {
4238
2685
  const d = db || getDatabase();
4239
2686
  const deps = getTaskDependencies(id, d);
@@ -4252,38 +2699,22 @@ function startTask(id, agentId, db) {
4252
2699
  const task = getTask(id, d);
4253
2700
  if (!task)
4254
2701
  throw new TaskNotFoundError(id);
4255
- assertStartable(task, agentId);
4256
2702
  const blocking = getBlockingDeps(id, d);
4257
2703
  if (blocking.length > 0) {
4258
2704
  const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
4259
- emitLocalEventHooksQuiet({
4260
- type: "task.blocked",
4261
- payload: {
4262
- id,
4263
- agent_id: agentId,
4264
- title: task.title,
4265
- blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
4266
- }
4267
- });
4268
2705
  throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
4269
2706
  }
4270
2707
  const cutoff = lockExpiryCutoff();
4271
2708
  const timestamp = now();
4272
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 = ?
4273
- 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]);
4274
2711
  if (result.changes === 0) {
4275
- const current = getTask(id, d);
4276
- if (!current)
4277
- throw new TaskNotFoundError(id);
4278
- assertStartable(current, agentId);
4279
- if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
4280
- 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);
4281
2714
  }
4282
- throw new Error(`Task ${id} could not be started because it changed during claim`);
4283
2715
  }
4284
2716
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
4285
2717
  dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
4286
- emitLocalEventHooksQuiet({ type: "task.started", payload: { id, agent_id: agentId, title: task.title } });
4287
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 };
4288
2719
  }
4289
2720
  function completeTask(id, agentId, db, options) {
@@ -4321,10 +2752,9 @@ function completeTask(id, agentId, db, options) {
4321
2752
  tx();
4322
2753
  logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
4323
2754
  dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
4324
- emitLocalEventHooksQuiet({ type: "task.completed", payload: { id, agent_id: agentId, title: task.title, completed_at: timestamp } });
4325
2755
  let spawnedTask = null;
4326
2756
  if (task.recurrence_rule && !options?.skip_recurrence) {
4327
- spawnedTask = spawnNextRecurrence(task, d, timestamp);
2757
+ spawnedTask = spawnNextRecurrence(task, d);
4328
2758
  }
4329
2759
  let spawnedFromTemplate = null;
4330
2760
  if (task.spawns_template_id) {
@@ -4363,7 +2793,6 @@ function completeTask(id, agentId, db, options) {
4363
2793
  meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
4364
2794
  for (const dep of unblockedDeps) {
4365
2795
  dispatchWebhook("task.unblocked", { id: dep.id, unblocked_by: id, title: dep.title }, d).catch(() => {});
4366
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload: { id: dep.id, unblocked_by: id, title: dep.title } });
4367
2796
  }
4368
2797
  }
4369
2798
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
@@ -4476,7 +2905,6 @@ function failTask(id, agentId, reason, options, db) {
4476
2905
  WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
4477
2906
  logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
4478
2907
  dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
4479
- emitLocalEventHooksQuiet({ type: "task.failed", payload: { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title } });
4480
2908
  const failedTask = {
4481
2909
  ...task,
4482
2910
  status: "failed",
@@ -4521,31 +2949,28 @@ function failTask(id, agentId, reason, options, db) {
4521
2949
  }
4522
2950
  return { task: failedTask, retryTask };
4523
2951
  }
4524
- function getStaleTasks(staleQuery = 30, filters, db) {
2952
+ function getStaleTasks(staleMinutes = 30, filters, db) {
4525
2953
  const d = db || getDatabase();
4526
- const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
4527
- const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
4528
2954
  const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
4529
2955
  const conditions = [
4530
2956
  "status = 'in_progress'",
4531
2957
  "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
4532
2958
  ];
4533
2959
  const params = [cutoff, cutoff];
4534
- if (effectiveFilters?.project_id) {
2960
+ if (filters?.project_id) {
4535
2961
  conditions.push("project_id = ?");
4536
- params.push(effectiveFilters.project_id);
2962
+ params.push(filters.project_id);
4537
2963
  }
4538
- if (effectiveFilters?.task_list_id) {
2964
+ if (filters?.task_list_id) {
4539
2965
  conditions.push("task_list_id = ?");
4540
- params.push(effectiveFilters.task_list_id);
2966
+ params.push(filters.task_list_id);
4541
2967
  }
4542
2968
  const where = conditions.join(" AND ");
4543
2969
  const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
4544
2970
  return rows.map(rowToTask);
4545
2971
  }
4546
- function spawnNextRecurrence(completedTask, db, completedAt) {
4547
- const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
4548
- const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
2972
+ function spawnNextRecurrence(completedTask, db) {
2973
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
4549
2974
  let title = completedTask.title;
4550
2975
  if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
4551
2976
  title = title.slice(completedTask.short_id.length + 2);
@@ -4562,7 +2987,6 @@ function spawnNextRecurrence(completedTask, db, completedAt) {
4562
2987
  tags: completedTask.tags,
4563
2988
  metadata: completedTask.metadata,
4564
2989
  estimated_minutes: completedTask.estimated_minutes ?? undefined,
4565
- sla_minutes: completedTask.sla_minutes ?? undefined,
4566
2990
  recurrence_rule: completedTask.recurrence_rule,
4567
2991
  recurrence_parent_id: recurrenceParentId,
4568
2992
  due_at: dueAt
@@ -4654,130 +3078,6 @@ function getTaskStats(filters, db) {
4654
3078
  }
4655
3079
  // src/db/task-relations.ts
4656
3080
  init_database();
4657
- // src/db/boards.ts
4658
- init_database();
4659
-
4660
- // src/db/plans.ts
4661
- init_database();
4662
- function createPlan(input, db) {
4663
- const d = db || getDatabase();
4664
- const id = uuid();
4665
- const timestamp = now();
4666
- d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at)
4667
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
4668
- id,
4669
- input.project_id || null,
4670
- input.task_list_id || null,
4671
- input.agent_id || null,
4672
- input.name,
4673
- input.description || null,
4674
- input.status || "active",
4675
- timestamp,
4676
- timestamp
4677
- ]);
4678
- return getPlan(id, d);
4679
- }
4680
- function getPlan(id, db) {
4681
- const d = db || getDatabase();
4682
- const row = d.query("SELECT * FROM plans WHERE id = ?").get(id);
4683
- return row;
4684
- }
4685
- function listPlans(projectId, db) {
4686
- const d = db || getDatabase();
4687
- if (projectId) {
4688
- return d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
4689
- }
4690
- return d.query("SELECT * FROM plans ORDER BY created_at DESC").all();
4691
- }
4692
- function updatePlan(id, input, db) {
4693
- const d = db || getDatabase();
4694
- const plan = getPlan(id, d);
4695
- if (!plan)
4696
- throw new PlanNotFoundError(id);
4697
- const sets = ["updated_at = ?"];
4698
- const params = [now()];
4699
- if (input.name !== undefined) {
4700
- sets.push("name = ?");
4701
- params.push(input.name);
4702
- }
4703
- if (input.description !== undefined) {
4704
- sets.push("description = ?");
4705
- params.push(input.description);
4706
- }
4707
- if (input.status !== undefined) {
4708
- sets.push("status = ?");
4709
- params.push(input.status);
4710
- }
4711
- if (input.task_list_id !== undefined) {
4712
- sets.push("task_list_id = ?");
4713
- params.push(input.task_list_id);
4714
- }
4715
- if (input.agent_id !== undefined) {
4716
- sets.push("agent_id = ?");
4717
- params.push(input.agent_id);
4718
- }
4719
- params.push(id);
4720
- d.run(`UPDATE plans SET ${sets.join(", ")} WHERE id = ?`, params);
4721
- const updated = getPlan(id, d);
4722
- emitLocalEventHooksQuiet({
4723
- type: "plan.updated",
4724
- payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id }
4725
- });
4726
- return updated;
4727
- }
4728
- function deletePlan(id, db) {
4729
- const d = db || getDatabase();
4730
- const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
4731
- return result.changes > 0;
4732
- }
4733
- // src/db/calendar.ts
4734
- init_database();
4735
-
4736
- // src/lib/artifact-store.ts
4737
- init_database();
4738
-
4739
- // src/db/comments.ts
4740
- init_database();
4741
- function addComment(input, db) {
4742
- const d = db || getDatabase();
4743
- if (!getTask(input.task_id, d)) {
4744
- throw new TaskNotFoundError(input.task_id);
4745
- }
4746
- const id = uuid();
4747
- const timestamp = now();
4748
- d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, type, progress_pct, created_at)
4749
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
4750
- id,
4751
- input.task_id,
4752
- input.agent_id || null,
4753
- input.session_id || null,
4754
- redactEvidenceText(input.content),
4755
- input.type || "comment",
4756
- input.progress_pct ?? null,
4757
- timestamp
4758
- ]);
4759
- return getComment(id, d);
4760
- }
4761
- function logProgress(taskId, message, pct, agentId, db) {
4762
- return addComment({ task_id: taskId, content: message, type: "progress", progress_pct: pct, agent_id: agentId }, db);
4763
- }
4764
- function getComment(id, db) {
4765
- const d = db || getDatabase();
4766
- return d.query("SELECT * FROM task_comments WHERE id = ?").get(id);
4767
- }
4768
- function listComments(taskId, db) {
4769
- const d = db || getDatabase();
4770
- return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
4771
- }
4772
-
4773
- // src/db/task-runs.ts
4774
- init_database();
4775
-
4776
- // src/db/task-files.ts
4777
- init_database();
4778
-
4779
- // src/db/task-commits.ts
4780
- init_database();
4781
3081
  // src/db/agents.ts
4782
3082
  init_database();
4783
3083
 
@@ -4927,12 +3227,48 @@ var RESERVED_GENERIC_NAMES = new Set([
4927
3227
  ]);
4928
3228
  var NUMERIC_SUFFIX_RE = /[-_]\d+$/;
4929
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);
4930
3253
  function normalizeAgentNameInput(name) {
4931
3254
  return name.trim().toLowerCase();
4932
3255
  }
4933
3256
  function hasGeneratedNumericSuffix(name) {
4934
3257
  return NUMERIC_SUFFIX_RE.test(normalizeAgentNameInput(name));
4935
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
+ }
4936
3272
  function isGenericAgentName(name) {
4937
3273
  const normalized = normalizeAgentNameInput(name);
4938
3274
  if (RESERVED_GENERIC_NAMES.has(normalized))
@@ -5055,11 +3391,21 @@ function validateAgentName(name, existingNames = []) {
5055
3391
  if (hasGeneratedNumericSuffix(normalized)) {
5056
3392
  throw new InvalidAgentNameError(name, "numbered suffix names are not allowed; pick a distinct human-readable name", suggestions);
5057
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
+ }
5058
3397
  if (!ONE_WORD_NAME_RE.test(normalized)) {
5059
3398
  throw new InvalidAgentNameError(name, "use one word made of letters only, preferably a Roman or Greek name", suggestions);
5060
3399
  }
5061
3400
  return normalized;
5062
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
+ ]);
5063
3409
 
5064
3410
  // src/db/agents.ts
5065
3411
  function getActiveWindowMs() {
@@ -5296,6 +3642,78 @@ function getOrgChart(db) {
5296
3642
  return buildTree(null);
5297
3643
  }
5298
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
+
5299
3717
  // src/db/orgs.ts
5300
3718
  init_database();
5301
3719
  function rowToOrg(row) {
@@ -5345,8 +3763,42 @@ function deleteOrg(id, db) {
5345
3763
  return d.run("DELETE FROM orgs WHERE id = ?", [id]).changes > 0;
5346
3764
  }
5347
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
+
5348
3800
  // src/server/routes.ts
5349
- import { join as join5, resolve as resolve5, sep } from "path";
3801
+ import { join as join4, resolve as resolve2, sep } from "path";
5350
3802
  function parseFieldsParam(url) {
5351
3803
  const fieldsParam = url.searchParams.get("fields");
5352
3804
  return fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
@@ -5917,8 +4369,16 @@ async function handleBulkDeleteProjects(req, _ctx, json2) {
5917
4369
  }
5918
4370
  }
5919
4371
  function handleDoctor(_ctx, json2) {
5920
- const { runTodosDoctor: runTodosDoctor2 } = (init_doctor(), __toCommonJS(exports_doctor));
5921
- 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 });
5922
4382
  }
5923
4383
  function handleReport(_req, url, _ctx, json2) {
5924
4384
  const days = parseInt(url.searchParams.get("days") || "7", 10);
@@ -6046,9 +4506,9 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
6046
4506
  if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
6047
4507
  return null;
6048
4508
  if (path !== "/") {
6049
- const filePath = join5(ctx.dashboardDir, path);
6050
- const resolvedFile = resolve5(filePath);
6051
- const resolvedBase = resolve5(ctx.dashboardDir);
4509
+ const filePath = join4(ctx.dashboardDir, path);
4510
+ const resolvedFile = resolve2(filePath);
4511
+ const resolvedBase = resolve2(ctx.dashboardDir);
6052
4512
  if (!resolvedFile.startsWith(resolvedBase + sep) && resolvedFile !== resolvedBase) {
6053
4513
  return json2({ error: "Forbidden" }, 403);
6054
4514
  }
@@ -6056,7 +4516,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
6056
4516
  if (res2)
6057
4517
  return res2;
6058
4518
  }
6059
- const indexPath = join5(ctx.dashboardDir, "index.html");
4519
+ const indexPath = join4(ctx.dashboardDir, "index.html");
6060
4520
  const res = serveStaticFile2(indexPath);
6061
4521
  if (res)
6062
4522
  return res;
@@ -6067,21 +4527,21 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
6067
4527
  function resolveDashboardDir() {
6068
4528
  const candidates = [];
6069
4529
  try {
6070
- const scriptDir = dirname6(fileURLToPath2(import.meta.url));
6071
- candidates.push(join6(scriptDir, "..", "dashboard", "dist"));
6072
- 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"));
6073
4533
  } catch {}
6074
4534
  if (process.argv[1]) {
6075
- const mainDir = dirname6(process.argv[1]);
6076
- candidates.push(join6(mainDir, "..", "dashboard", "dist"));
6077
- 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"));
6078
4538
  }
6079
- candidates.push(join6(process.cwd(), "dashboard", "dist"));
4539
+ candidates.push(join5(process.cwd(), "dashboard", "dist"));
6080
4540
  for (const candidate of candidates) {
6081
- if (existsSync6(candidate))
4541
+ if (existsSync5(candidate))
6082
4542
  return candidate;
6083
4543
  }
6084
- return join6(process.cwd(), "dashboard", "dist");
4544
+ return join5(process.cwd(), "dashboard", "dist");
6085
4545
  }
6086
4546
  var MIME_TYPES = {
6087
4547
  ".html": "text/html; charset=utf-8",
@@ -6153,7 +4613,7 @@ function json(data, status = 200, headers) {
6153
4613
  });
6154
4614
  }
6155
4615
  function serveStaticFile(filePath) {
6156
- if (!existsSync6(filePath))
4616
+ if (!existsSync5(filePath))
6157
4617
  return null;
6158
4618
  const ext = extname(filePath);
6159
4619
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -6232,7 +4692,7 @@ data: ${data}
6232
4692
  filteredSseClients.delete(client);
6233
4693
  }
6234
4694
  const dashboardDir = resolveDashboardDir();
6235
- const dashboardExists = existsSync6(dashboardDir);
4695
+ const dashboardExists = existsSync5(dashboardDir);
6236
4696
  if (!dashboardExists) {
6237
4697
  console.error(`
6238
4698
  Dashboard not found at: ${dashboardDir}`);