@hasna/todos 0.11.52 → 0.11.53

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 (210) hide show
  1. package/README.md +14 -1
  2. package/dashboard/dist/assets/{index-C3fBxEWP.js → index-aJefI7kh.js} +1 -1
  3. package/dashboard/dist/index.html +1 -1
  4. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/machines.d.ts.map +1 -1
  6. package/dist/cli/commands/storage-commands.d.ts +3 -0
  7. package/dist/cli/commands/storage-commands.d.ts.map +1 -0
  8. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  9. package/dist/cli/helpers.d.ts +6 -0
  10. package/dist/cli/helpers.d.ts.map +1 -1
  11. package/dist/cli/index.js +3306 -445
  12. package/dist/contracts.js +289 -8
  13. package/dist/db/artifacts.d.ts.map +1 -1
  14. package/dist/db/builtin-templates.d.ts +0 -1
  15. package/dist/db/builtin-templates.d.ts.map +1 -1
  16. package/dist/db/database.d.ts.map +1 -1
  17. package/dist/db/schema.d.ts.map +1 -1
  18. package/dist/db/tags.d.ts +26 -0
  19. package/dist/db/tags.d.ts.map +1 -0
  20. package/dist/db/task-commits.d.ts +14 -6
  21. package/dist/db/task-commits.d.ts.map +1 -1
  22. package/dist/db/tasks.d.ts +1 -1
  23. package/dist/db/tasks.d.ts.map +1 -1
  24. package/dist/index.d.ts +32 -60
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2473 -200
  27. package/dist/lib/agent-adapter-docs.d.ts +1 -1
  28. package/dist/lib/agent-adapter-docs.d.ts.map +1 -1
  29. package/dist/lib/agent-coordination.d.ts.map +1 -1
  30. package/dist/lib/agent-run-dispatcher.d.ts +5 -2
  31. package/dist/lib/agent-run-dispatcher.d.ts.map +1 -1
  32. package/dist/lib/agent-workflow-demo.d.ts +1 -1
  33. package/dist/lib/agent-workflow-demo.d.ts.map +1 -1
  34. package/dist/lib/db-backup.d.ts.map +1 -1
  35. package/dist/lib/headless-boundaries.d.ts +2 -2
  36. package/dist/lib/headless-boundaries.d.ts.map +1 -1
  37. package/dist/lib/import-export-bridge.d.ts.map +1 -1
  38. package/dist/lib/inbox-intake.d.ts.map +1 -1
  39. package/dist/lib/issue-importers.d.ts.map +1 -1
  40. package/dist/lib/machine-topology.d.ts +1 -1
  41. package/dist/lib/machine-topology.d.ts.map +1 -1
  42. package/dist/lib/native-storage-status.d.ts +65 -0
  43. package/dist/lib/native-storage-status.d.ts.map +1 -0
  44. package/dist/lib/nl-intake.d.ts.map +1 -1
  45. package/dist/lib/notification-reminders.d.ts.map +1 -1
  46. package/dist/lib/plan-execution.d.ts +1 -0
  47. package/dist/lib/plan-execution.d.ts.map +1 -1
  48. package/dist/lib/release-checks.d.ts.map +1 -1
  49. package/dist/lib/resource-snapshots.d.ts.map +1 -1
  50. package/dist/lib/saved-views.d.ts.map +1 -1
  51. package/dist/lib/secret-redaction.d.ts +1 -1
  52. package/dist/lib/secret-redaction.d.ts.map +1 -1
  53. package/dist/lib/task-scheduling.d.ts +1 -1
  54. package/dist/lib/task-scheduling.d.ts.map +1 -1
  55. package/dist/lib/template-library.d.ts +1 -1
  56. package/dist/lib/template-library.d.ts.map +1 -1
  57. package/dist/lib/user-scaffolds.d.ts.map +1 -1
  58. package/dist/lib/verification-evidence.d.ts +4 -4
  59. package/dist/lib/verification-evidence.d.ts.map +1 -1
  60. package/dist/lib/verification-providers.d.ts +3 -3
  61. package/dist/lib/verification-providers.d.ts.map +1 -1
  62. package/dist/mcp/index.d.ts.map +1 -1
  63. package/dist/mcp/index.js +526 -50
  64. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  65. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  66. package/dist/registry.js +289 -8
  67. package/dist/release-provenance.json +7 -0
  68. package/dist/server/index.js +552 -52
  69. package/dist/server/serve.d.ts.map +1 -1
  70. package/dist/storage/config.d.ts +86 -0
  71. package/dist/storage/config.d.ts.map +1 -0
  72. package/dist/storage/factory.d.ts +17 -0
  73. package/dist/storage/factory.d.ts.map +1 -0
  74. package/dist/storage/hybrid.d.ts +26 -0
  75. package/dist/storage/hybrid.d.ts.map +1 -0
  76. package/dist/storage/index.d.ts +15 -0
  77. package/dist/storage/index.d.ts.map +1 -1
  78. package/dist/storage/local-sqlite.d.ts.map +1 -1
  79. package/dist/storage/postgres-adapter.d.ts +11 -0
  80. package/dist/storage/postgres-adapter.d.ts.map +1 -0
  81. package/dist/storage/postgres-sync.d.ts +44 -0
  82. package/dist/storage/postgres-sync.d.ts.map +1 -0
  83. package/dist/storage/s3-artifact-sync.d.ts +75 -0
  84. package/dist/storage/s3-artifact-sync.d.ts.map +1 -0
  85. package/dist/storage/s3-artifacts.d.ts +51 -0
  86. package/dist/storage/s3-artifacts.d.ts.map +1 -0
  87. package/dist/storage/sqlite-snapshot.d.ts +5 -0
  88. package/dist/storage/sqlite-snapshot.d.ts.map +1 -0
  89. package/dist/storage.d.ts +2 -1
  90. package/dist/storage.d.ts.map +1 -1
  91. package/dist/storage.js +2053 -10
  92. package/package.json +1 -1
  93. package/dist/cli/brains.d.ts +0 -3
  94. package/dist/cli/brains.d.ts.map +0 -1
  95. package/dist/db/file-locks.d.ts +0 -43
  96. package/dist/db/file-locks.d.ts.map +0 -1
  97. package/dist/db/project-agent-roles.d.ts +0 -34
  98. package/dist/db/project-agent-roles.d.ts.map +0 -1
  99. package/dist/db/task-claim.d.ts +0 -7
  100. package/dist/db/task-claim.d.ts.map +0 -1
  101. package/dist/db/task-workflow.d.ts +0 -7
  102. package/dist/db/task-workflow.d.ts.map +0 -1
  103. package/dist/lib/north-star.d.ts +0 -33
  104. package/dist/lib/north-star.d.ts.map +0 -1
  105. package/dist/lib/public-release-gate.d.ts +0 -57
  106. package/dist/lib/public-release-gate.d.ts.map +0 -1
  107. package/dist/lib/task-runner.d.ts +0 -101
  108. package/dist/lib/task-runner.d.ts.map +0 -1
  109. package/dist/mcp/tools/access-profiles.d.ts +0 -8
  110. package/dist/mcp/tools/access-profiles.d.ts.map +0 -1
  111. package/dist/mcp/tools/activity-audit.d.ts +0 -9
  112. package/dist/mcp/tools/activity-audit.d.ts.map +0 -1
  113. package/dist/mcp/tools/agent-adapter-docs.d.ts +0 -8
  114. package/dist/mcp/tools/agent-adapter-docs.d.ts.map +0 -1
  115. package/dist/mcp/tools/agent-coordination.d.ts +0 -9
  116. package/dist/mcp/tools/agent-coordination.d.ts.map +0 -1
  117. package/dist/mcp/tools/agent-runs.d.ts +0 -9
  118. package/dist/mcp/tools/agent-runs.d.ts.map +0 -1
  119. package/dist/mcp/tools/agent-workflow-demo.d.ts +0 -8
  120. package/dist/mcp/tools/agent-workflow-demo.d.ts.map +0 -1
  121. package/dist/mcp/tools/approval-gates.d.ts +0 -9
  122. package/dist/mcp/tools/approval-gates.d.ts.map +0 -1
  123. package/dist/mcp/tools/artifacts.d.ts +0 -9
  124. package/dist/mcp/tools/artifacts.d.ts.map +0 -1
  125. package/dist/mcp/tools/branch-work-plans.d.ts +0 -8
  126. package/dist/mcp/tools/branch-work-plans.d.ts.map +0 -1
  127. package/dist/mcp/tools/cli-docs.d.ts +0 -8
  128. package/dist/mcp/tools/cli-docs.d.ts.map +0 -1
  129. package/dist/mcp/tools/command-aliases.d.ts +0 -8
  130. package/dist/mcp/tools/command-aliases.d.ts.map +0 -1
  131. package/dist/mcp/tools/context-packs.d.ts +0 -9
  132. package/dist/mcp/tools/context-packs.d.ts.map +0 -1
  133. package/dist/mcp/tools/crypto.d.ts +0 -8
  134. package/dist/mcp/tools/crypto.d.ts.map +0 -1
  135. package/dist/mcp/tools/db-backup.d.ts +0 -8
  136. package/dist/mcp/tools/db-backup.d.ts.map +0 -1
  137. package/dist/mcp/tools/decision-records.d.ts +0 -9
  138. package/dist/mcp/tools/decision-records.d.ts.map +0 -1
  139. package/dist/mcp/tools/dependency-graph.d.ts +0 -9
  140. package/dist/mcp/tools/dependency-graph.d.ts.map +0 -1
  141. package/dist/mcp/tools/failure-triage.d.ts +0 -9
  142. package/dist/mcp/tools/failure-triage.d.ts.map +0 -1
  143. package/dist/mcp/tools/feature-manifest.d.ts +0 -8
  144. package/dist/mcp/tools/feature-manifest.d.ts.map +0 -1
  145. package/dist/mcp/tools/git-traceability.d.ts +0 -9
  146. package/dist/mcp/tools/git-traceability.d.ts.map +0 -1
  147. package/dist/mcp/tools/goal.d.ts +0 -9
  148. package/dist/mcp/tools/goal.d.ts.map +0 -1
  149. package/dist/mcp/tools/handoff-packets.d.ts +0 -9
  150. package/dist/mcp/tools/handoff-packets.d.ts.map +0 -1
  151. package/dist/mcp/tools/import-export-bridge.d.ts +0 -9
  152. package/dist/mcp/tools/import-export-bridge.d.ts.map +0 -1
  153. package/dist/mcp/tools/inbox-intake.d.ts +0 -9
  154. package/dist/mcp/tools/inbox-intake.d.ts.map +0 -1
  155. package/dist/mcp/tools/issue-importers.d.ts +0 -9
  156. package/dist/mcp/tools/issue-importers.d.ts.map +0 -1
  157. package/dist/mcp/tools/json-schemas.d.ts +0 -8
  158. package/dist/mcp/tools/json-schemas.d.ts.map +0 -1
  159. package/dist/mcp/tools/machine-topology.d.ts +0 -8
  160. package/dist/mcp/tools/machine-topology.d.ts.map +0 -1
  161. package/dist/mcp/tools/mention-resolver.d.ts +0 -9
  162. package/dist/mcp/tools/mention-resolver.d.ts.map +0 -1
  163. package/dist/mcp/tools/nl-intake.d.ts +0 -9
  164. package/dist/mcp/tools/nl-intake.d.ts.map +0 -1
  165. package/dist/mcp/tools/notification-reminders.d.ts +0 -9
  166. package/dist/mcp/tools/notification-reminders.d.ts.map +0 -1
  167. package/dist/mcp/tools/parity.d.ts +0 -8
  168. package/dist/mcp/tools/parity.d.ts.map +0 -1
  169. package/dist/mcp/tools/plan-execution.d.ts +0 -9
  170. package/dist/mcp/tools/plan-execution.d.ts.map +0 -1
  171. package/dist/mcp/tools/policy-packs.d.ts +0 -9
  172. package/dist/mcp/tools/policy-packs.d.ts.map +0 -1
  173. package/dist/mcp/tools/project-bootstrap.d.ts +0 -8
  174. package/dist/mcp/tools/project-bootstrap.d.ts.map +0 -1
  175. package/dist/mcp/tools/release-checks.d.ts +0 -8
  176. package/dist/mcp/tools/release-checks.d.ts.map +0 -1
  177. package/dist/mcp/tools/release-notes.d.ts +0 -9
  178. package/dist/mcp/tools/release-notes.d.ts.map +0 -1
  179. package/dist/mcp/tools/report-exports.d.ts +0 -9
  180. package/dist/mcp/tools/report-exports.d.ts.map +0 -1
  181. package/dist/mcp/tools/resource-subscriptions.d.ts +0 -8
  182. package/dist/mcp/tools/resource-subscriptions.d.ts.map +0 -1
  183. package/dist/mcp/tools/run-records.d.ts +0 -9
  184. package/dist/mcp/tools/run-records.d.ts.map +0 -1
  185. package/dist/mcp/tools/sandbox.d.ts +0 -8
  186. package/dist/mcp/tools/sandbox.d.ts.map +0 -1
  187. package/dist/mcp/tools/saved-views.d.ts +0 -9
  188. package/dist/mcp/tools/saved-views.d.ts.map +0 -1
  189. package/dist/mcp/tools/secret-redaction.d.ts +0 -8
  190. package/dist/mcp/tools/secret-redaction.d.ts.map +0 -1
  191. package/dist/mcp/tools/task-dedupe.d.ts +0 -9
  192. package/dist/mcp/tools/task-dedupe.d.ts.map +0 -1
  193. package/dist/mcp/tools/task-scheduling.d.ts +0 -9
  194. package/dist/mcp/tools/task-scheduling.d.ts.map +0 -1
  195. package/dist/mcp/tools/template-library.d.ts +0 -8
  196. package/dist/mcp/tools/template-library.d.ts.map +0 -1
  197. package/dist/mcp/tools/terminal-notifications.d.ts +0 -9
  198. package/dist/mcp/tools/terminal-notifications.d.ts.map +0 -1
  199. package/dist/mcp/tools/todos-md.d.ts +0 -8
  200. package/dist/mcp/tools/todos-md.d.ts.map +0 -1
  201. package/dist/mcp/tools/user-scaffolds.d.ts +0 -8
  202. package/dist/mcp/tools/user-scaffolds.d.ts.map +0 -1
  203. package/dist/mcp/tools/verification.d.ts +0 -9
  204. package/dist/mcp/tools/verification.d.ts.map +0 -1
  205. package/dist/mcp/tools/webhooks.d.ts +0 -8
  206. package/dist/mcp/tools/webhooks.d.ts.map +0 -1
  207. package/dist/mcp/tools/workspace-trust.d.ts +0 -8
  208. package/dist/mcp/tools/workspace-trust.d.ts.map +0 -1
  209. package/dist/test/no-network.d.ts +0 -7
  210. package/dist/test/no-network.d.ts.map +0 -1
package/dist/storage.js CHANGED
@@ -1353,6 +1353,15 @@ function ensureSchema(db) {
1353
1353
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1354
1354
  tag TEXT NOT NULL, PRIMARY KEY (task_id, tag)
1355
1355
  )`);
1356
+ ensureTable("tags", `
1357
+ CREATE TABLE tags (
1358
+ id TEXT PRIMARY KEY,
1359
+ name TEXT NOT NULL UNIQUE,
1360
+ color TEXT,
1361
+ description TEXT,
1362
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1363
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1364
+ )`);
1356
1365
  ensureTable("task_dependencies", `
1357
1366
  CREATE TABLE task_dependencies (
1358
1367
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
@@ -1566,6 +1575,36 @@ function ensureSchema(db) {
1566
1575
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_task ON task_git_refs(task_id)");
1567
1576
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_lookup ON task_git_refs(ref_type, name)");
1568
1577
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_url ON task_git_refs(url)");
1578
+ ensureTable("task_commits", `
1579
+ CREATE TABLE task_commits (
1580
+ id TEXT PRIMARY KEY,
1581
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1582
+ sha TEXT NOT NULL,
1583
+ message TEXT,
1584
+ author TEXT,
1585
+ files_changed TEXT,
1586
+ committed_at TEXT,
1587
+ branch TEXT,
1588
+ pr_url TEXT,
1589
+ pr_number INTEGER,
1590
+ pr_state TEXT,
1591
+ ci_snapshot TEXT,
1592
+ release_tag TEXT,
1593
+ repo_path TEXT,
1594
+ traceability TEXT DEFAULT '{}',
1595
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1596
+ UNIQUE(task_id, sha)
1597
+ )`);
1598
+ ensureColumn("task_commits", "branch", "TEXT");
1599
+ ensureColumn("task_commits", "pr_url", "TEXT");
1600
+ ensureColumn("task_commits", "pr_number", "INTEGER");
1601
+ ensureColumn("task_commits", "pr_state", "TEXT");
1602
+ ensureColumn("task_commits", "ci_snapshot", "TEXT");
1603
+ ensureColumn("task_commits", "release_tag", "TEXT");
1604
+ ensureColumn("task_commits", "repo_path", "TEXT");
1605
+ ensureColumn("task_commits", "traceability", "TEXT DEFAULT '{}'");
1606
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_commits_task ON task_commits(task_id)");
1607
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_commits_sha ON task_commits(sha)");
1569
1608
  ensureTable("task_verifications", `
1570
1609
  CREATE TABLE task_verifications (
1571
1610
  id TEXT PRIMARY KEY,
@@ -1750,6 +1789,9 @@ function ensureSchema(db) {
1750
1789
  ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
1751
1790
  ensureColumn("tasks", "retry_after", "TEXT");
1752
1791
  ensureColumn("tasks", "sla_minutes", "INTEGER");
1792
+ ensureColumn("tasks", "scheduled_start_at", "TEXT");
1793
+ ensureColumn("tasks", "priority_score", "INTEGER");
1794
+ ensureColumn("tasks", "priority_reason", "TEXT");
1753
1795
  ensureColumn("tasks", "archived_at", "TEXT");
1754
1796
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
1755
1797
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
@@ -1872,6 +1914,7 @@ function ensureSchema(db) {
1872
1914
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug)");
1873
1915
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag)");
1874
1916
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id)");
1917
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)");
1875
1918
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
1876
1919
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
1877
1920
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
@@ -2009,6 +2052,217 @@ function ensureSchema(db) {
2009
2052
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_cycles_dates ON cycles(start_date, end_date)");
2010
2053
  ensureColumn("tasks", "cycle_id", "TEXT REFERENCES cycles(id) ON DELETE SET NULL");
2011
2054
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_cycle ON tasks(cycle_id) WHERE cycle_id IS NOT NULL");
2055
+ ensureTable("labels", `
2056
+ CREATE TABLE labels (
2057
+ id TEXT PRIMARY KEY,
2058
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
2059
+ name TEXT NOT NULL,
2060
+ color TEXT,
2061
+ description TEXT,
2062
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2063
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
2064
+ UNIQUE(project_id, name)
2065
+ )`);
2066
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_labels_project ON labels(project_id)");
2067
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_labels_name ON labels(name)");
2068
+ ensureTable("task_labels", `
2069
+ CREATE TABLE task_labels (
2070
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
2071
+ label_id TEXT NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
2072
+ PRIMARY KEY (task_id, label_id)
2073
+ )`);
2074
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_labels_task ON task_labels(task_id)");
2075
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_labels_label ON task_labels(label_id)");
2076
+ ensureTable("custom_field_definitions", `
2077
+ CREATE TABLE custom_field_definitions (
2078
+ id TEXT PRIMARY KEY,
2079
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
2080
+ name TEXT NOT NULL,
2081
+ slug TEXT NOT NULL,
2082
+ field_type TEXT NOT NULL CHECK(field_type IN ('text', 'number', 'boolean', 'date', 'enum')),
2083
+ options TEXT DEFAULT '[]',
2084
+ required INTEGER NOT NULL DEFAULT 0,
2085
+ default_value TEXT,
2086
+ sort_order INTEGER NOT NULL DEFAULT 0,
2087
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2088
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
2089
+ UNIQUE(project_id, slug)
2090
+ )`);
2091
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_custom_fields_project ON custom_field_definitions(project_id)");
2092
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_custom_fields_slug ON custom_field_definitions(slug)");
2093
+ ensureTable("task_custom_field_values", `
2094
+ CREATE TABLE task_custom_field_values (
2095
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
2096
+ field_id TEXT NOT NULL REFERENCES custom_field_definitions(id) ON DELETE CASCADE,
2097
+ value TEXT,
2098
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
2099
+ PRIMARY KEY (task_id, field_id)
2100
+ )`);
2101
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_custom_values_task ON task_custom_field_values(task_id)");
2102
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_custom_values_field ON task_custom_field_values(field_id)");
2103
+ ensureTable("saved_views", `
2104
+ CREATE TABLE saved_views (
2105
+ id TEXT PRIMARY KEY,
2106
+ name TEXT NOT NULL UNIQUE,
2107
+ slug TEXT NOT NULL UNIQUE,
2108
+ entity_type TEXT NOT NULL DEFAULT 'task',
2109
+ filters TEXT NOT NULL DEFAULT '{}',
2110
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2111
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2112
+ )`);
2113
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_saved_views_slug ON saved_views(slug)");
2114
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_saved_views_entity ON saved_views(entity_type)");
2115
+ ensureTable("decision_records", `
2116
+ CREATE TABLE decision_records (
2117
+ id TEXT PRIMARY KEY,
2118
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
2119
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
2120
+ plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
2121
+ agent_id TEXT,
2122
+ sequence_num INTEGER NOT NULL,
2123
+ short_ref TEXT NOT NULL UNIQUE,
2124
+ title TEXT NOT NULL,
2125
+ status TEXT NOT NULL DEFAULT 'proposed' CHECK(status IN ('proposed', 'accepted', 'deprecated', 'superseded', 'rejected')),
2126
+ context TEXT,
2127
+ decision TEXT NOT NULL,
2128
+ consequences TEXT,
2129
+ alternatives TEXT DEFAULT '[]',
2130
+ tags TEXT DEFAULT '[]',
2131
+ supersedes_id TEXT REFERENCES decision_records(id) ON DELETE SET NULL,
2132
+ superseded_by_id TEXT REFERENCES decision_records(id) ON DELETE SET NULL,
2133
+ metadata TEXT DEFAULT '{}',
2134
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2135
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2136
+ )`);
2137
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_project ON decision_records(project_id)");
2138
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_task ON decision_records(task_id)");
2139
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_plan ON decision_records(plan_id)");
2140
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_status ON decision_records(status)");
2141
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_short_ref ON decision_records(short_ref)");
2142
+ ensureTable("knowledge_snapshots", `
2143
+ CREATE TABLE knowledge_snapshots (
2144
+ id TEXT PRIMARY KEY,
2145
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
2146
+ title TEXT NOT NULL,
2147
+ summary TEXT,
2148
+ content_hash TEXT NOT NULL,
2149
+ snapshot TEXT NOT NULL,
2150
+ decision_ids TEXT DEFAULT '[]',
2151
+ topics TEXT DEFAULT '[]',
2152
+ source TEXT NOT NULL DEFAULT 'auto' CHECK(source IN ('manual', 'auto', 'import')),
2153
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2154
+ )`);
2155
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_knowledge_snapshots_project ON knowledge_snapshots(project_id)");
2156
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_knowledge_snapshots_hash ON knowledge_snapshots(content_hash)");
2157
+ ensureTable("verification_records", `
2158
+ CREATE TABLE verification_records (
2159
+ id TEXT PRIMARY KEY,
2160
+ task_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
2161
+ provider_name TEXT NOT NULL,
2162
+ provider_type TEXT NOT NULL,
2163
+ status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
2164
+ summary TEXT,
2165
+ evidence TEXT DEFAULT '{}',
2166
+ artifact_id TEXT,
2167
+ started_at TEXT,
2168
+ completed_at TEXT,
2169
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2170
+ )`);
2171
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_verification_records_task ON verification_records(task_id)");
2172
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_verification_records_status ON verification_records(status)");
2173
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_verification_records_created ON verification_records(created_at)");
2174
+ ensureTable("task_leases", `
2175
+ CREATE TABLE task_leases (
2176
+ task_id TEXT PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE,
2177
+ agent_id TEXT NOT NULL,
2178
+ acquired_at TEXT NOT NULL,
2179
+ expires_at TEXT NOT NULL,
2180
+ heartbeat_at TEXT,
2181
+ steal_count INTEGER NOT NULL DEFAULT 0,
2182
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2183
+ )`);
2184
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_leases_agent ON task_leases(agent_id)");
2185
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_leases_expires ON task_leases(expires_at)");
2186
+ ensureTable("reminder_preferences", `
2187
+ CREATE TABLE reminder_preferences (
2188
+ id TEXT PRIMARY KEY,
2189
+ due_soon_hours INTEGER NOT NULL DEFAULT 24,
2190
+ sla_warning_minutes INTEGER NOT NULL DEFAULT 30,
2191
+ enabled INTEGER NOT NULL DEFAULT 1,
2192
+ desktop_notify INTEGER NOT NULL DEFAULT 0,
2193
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2194
+ )`);
2195
+ ensureTable("notification_reminders", `
2196
+ CREATE TABLE notification_reminders (
2197
+ id TEXT PRIMARY KEY,
2198
+ task_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
2199
+ reminder_type TEXT NOT NULL CHECK(reminder_type IN ('due_soon', 'due_overdue', 'sla_warning', 'sla_breach', 'custom')),
2200
+ title TEXT NOT NULL,
2201
+ message TEXT,
2202
+ trigger_at TEXT NOT NULL,
2203
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'fired', 'dismissed', 'snoozed')),
2204
+ snoozed_until TEXT,
2205
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
2206
+ agent_id TEXT,
2207
+ priority TEXT NOT NULL DEFAULT 'medium',
2208
+ metadata TEXT DEFAULT '{}',
2209
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2210
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
2211
+ fired_at TEXT,
2212
+ dismissed_at TEXT
2213
+ )`);
2214
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_task ON notification_reminders(task_id)");
2215
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_status ON notification_reminders(status)");
2216
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_trigger ON notification_reminders(trigger_at)");
2217
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_project ON notification_reminders(project_id)");
2218
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_agent ON notification_reminders(agent_id)");
2219
+ ensureTable("run_records", `
2220
+ CREATE TABLE run_records (
2221
+ id TEXT PRIMARY KEY,
2222
+ agent_run_id TEXT,
2223
+ agent_id TEXT,
2224
+ objective TEXT,
2225
+ plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
2226
+ claimed_task_ids TEXT DEFAULT '[]',
2227
+ commands TEXT DEFAULT '[]',
2228
+ stdout_summary TEXT,
2229
+ stderr_summary TEXT,
2230
+ files_touched TEXT DEFAULT '[]',
2231
+ verification_results TEXT DEFAULT '[]',
2232
+ artifact_ids TEXT DEFAULT '[]',
2233
+ status_transitions TEXT DEFAULT '[]',
2234
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'failed', 'archived')),
2235
+ replay_bundle TEXT,
2236
+ metadata TEXT DEFAULT '{}',
2237
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
2238
+ completed_at TEXT,
2239
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2240
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2241
+ )`);
2242
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_agent_run ON run_records(agent_run_id)");
2243
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_agent ON run_records(agent_id)");
2244
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_plan ON run_records(plan_id)");
2245
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_status ON run_records(status)");
2246
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_started ON run_records(started_at)");
2247
+ ensureTable("activity_log", `
2248
+ CREATE TABLE activity_log (
2249
+ id TEXT PRIMARY KEY,
2250
+ entity_type TEXT NOT NULL,
2251
+ entity_id TEXT NOT NULL,
2252
+ action TEXT NOT NULL,
2253
+ field TEXT,
2254
+ old_value TEXT,
2255
+ new_value TEXT,
2256
+ actor_id TEXT,
2257
+ session_id TEXT,
2258
+ machine_id TEXT,
2259
+ metadata TEXT DEFAULT '{}',
2260
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2261
+ )`);
2262
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_activity_log_entity ON activity_log(entity_type, entity_id)");
2263
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_activity_log_actor ON activity_log(actor_id)");
2264
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_activity_log_action ON activity_log(action)");
2265
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_activity_log_created ON activity_log(created_at)");
2012
2266
  ensureTable("api_keys", `
2013
2267
  CREATE TABLE api_keys (
2014
2268
  id TEXT PRIMARY KEY,
@@ -2445,11 +2699,17 @@ function ensureDir(filePath) {
2445
2699
  }
2446
2700
  }
2447
2701
  function getDatabase(dbPath) {
2448
- if (_db)
2449
- return _db;
2450
2702
  const path = dbPath || getDbPath();
2703
+ if (_db && _dbPath === path)
2704
+ return _db;
2705
+ if (_db && _dbPath !== path) {
2706
+ _db.close();
2707
+ _db = null;
2708
+ _dbPath = null;
2709
+ }
2451
2710
  ensureDir(path);
2452
2711
  _db = new Database(path);
2712
+ _dbPath = path;
2453
2713
  _db.run("PRAGMA journal_mode = WAL");
2454
2714
  _db.run("PRAGMA busy_timeout = 5000");
2455
2715
  _db.run("PRAGMA foreign_keys = ON");
@@ -2462,10 +2722,12 @@ function closeDatabase() {
2462
2722
  if (_db) {
2463
2723
  _db.close();
2464
2724
  _db = null;
2725
+ _dbPath = null;
2465
2726
  }
2466
2727
  }
2467
2728
  function resetDatabase() {
2468
2729
  _db = null;
2730
+ _dbPath = null;
2469
2731
  }
2470
2732
  function now() {
2471
2733
  return new Date().toISOString();
@@ -2526,7 +2788,7 @@ function resolvePartialId(db, table, partialId) {
2526
2788
  }
2527
2789
  return null;
2528
2790
  }
2529
- var LOCK_EXPIRY_MINUTES = 30, _db = null, ALLOWED_TABLES;
2791
+ var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
2530
2792
  var init_database = __esm(() => {
2531
2793
  init_schema();
2532
2794
  init_machines();
@@ -2888,9 +3150,10 @@ function redactExportRecord(record) {
2888
3150
  function getDefaultSecretPatterns() {
2889
3151
  return DEFAULT_PATTERNS.map((p) => ({ name: p.name, source: p.pattern.source }));
2890
3152
  }
2891
- var SECRET_REDACTION_SCHEMA = "todos.secret_redaction.v1", REDACTION_PLACEHOLDER = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
3153
+ var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
2892
3154
  var init_secret_redaction = __esm(() => {
2893
3155
  init_redaction();
3156
+ SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
2894
3157
  DEFAULT_PATTERNS = [
2895
3158
  { name: "openai_sk", pattern: /\bsk-[a-zA-Z0-9]{10,}\b/g },
2896
3159
  { name: "github_pat", pattern: /\bghp_[a-zA-Z0-9]{20,}\b/g },
@@ -3095,6 +3358,168 @@ var init_activity_audit = __esm(() => {
3095
3358
  ];
3096
3359
  });
3097
3360
 
3361
+ // src/storage/config.ts
3362
+ var TODOS_STORAGE_TABLES = [
3363
+ "todos_sync_records",
3364
+ "todos_sync_cursors"
3365
+ ];
3366
+ var STORAGE_TABLES = TODOS_STORAGE_TABLES;
3367
+ var TODOS_STORAGE_ENV = {
3368
+ mode: "HASNA_TODOS_STORAGE_MODE",
3369
+ databaseUrl: "HASNA_TODOS_DATABASE_URL",
3370
+ databaseSsl: "HASNA_TODOS_DATABASE_SSL",
3371
+ databaseSchema: "HASNA_TODOS_DATABASE_SCHEMA",
3372
+ s3Bucket: "HASNA_TODOS_S3_BUCKET",
3373
+ s3Prefix: "HASNA_TODOS_S3_PREFIX",
3374
+ awsRegion: "HASNA_TODOS_AWS_REGION",
3375
+ s3Endpoint: "HASNA_TODOS_S3_ENDPOINT",
3376
+ s3ForcePathStyle: "HASNA_TODOS_S3_FORCE_PATH_STYLE",
3377
+ s3AccessKeyId: "HASNA_TODOS_S3_ACCESS_KEY_ID",
3378
+ s3SecretAccessKey: "HASNA_TODOS_S3_SECRET_ACCESS_KEY",
3379
+ s3SessionToken: "HASNA_TODOS_S3_SESSION_TOKEN",
3380
+ syncBatchSize: "HASNA_TODOS_SYNC_BATCH_SIZE",
3381
+ syncDryRun: "HASNA_TODOS_SYNC_DRY_RUN"
3382
+ };
3383
+ var TODOS_STORAGE_FALLBACK_ENV = {
3384
+ mode: "TODOS_STORAGE_MODE",
3385
+ databaseUrl: "TODOS_DATABASE_URL",
3386
+ databaseSsl: "TODOS_DATABASE_SSL",
3387
+ databaseSchema: "TODOS_DATABASE_SCHEMA",
3388
+ s3Bucket: "TODOS_S3_BUCKET",
3389
+ s3Prefix: "TODOS_S3_PREFIX",
3390
+ awsRegion: "TODOS_AWS_REGION",
3391
+ s3Endpoint: "TODOS_S3_ENDPOINT",
3392
+ s3ForcePathStyle: "TODOS_S3_FORCE_PATH_STYLE",
3393
+ s3AccessKeyId: "TODOS_S3_ACCESS_KEY_ID",
3394
+ s3SecretAccessKey: "TODOS_S3_SECRET_ACCESS_KEY",
3395
+ s3SessionToken: "TODOS_S3_SESSION_TOKEN",
3396
+ syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
3397
+ syncDryRun: "TODOS_SYNC_DRY_RUN"
3398
+ };
3399
+ var CANONICAL_TODOS_RDS_CLUSTER = "hasna-xyz-infra-apps-prod-postgres";
3400
+ var CANONICAL_TODOS_RDS_DATABASE = "todos";
3401
+ var CANONICAL_TODOS_RDS_RUNTIME_PATH = "hasna/xyz/opensource/todos/prod/rds";
3402
+ function getCanonicalTodosRdsConfig() {
3403
+ return {
3404
+ cluster: CANONICAL_TODOS_RDS_CLUSTER,
3405
+ database: CANONICAL_TODOS_RDS_DATABASE,
3406
+ runtimeSecretPath: CANONICAL_TODOS_RDS_RUNTIME_PATH,
3407
+ primaryEnv: TODOS_STORAGE_ENV.databaseUrl,
3408
+ fallbackEnv: TODOS_STORAGE_FALLBACK_ENV.databaseUrl
3409
+ };
3410
+ }
3411
+ function loadTodosStorageConfig(env = process.env) {
3412
+ const mode = getTodosStorageMode(env);
3413
+ const databaseUrl = getTodosStorageDatabaseUrl(env);
3414
+ const bucket = readStorageEnv(env, "s3Bucket").value;
3415
+ const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
3416
+ const region = readStorageEnv(env, "awsRegion").value;
3417
+ const endpoint = readStorageEnv(env, "s3Endpoint").value;
3418
+ const schema = readStorageEnv(env, "databaseSchema").value;
3419
+ return {
3420
+ service: "todos",
3421
+ mode,
3422
+ ...databaseUrl ? {
3423
+ database: {
3424
+ provider: "postgres",
3425
+ url: databaseUrl,
3426
+ ssl: parseBoolean(readStorageEnv(env, "databaseSsl").value, true),
3427
+ ...schema ? { schema } : {}
3428
+ }
3429
+ } : {},
3430
+ ...bucket ? {
3431
+ objectStorage: {
3432
+ provider: "s3",
3433
+ bucket,
3434
+ prefix,
3435
+ ...region ? { region } : {},
3436
+ ...endpoint ? { endpoint } : {},
3437
+ forcePathStyle: parseBoolean(readStorageEnv(env, "s3ForcePathStyle").value, false)
3438
+ }
3439
+ } : {},
3440
+ sync: {
3441
+ batchSize: parsePositiveInteger(readStorageEnv(env, "syncBatchSize").value, 500),
3442
+ dryRun: parseBoolean(readStorageEnv(env, "syncDryRun").value, false)
3443
+ }
3444
+ };
3445
+ }
3446
+ function loadStorageConfig(env = process.env) {
3447
+ return loadTodosStorageConfig(env);
3448
+ }
3449
+ function isTodosRemoteStorageEnabled(config) {
3450
+ return config.mode === "remote" || config.mode === "hybrid";
3451
+ }
3452
+ function assertTodosRemoteStorageConfig(config) {
3453
+ if (!isTodosRemoteStorageEnabled(config))
3454
+ return;
3455
+ if (!config.database?.url) {
3456
+ throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
3457
+ }
3458
+ }
3459
+ function parseStorageMode(value) {
3460
+ const normalized = clean(value)?.toLowerCase();
3461
+ if (!normalized)
3462
+ return "local";
3463
+ if (normalized === "local" || normalized === "remote" || normalized === "hybrid")
3464
+ return normalized;
3465
+ throw new Error(`${TODOS_STORAGE_ENV.mode} must be local, remote, or hybrid`);
3466
+ }
3467
+ function getTodosStorageMode(env = process.env) {
3468
+ return parseStorageMode(readStorageEnv(env, "mode").value);
3469
+ }
3470
+ function getStorageMode(env = process.env) {
3471
+ return getTodosStorageMode(env);
3472
+ }
3473
+ function getTodosStorageDatabaseEnv(env = process.env) {
3474
+ return readStorageEnv(env, "databaseUrl").name;
3475
+ }
3476
+ function getStorageDatabaseEnv(env = process.env) {
3477
+ return getTodosStorageDatabaseEnv(env);
3478
+ }
3479
+ function getTodosStorageDatabaseUrl(env = process.env) {
3480
+ return readStorageEnv(env, "databaseUrl").value;
3481
+ }
3482
+ function getStorageDatabaseUrl(env = process.env) {
3483
+ return getTodosStorageDatabaseUrl(env);
3484
+ }
3485
+ function getTodosStorageEnvName(env, key) {
3486
+ return readStorageEnv(env, key).name;
3487
+ }
3488
+ function readStorageEnv(env, key) {
3489
+ const primaryName = TODOS_STORAGE_ENV[key];
3490
+ const primaryValue = clean(env[primaryName]);
3491
+ if (primaryValue !== undefined)
3492
+ return { name: primaryName, value: primaryValue };
3493
+ const fallbackName = TODOS_STORAGE_FALLBACK_ENV[key];
3494
+ const fallbackValue = clean(env[fallbackName]);
3495
+ if (fallbackValue !== undefined)
3496
+ return { name: fallbackName, value: fallbackValue };
3497
+ return { name: primaryName };
3498
+ }
3499
+ function clean(value) {
3500
+ const trimmed = value?.trim();
3501
+ return trimmed ? trimmed : undefined;
3502
+ }
3503
+ function parseBoolean(value, fallback) {
3504
+ const normalized = clean(value)?.toLowerCase();
3505
+ if (!normalized)
3506
+ return fallback;
3507
+ if (["1", "true", "yes", "on"].includes(normalized))
3508
+ return true;
3509
+ if (["0", "false", "no", "off"].includes(normalized))
3510
+ return false;
3511
+ throw new Error(`Expected boolean env value, got ${value}`);
3512
+ }
3513
+ function parsePositiveInteger(value, fallback) {
3514
+ const normalized = clean(value);
3515
+ if (!normalized)
3516
+ return fallback;
3517
+ const parsed = Number.parseInt(normalized, 10);
3518
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
3519
+ throw new Error(`${TODOS_STORAGE_ENV.syncBatchSize} must be a positive integer`);
3520
+ }
3521
+ return parsed;
3522
+ }
3098
3523
  // src/db/task-crud.ts
3099
3524
  init_types();
3100
3525
  init_database();
@@ -7177,7 +7602,7 @@ function storeArtifactFile(input) {
7177
7602
  }
7178
7603
  return { sourcePath, localPath, contentHash, mimeType, sizeBytes: buffer.length };
7179
7604
  }
7180
- function deleteStoredArtifactFile(localPath, storageMode, _dbPath) {
7605
+ function deleteStoredArtifactFile(localPath, storageMode, _dbPath2) {
7181
7606
  if (storageMode === "reference")
7182
7607
  return false;
7183
7608
  if (!localPath || !existsSync6(localPath))
@@ -7432,11 +7857,19 @@ function getTaskVerifications(taskId, db) {
7432
7857
  }
7433
7858
  function getTaskTraceability(taskId, db) {
7434
7859
  const d = db || getDatabase();
7860
+ const commits = getTaskCommits(taskId, d);
7861
+ const gitRefs = getTaskGitRefs(taskId, d);
7435
7862
  return {
7436
7863
  task_id: taskId,
7437
- commits: getTaskCommits(taskId, d),
7438
- git_refs: getTaskGitRefs(taskId, d),
7439
- verifications: getTaskVerifications(taskId, d)
7864
+ commits,
7865
+ git_refs: gitRefs,
7866
+ verifications: getTaskVerifications(taskId, d),
7867
+ branches: Array.from(new Set([
7868
+ ...commits.map((commit) => commit.branch).filter((branch) => Boolean(branch)),
7869
+ ...gitRefs.filter((ref) => ref.ref_type === "branch").map((ref) => ref.name)
7870
+ ])).sort(),
7871
+ release_tags: Array.from(new Set(commits.map((commit) => commit.release_tag).filter((tag) => Boolean(tag)))).sort(),
7872
+ pull_requests: commits.filter((commit) => commit.pr_url).map((commit) => ({ url: commit.pr_url, state: commit.pr_state, number: commit.pr_number }))
7440
7873
  };
7441
7874
  }
7442
7875
 
@@ -8795,6 +9228,260 @@ function ensureTaskList(name, slug, projectId, db) {
8795
9228
 
8796
9229
  // src/storage/local-sqlite.ts
8797
9230
  init_database();
9231
+
9232
+ // src/storage/sqlite-snapshot.ts
9233
+ init_database();
9234
+ var PROJECT_COLUMNS = [
9235
+ "id",
9236
+ "name",
9237
+ "path",
9238
+ "description",
9239
+ "task_list_id",
9240
+ "task_prefix",
9241
+ "task_counter",
9242
+ "created_at",
9243
+ "updated_at",
9244
+ "machine_id",
9245
+ "synced_at"
9246
+ ];
9247
+ var TASK_LIST_COLUMNS = [
9248
+ "id",
9249
+ "project_id",
9250
+ "slug",
9251
+ "name",
9252
+ "description",
9253
+ "metadata",
9254
+ "created_at",
9255
+ "updated_at",
9256
+ "machine_id",
9257
+ "synced_at"
9258
+ ];
9259
+ var PLAN_COLUMNS = [
9260
+ "id",
9261
+ "project_id",
9262
+ "task_list_id",
9263
+ "agent_id",
9264
+ "name",
9265
+ "description",
9266
+ "status",
9267
+ "created_at",
9268
+ "updated_at",
9269
+ "machine_id",
9270
+ "synced_at"
9271
+ ];
9272
+ var AGENT_COLUMNS = [
9273
+ "id",
9274
+ "name",
9275
+ "description",
9276
+ "role",
9277
+ "title",
9278
+ "level",
9279
+ "permissions",
9280
+ "capabilities",
9281
+ "reports_to",
9282
+ "org_id",
9283
+ "metadata",
9284
+ "status",
9285
+ "created_at",
9286
+ "last_seen_at",
9287
+ "session_id",
9288
+ "working_dir",
9289
+ "active_project_id",
9290
+ "machine_id",
9291
+ "synced_at"
9292
+ ];
9293
+ var TEMPLATE_COLUMNS = [
9294
+ "id",
9295
+ "name",
9296
+ "title_pattern",
9297
+ "description",
9298
+ "priority",
9299
+ "tags",
9300
+ "variables",
9301
+ "project_id",
9302
+ "plan_id",
9303
+ "metadata",
9304
+ "version",
9305
+ "created_at",
9306
+ "machine_id",
9307
+ "synced_at"
9308
+ ];
9309
+ var TASK_COLUMNS = [
9310
+ "id",
9311
+ "short_id",
9312
+ "project_id",
9313
+ "parent_id",
9314
+ "plan_id",
9315
+ "task_list_id",
9316
+ "title",
9317
+ "description",
9318
+ "status",
9319
+ "priority",
9320
+ "agent_id",
9321
+ "assigned_to",
9322
+ "session_id",
9323
+ "working_dir",
9324
+ "tags",
9325
+ "metadata",
9326
+ "version",
9327
+ "locked_by",
9328
+ "locked_at",
9329
+ "created_at",
9330
+ "updated_at",
9331
+ "started_at",
9332
+ "completed_at",
9333
+ "due_at",
9334
+ "estimated_minutes",
9335
+ "actual_minutes",
9336
+ "requires_approval",
9337
+ "approved_by",
9338
+ "approved_at",
9339
+ "recurrence_rule",
9340
+ "recurrence_parent_id",
9341
+ "spawns_template_id",
9342
+ "confidence",
9343
+ "reason",
9344
+ "spawned_from_session",
9345
+ "assigned_by",
9346
+ "assigned_from_project",
9347
+ "task_type",
9348
+ "cost_tokens",
9349
+ "cost_usd",
9350
+ "delegated_from",
9351
+ "delegation_depth",
9352
+ "retry_count",
9353
+ "max_retries",
9354
+ "retry_after",
9355
+ "sla_minutes",
9356
+ "runner_id",
9357
+ "runner_started_at",
9358
+ "runner_completed_at",
9359
+ "current_step",
9360
+ "total_steps",
9361
+ "cycle_id",
9362
+ "machine_id",
9363
+ "synced_at",
9364
+ "archived_at"
9365
+ ];
9366
+ var AUDIT_COLUMNS = [
9367
+ "id",
9368
+ "task_id",
9369
+ "action",
9370
+ "field",
9371
+ "old_value",
9372
+ "new_value",
9373
+ "agent_id",
9374
+ "created_at",
9375
+ "machine_id"
9376
+ ];
9377
+ var JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables"]);
9378
+ var BOOLEAN_COLUMNS = new Set(["requires_approval"]);
9379
+ function exportSqliteTodosStorageSnapshot(db) {
9380
+ const d = db ?? getDatabase();
9381
+ return {
9382
+ exportedAt: new Date().toISOString(),
9383
+ source: "sqlite",
9384
+ tasks: listTasks({ include_archived: true }, d),
9385
+ projects: listProjects(d),
9386
+ plans: listPlans(undefined, d),
9387
+ agents: listAgents({ include_archived: true }, d),
9388
+ taskLists: listTaskLists(undefined, d),
9389
+ templates: listTemplates(d),
9390
+ auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d)
9391
+ };
9392
+ }
9393
+ function importSqliteTodosStorageSnapshot(snapshot, db) {
9394
+ const d = db ?? getDatabase();
9395
+ const result = {
9396
+ inserted: 0,
9397
+ updated: 0,
9398
+ skipped: 0,
9399
+ errors: []
9400
+ };
9401
+ const applyRows = (table, columns, rows, updateClockColumn, afterUpsert) => {
9402
+ for (const row of rows) {
9403
+ try {
9404
+ const record = asRecord(row);
9405
+ const state = upsertById(d, table, columns, record, updateClockColumn);
9406
+ if (state === "inserted")
9407
+ result.inserted += 1;
9408
+ else if (state === "updated")
9409
+ result.updated += 1;
9410
+ else
9411
+ result.skipped += 1;
9412
+ afterUpsert?.(record, state !== "skipped");
9413
+ } catch (error) {
9414
+ result.errors.push(error instanceof Error ? error.message : String(error));
9415
+ }
9416
+ }
9417
+ };
9418
+ applyRows("projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
9419
+ applyRows("agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
9420
+ applyRows("task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
9421
+ applyRows("plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
9422
+ applyRows("task_templates", TEMPLATE_COLUMNS, snapshot.templates);
9423
+ applyRows("tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
9424
+ if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
9425
+ replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
9426
+ }
9427
+ });
9428
+ applyRows("task_history", AUDIT_COLUMNS, snapshot.auditHistory);
9429
+ return result;
9430
+ }
9431
+ function upsertById(db, table, columns, row, updateClockColumn) {
9432
+ const id = row["id"];
9433
+ if (typeof id !== "string" || !id)
9434
+ throw new Error(`${table} row is missing id`);
9435
+ const presentColumns = columns.filter((column) => (column in row));
9436
+ if (!presentColumns.includes("id"))
9437
+ presentColumns.unshift("id");
9438
+ const existing = existsById(db, table, id);
9439
+ const placeholders = presentColumns.map(() => "?").join(", ");
9440
+ const values = presentColumns.map((column) => valueForColumn(column, row[column]));
9441
+ const updateColumns = presentColumns.filter((column) => column !== "id");
9442
+ const updateSet = updateColumns.map((column) => `${column} = excluded.${column}`).join(", ");
9443
+ const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
9444
+ const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})
9445
+ ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})`;
9446
+ const changes = db.run(sql, values).changes;
9447
+ if (changes === 0)
9448
+ return "skipped";
9449
+ return existing ? "updated" : "inserted";
9450
+ }
9451
+ function existsById(db, table, id) {
9452
+ return Boolean(db.query(`SELECT id FROM ${table} WHERE id = ?`).get(id));
9453
+ }
9454
+ function valueForColumn(column, value) {
9455
+ if (BOOLEAN_COLUMNS.has(column))
9456
+ return value ? 1 : 0;
9457
+ if (JSON_COLUMNS.has(column))
9458
+ return JSON.stringify(value ?? (column === "tags" || column === "permissions" || column === "capabilities" || column === "variables" ? [] : {}));
9459
+ return value === undefined ? null : value;
9460
+ }
9461
+ function asRecord(value) {
9462
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
9463
+ throw new Error("snapshot rows must be objects");
9464
+ }
9465
+ return value;
9466
+ }
9467
+ function sortedTasks(tasks) {
9468
+ const byId = new Map(tasks.map((task) => [task.id, task]));
9469
+ const seen = new Set;
9470
+ const result = [];
9471
+ const visit = (task) => {
9472
+ if (seen.has(task.id))
9473
+ return;
9474
+ if (task.parent_id && byId.has(task.parent_id))
9475
+ visit(byId.get(task.parent_id));
9476
+ seen.add(task.id);
9477
+ result.push(task);
9478
+ };
9479
+ for (const task of tasks)
9480
+ visit(task);
9481
+ return result;
9482
+ }
9483
+
9484
+ // src/storage/local-sqlite.ts
8798
9485
  function createLocalSqliteTodosStorageAdapter(options = {}) {
8799
9486
  const database = () => options.db ?? getDatabase();
8800
9487
  let adapter;
@@ -8867,7 +9554,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
8867
9554
  getRecentActivity: (limit) => getRecentActivity(limit, database())
8868
9555
  },
8869
9556
  sync: {
8870
- getTasksChangedSince: (since, filters) => getTasksChangedSince(since, filters, database())
9557
+ getTasksChangedSince: (since, filters) => getTasksChangedSince(since, filters, database()),
9558
+ exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
9559
+ importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
8871
9560
  },
8872
9561
  transaction: (fn) => {
8873
9562
  const tx = database().transaction(() => fn(adapter));
@@ -8876,6 +9565,1360 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
8876
9565
  };
8877
9566
  return adapter;
8878
9567
  }
9568
+
9569
+ // src/storage/postgres-sync.ts
9570
+ var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
9571
+ var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
9572
+ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE, cursorTableName = DEFAULT_TODOS_POSTGRES_CURSOR_TABLE) {
9573
+ assertSafeIdentifier(tableName);
9574
+ assertSafeIdentifier(cursorTableName);
9575
+ return [
9576
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
9577
+ service text NOT NULL,
9578
+ object_type text NOT NULL,
9579
+ object_id text NOT NULL,
9580
+ payload jsonb NOT NULL,
9581
+ updated_at timestamptz NOT NULL,
9582
+ deleted_at timestamptz,
9583
+ source_machine_id text,
9584
+ version integer,
9585
+ PRIMARY KEY (service, object_type, object_id)
9586
+ )`,
9587
+ `CREATE INDEX IF NOT EXISTS ${tableName}_updated_idx ON ${tableName} (service, updated_at)`,
9588
+ `CREATE TABLE IF NOT EXISTS ${cursorTableName} (
9589
+ service text NOT NULL,
9590
+ cursor_name text NOT NULL,
9591
+ value text NOT NULL,
9592
+ updated_at timestamptz NOT NULL DEFAULT now(),
9593
+ PRIMARY KEY (service, cursor_name)
9594
+ )`
9595
+ ];
9596
+ }
9597
+
9598
+ class PostgresTodosSyncStore {
9599
+ client;
9600
+ service;
9601
+ sourceMachineId;
9602
+ tableName;
9603
+ cursorTableName;
9604
+ constructor(client, options = {}) {
9605
+ this.client = client;
9606
+ this.service = options.service ?? "todos";
9607
+ this.sourceMachineId = options.sourceMachineId;
9608
+ this.tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
9609
+ this.cursorTableName = options.cursorTableName ?? DEFAULT_TODOS_POSTGRES_CURSOR_TABLE;
9610
+ assertSafeIdentifier(this.tableName);
9611
+ assertSafeIdentifier(this.cursorTableName);
9612
+ }
9613
+ async ensureSchema() {
9614
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
9615
+ await this.client.query(sql);
9616
+ }
9617
+ }
9618
+ async pushSnapshot(snapshot, context = {}) {
9619
+ const result = { records: 0, objectTypes: {} };
9620
+ const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
9621
+ for (const entry of snapshotEntries(snapshot)) {
9622
+ await this.client.query(`INSERT INTO ${this.tableName} (
9623
+ service, object_type, object_id, payload, updated_at,
9624
+ deleted_at, source_machine_id, version
9625
+ ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
9626
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
9627
+ payload = EXCLUDED.payload,
9628
+ updated_at = EXCLUDED.updated_at,
9629
+ deleted_at = EXCLUDED.deleted_at,
9630
+ source_machine_id = EXCLUDED.source_machine_id,
9631
+ version = EXCLUDED.version
9632
+ WHERE ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
9633
+ this.service,
9634
+ entry.type,
9635
+ entry.id,
9636
+ JSON.stringify(entry.payload),
9637
+ entry.updatedAt,
9638
+ entry.deletedAt,
9639
+ sourceMachineId,
9640
+ entry.version
9641
+ ]);
9642
+ result.records += 1;
9643
+ result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
9644
+ }
9645
+ return result;
9646
+ }
9647
+ async pullSnapshot(options = {}) {
9648
+ const params = [this.service];
9649
+ const filters = ["service = $1", "deleted_at IS NULL"];
9650
+ if (options.since) {
9651
+ params.push(options.since);
9652
+ filters.push(`updated_at > $${params.length}::timestamptz`);
9653
+ }
9654
+ if (options.objectTypes?.length) {
9655
+ params.push(options.objectTypes);
9656
+ filters.push(`object_type = ANY($${params.length}::text[])`);
9657
+ }
9658
+ const response = await this.client.query(`SELECT object_type, payload FROM ${this.tableName}
9659
+ WHERE ${filters.join(" AND ")}
9660
+ ORDER BY updated_at ASC, object_type ASC, object_id ASC`, params);
9661
+ return rowsToSnapshot(response.rows);
9662
+ }
9663
+ async getCursor(name) {
9664
+ const result = await this.client.query(`SELECT value FROM ${this.cursorTableName} WHERE service = $1 AND cursor_name = $2`, [this.service, name]);
9665
+ return result.rows[0]?.value ?? null;
9666
+ }
9667
+ async setCursor(name, value) {
9668
+ await this.client.query(`INSERT INTO ${this.cursorTableName} (service, cursor_name, value, updated_at)
9669
+ VALUES ($1, $2, $3, now())
9670
+ ON CONFLICT (service, cursor_name) DO UPDATE SET
9671
+ value = EXCLUDED.value,
9672
+ updated_at = EXCLUDED.updated_at`, [this.service, name, value]);
9673
+ }
9674
+ }
9675
+ function createPostgresTodosSyncStore(client, options = {}) {
9676
+ return new PostgresTodosSyncStore(client, options);
9677
+ }
9678
+ function snapshotEntries(snapshot) {
9679
+ return [
9680
+ ...snapshot.tasks.map((payload) => entry("tasks", payload, snapshot.exportedAt)),
9681
+ ...snapshot.projects.map((payload) => entry("projects", payload, snapshot.exportedAt)),
9682
+ ...snapshot.plans.map((payload) => entry("plans", payload, snapshot.exportedAt)),
9683
+ ...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
9684
+ ...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
9685
+ ...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
9686
+ ...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt))
9687
+ ];
9688
+ }
9689
+ function entry(type, payload, fallbackUpdatedAt) {
9690
+ const id = payload["id"];
9691
+ if (typeof id !== "string" || !id)
9692
+ throw new Error(`${type} record is missing a stable id`);
9693
+ return {
9694
+ type,
9695
+ id,
9696
+ payload,
9697
+ updatedAt: stringValue(payload["updated_at"]) ?? stringValue(payload["created_at"]) ?? fallbackUpdatedAt,
9698
+ deletedAt: stringValue(payload["deleted_at"]),
9699
+ version: numberValue(payload["version"])
9700
+ };
9701
+ }
9702
+ function rowsToSnapshot(rows) {
9703
+ const snapshot = {
9704
+ exportedAt: new Date().toISOString(),
9705
+ source: "postgres",
9706
+ tasks: [],
9707
+ projects: [],
9708
+ plans: [],
9709
+ agents: [],
9710
+ taskLists: [],
9711
+ templates: [],
9712
+ auditHistory: []
9713
+ };
9714
+ for (const row of rows) {
9715
+ const payload = payloadRecord(row.payload);
9716
+ if (row.object_type === "tasks")
9717
+ snapshot.tasks.push(payload);
9718
+ else if (row.object_type === "projects")
9719
+ snapshot.projects.push(payload);
9720
+ else if (row.object_type === "plans")
9721
+ snapshot.plans.push(payload);
9722
+ else if (row.object_type === "agents")
9723
+ snapshot.agents.push(payload);
9724
+ else if (row.object_type === "task_lists")
9725
+ snapshot.taskLists.push(payload);
9726
+ else if (row.object_type === "templates")
9727
+ snapshot.templates.push(payload);
9728
+ else if (row.object_type === "audit_history")
9729
+ snapshot.auditHistory.push(payload);
9730
+ }
9731
+ return snapshot;
9732
+ }
9733
+ function payloadRecord(value) {
9734
+ if (typeof value === "string")
9735
+ return JSON.parse(value);
9736
+ if (value && typeof value === "object" && !Array.isArray(value))
9737
+ return value;
9738
+ throw new Error("Postgres sync payload must be a JSON object");
9739
+ }
9740
+ function stringValue(value) {
9741
+ return typeof value === "string" && value ? value : null;
9742
+ }
9743
+ function numberValue(value) {
9744
+ return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
9745
+ }
9746
+ function assertSafeIdentifier(value) {
9747
+ if (!/^[a-z_][a-z0-9_]*$/i.test(value))
9748
+ throw new Error(`Unsafe Postgres identifier: ${value}`);
9749
+ }
9750
+
9751
+ // src/storage/hybrid.ts
9752
+ function createHybridTodosStorageAdapter(options) {
9753
+ const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
9754
+ const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
9755
+ if (!syncStore)
9756
+ throw new Error("hybrid storage requires a Postgres sync store or query client");
9757
+ if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
9758
+ throw new Error("hybrid storage requires local snapshot export/import support");
9759
+ }
9760
+ const exportSnapshot = async (context) => await local.sync.exportSnapshot(context);
9761
+ const importSnapshot = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
9762
+ const remote = {
9763
+ ensureSchema: () => syncStore.ensureSchema(),
9764
+ async pushSnapshot(context) {
9765
+ const snapshot = await exportSnapshot(context);
9766
+ return syncStore.pushSnapshot(snapshot, context);
9767
+ },
9768
+ async pullSnapshot(options2, context) {
9769
+ const snapshot = await syncStore.pullSnapshot(options2);
9770
+ return importSnapshot(snapshot, context);
9771
+ },
9772
+ async syncOnce(options2, context) {
9773
+ const pulled = await remote.pullSnapshot(options2, context);
9774
+ const pushed = await remote.pushSnapshot(context);
9775
+ return { pulled, pushed };
9776
+ }
9777
+ };
9778
+ return {
9779
+ ...local,
9780
+ kind: "hybrid",
9781
+ capabilities: {
9782
+ ...local.capabilities,
9783
+ localPersistence: true,
9784
+ remotePersistence: true,
9785
+ sync: true
9786
+ },
9787
+ sync: {
9788
+ ...local.sync,
9789
+ exportSnapshot,
9790
+ importSnapshot
9791
+ },
9792
+ remote
9793
+ };
9794
+ }
9795
+
9796
+ // src/storage/postgres-adapter.ts
9797
+ import { randomUUID as randomUUID2 } from "crypto";
9798
+ function createPostgresTodosStorageAdapter(options) {
9799
+ const store = new PostgresJsonRecordStore(options);
9800
+ const adapter = {
9801
+ kind: "postgres",
9802
+ capabilities: {
9803
+ localPersistence: false,
9804
+ remotePersistence: true,
9805
+ transactions: false,
9806
+ auditLog: true,
9807
+ sync: true
9808
+ },
9809
+ tasks: {
9810
+ create: (input, context) => createTask2(input, store, context),
9811
+ get: (id) => store.get("tasks", id),
9812
+ list: (filter = {}) => listTasks2(filter, store),
9813
+ count: async (filter = {}) => (await listTasks2(filter, store)).length,
9814
+ update: (id, input) => updateTask2(id, input, store),
9815
+ delete: (id, context) => store.delete("tasks", id, context),
9816
+ start: (id, agentId) => startTask2(id, agentId, store),
9817
+ complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
9818
+ fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
9819
+ claimNext: (agentId, filters) => claimNextTask2(agentId, filters, store),
9820
+ getNext: (_agentId, filters) => getNextTask2(filters, store),
9821
+ getActiveWork: (filters) => getActiveWork2(filters, store),
9822
+ getChangedSince: (since, filters) => getChangedSince(since, filters, store)
9823
+ },
9824
+ projects: {
9825
+ create: (input, context) => createProject2(input, store, context),
9826
+ get: (id) => store.get("projects", id),
9827
+ getByPath: async (path) => (await store.list("projects")).find((project) => project.path === path) ?? null,
9828
+ list: async () => (await store.list("projects")).sort((a, b) => a.name.localeCompare(b.name)),
9829
+ update: (id, input) => updateProject2(id, input, store),
9830
+ delete: (id, context) => store.delete("projects", id, context)
9831
+ },
9832
+ plans: {
9833
+ create: (input, context) => createPlan2(input, store, context),
9834
+ get: (id) => store.get("plans", id),
9835
+ list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
9836
+ update: (id, input) => updatePlan2(id, input, store),
9837
+ delete: (id, context) => store.delete("plans", id, context)
9838
+ },
9839
+ agents: {
9840
+ register: (input, context) => registerAgent2(input, store, context),
9841
+ get: (id) => store.get("agents", id),
9842
+ getByName: async (name) => (await store.list("agents")).find((agent) => agent.name === name) ?? null,
9843
+ list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
9844
+ update: (id, input) => updateAgent2(id, input, store)
9845
+ },
9846
+ taskLists: {
9847
+ create: (input, context) => createTaskList2(input, store, context),
9848
+ get: (id) => store.get("task_lists", id),
9849
+ getBySlug: async (slug, projectId) => (await store.list("task_lists")).find((list) => list.slug === slug && (projectId === undefined || list.project_id === projectId)) ?? null,
9850
+ list: async (projectId) => (await store.list("task_lists")).filter((list) => projectId === undefined || list.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
9851
+ update: (id, input) => updateTaskList2(id, input, store),
9852
+ delete: (id, context) => store.delete("task_lists", id, context)
9853
+ },
9854
+ templates: {
9855
+ create: (input, context) => createTemplate2(input, store, context),
9856
+ get: (id) => store.get("templates", id),
9857
+ list: async () => (await store.list("templates")).sort((a, b) => a.name.localeCompare(b.name)),
9858
+ update: (id, input) => updateTemplate2(id, input, store),
9859
+ delete: (id, context) => store.delete("templates", id, context),
9860
+ getWithTasks: async (id) => {
9861
+ const template = await store.get("templates", id);
9862
+ return template ? { ...template, tasks: [] } : null;
9863
+ }
9864
+ },
9865
+ audit: {
9866
+ logTaskChange: (taskId, action, field, oldValue, newValue, agentId, context) => logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context),
9867
+ addComment: (input, context) => addComment2(input, store, context),
9868
+ getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
9869
+ getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
9870
+ },
9871
+ sync: {
9872
+ getTasksChangedSince: (since, filters) => getChangedSince(since, filters, store),
9873
+ exportSnapshot: () => exportSnapshot(store),
9874
+ importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
9875
+ },
9876
+ transaction: (fn) => fn(adapter)
9877
+ };
9878
+ return adapter;
9879
+ }
9880
+
9881
+ class PostgresJsonRecordStore {
9882
+ options;
9883
+ service;
9884
+ sourceMachineId;
9885
+ tableName;
9886
+ cursorTableName;
9887
+ schemaReady = null;
9888
+ constructor(options) {
9889
+ this.options = options;
9890
+ this.service = options.service ?? "todos";
9891
+ this.sourceMachineId = options.sourceMachineId;
9892
+ this.tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
9893
+ this.cursorTableName = options.cursorTableName ?? DEFAULT_TODOS_POSTGRES_CURSOR_TABLE;
9894
+ }
9895
+ async ensureSchema() {
9896
+ this.schemaReady ??= (async () => {
9897
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
9898
+ await this.options.client.query(sql);
9899
+ }
9900
+ })();
9901
+ await this.schemaReady;
9902
+ }
9903
+ async get(type, id) {
9904
+ await this.ensureSchema();
9905
+ const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
9906
+ FROM ${this.tableName}
9907
+ WHERE service = $1 AND object_type = $2 AND object_id = $3 AND deleted_at IS NULL
9908
+ LIMIT 1`, [this.service, type, id]);
9909
+ return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
9910
+ }
9911
+ async list(type) {
9912
+ return (await this.listRecords(type)).map((record) => record.payload);
9913
+ }
9914
+ async listRecords(type) {
9915
+ await this.ensureSchema();
9916
+ const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
9917
+ FROM ${this.tableName}
9918
+ WHERE service = $1 AND object_type = $2 AND deleted_at IS NULL
9919
+ ORDER BY updated_at ASC, object_id ASC`, [this.service, type]);
9920
+ return result.rows.map((row) => ({
9921
+ objectId: row.object_id,
9922
+ payload: payloadRecord2(row.payload),
9923
+ updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString()
9924
+ }));
9925
+ }
9926
+ async upsert(type, value, context = {}) {
9927
+ await this.ensureSchema();
9928
+ const updatedAt = stringValue2(value.updated_at) ?? stringValue2(value.created_at) ?? new Date().toISOString();
9929
+ await this.options.client.query(`INSERT INTO ${this.tableName} (
9930
+ service, object_type, object_id, payload, updated_at,
9931
+ deleted_at, source_machine_id, version
9932
+ ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, $7)
9933
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
9934
+ payload = EXCLUDED.payload,
9935
+ updated_at = EXCLUDED.updated_at,
9936
+ deleted_at = NULL,
9937
+ source_machine_id = EXCLUDED.source_machine_id,
9938
+ version = EXCLUDED.version`, [
9939
+ this.service,
9940
+ type,
9941
+ value.id,
9942
+ JSON.stringify(value),
9943
+ updatedAt,
9944
+ context.requestId ?? this.sourceMachineId ?? null,
9945
+ numberValue2(value.version)
9946
+ ]);
9947
+ return value;
9948
+ }
9949
+ async delete(type, id, context = {}) {
9950
+ await this.ensureSchema();
9951
+ const existing = await this.get(type, id);
9952
+ if (!existing)
9953
+ return false;
9954
+ const timestamp = new Date().toISOString();
9955
+ await this.options.client.query(`UPDATE ${this.tableName}
9956
+ SET deleted_at = $4::timestamptz,
9957
+ updated_at = $4::timestamptz,
9958
+ source_machine_id = $5
9959
+ WHERE service = $1 AND object_type = $2 AND object_id = $3`, [this.service, type, id, timestamp, context.requestId ?? this.sourceMachineId ?? null]);
9960
+ return true;
9961
+ }
9962
+ async getCursor(name) {
9963
+ await this.ensureSchema();
9964
+ const result = await this.options.client.query(`SELECT value FROM ${this.cursorTableName} WHERE service = $1 AND cursor_name = $2`, [this.service, name]);
9965
+ return result.rows[0]?.value ?? null;
9966
+ }
9967
+ async setCursor(name, value) {
9968
+ await this.ensureSchema();
9969
+ await this.options.client.query(`INSERT INTO ${this.cursorTableName} (service, cursor_name, value, updated_at)
9970
+ VALUES ($1, $2, $3, now())
9971
+ ON CONFLICT (service, cursor_name) DO UPDATE SET
9972
+ value = EXCLUDED.value,
9973
+ updated_at = EXCLUDED.updated_at`, [this.service, name, value]);
9974
+ }
9975
+ }
9976
+ async function createTask2(input, store, context) {
9977
+ const timestamp = new Date().toISOString();
9978
+ const shortId = input.project_id ? await nextTaskShortId2(input.project_id, store, context) : null;
9979
+ const task = {
9980
+ id: randomUUID2(),
9981
+ short_id: shortId,
9982
+ project_id: input.project_id ?? context?.projectId ?? null,
9983
+ parent_id: input.parent_id ?? null,
9984
+ plan_id: input.plan_id ?? null,
9985
+ task_list_id: input.task_list_id ?? context?.taskListId ?? null,
9986
+ title: input.title,
9987
+ description: input.description ?? null,
9988
+ status: input.status ?? "pending",
9989
+ priority: input.priority ?? "medium",
9990
+ agent_id: input.agent_id ?? null,
9991
+ assigned_to: input.assigned_to ?? null,
9992
+ session_id: input.session_id ?? context?.sessionId ?? null,
9993
+ working_dir: input.working_dir ?? null,
9994
+ tags: input.tags ?? [],
9995
+ metadata: input.metadata ?? {},
9996
+ version: 1,
9997
+ locked_by: null,
9998
+ locked_at: null,
9999
+ created_at: timestamp,
10000
+ updated_at: timestamp,
10001
+ started_at: null,
10002
+ completed_at: null,
10003
+ due_at: input.due_at ?? null,
10004
+ estimated_minutes: input.estimated_minutes ?? null,
10005
+ actual_minutes: null,
10006
+ requires_approval: input.requires_approval ?? false,
10007
+ approved_by: null,
10008
+ approved_at: null,
10009
+ recurrence_rule: input.recurrence_rule ?? null,
10010
+ recurrence_parent_id: input.recurrence_parent_id ?? null,
10011
+ spawns_template_id: input.spawns_template_id ?? null,
10012
+ confidence: input.confidence ?? null,
10013
+ reason: input.reason ?? null,
10014
+ spawned_from_session: input.spawned_from_session ?? null,
10015
+ assigned_by: input.assigned_by ?? null,
10016
+ assigned_from_project: input.assigned_from_project ?? null,
10017
+ task_type: input.task_type ?? null,
10018
+ cost_tokens: 0,
10019
+ cost_usd: 0,
10020
+ delegated_from: null,
10021
+ delegation_depth: 0,
10022
+ retry_count: input.retry_count ?? 0,
10023
+ max_retries: input.max_retries ?? 0,
10024
+ retry_after: input.retry_after ?? null,
10025
+ sla_minutes: input.sla_minutes ?? null,
10026
+ runner_id: null,
10027
+ runner_started_at: null,
10028
+ runner_completed_at: null,
10029
+ current_step: null,
10030
+ total_steps: null
10031
+ };
10032
+ await store.upsert("tasks", task, context);
10033
+ await logTaskChange2(task.id, "created", "status", null, task.status, task.assigned_by ?? task.agent_id, store, context);
10034
+ return task;
10035
+ }
10036
+ async function updateTask2(id, input, store) {
10037
+ const existing = await requireRecord("tasks", id, store);
10038
+ if (existing.version !== input.version) {
10039
+ throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
10040
+ }
10041
+ const task = {
10042
+ ...existing,
10043
+ ...definedPatch(input),
10044
+ version: existing.version + 1,
10045
+ updated_at: new Date().toISOString(),
10046
+ tags: input.tags ?? existing.tags,
10047
+ metadata: input.metadata ?? existing.metadata,
10048
+ requires_approval: input.requires_approval ?? existing.requires_approval,
10049
+ task_list_id: input.task_list_id ?? existing.task_list_id
10050
+ };
10051
+ await store.upsert("tasks", task);
10052
+ return task;
10053
+ }
10054
+ async function startTask2(id, agentId, store) {
10055
+ const task = await requireRecord("tasks", id, store);
10056
+ return patchTask(task, {
10057
+ status: "in_progress",
10058
+ assigned_to: task.assigned_to ?? agentId,
10059
+ agent_id: task.agent_id ?? agentId,
10060
+ locked_by: agentId,
10061
+ locked_at: new Date().toISOString(),
10062
+ started_at: task.started_at ?? new Date().toISOString()
10063
+ }, store);
10064
+ }
10065
+ async function completeTask2(id, agentId, options, store) {
10066
+ const task = await requireRecord("tasks", id, store);
10067
+ return patchTask(task, {
10068
+ status: "completed",
10069
+ assigned_to: task.assigned_to ?? agentId ?? null,
10070
+ completed_at: options?.completed_at ?? new Date().toISOString(),
10071
+ actual_minutes: task.actual_minutes,
10072
+ confidence: options?.confidence ?? task.confidence
10073
+ }, store);
10074
+ }
10075
+ async function failTask2(id, agentId, reason, options, store) {
10076
+ const task = await requireRecord("tasks", id, store);
10077
+ const failed = await patchTask(task, {
10078
+ status: "failed",
10079
+ assigned_to: task.assigned_to ?? agentId ?? null,
10080
+ reason: reason ?? task.reason,
10081
+ retry_after: options?.retry_after ?? task.retry_after
10082
+ }, store);
10083
+ if (!options?.retry)
10084
+ return { task: failed };
10085
+ const retryTask = await createTask2({
10086
+ title: task.title,
10087
+ description: task.description ?? undefined,
10088
+ project_id: task.project_id ?? undefined,
10089
+ parent_id: task.parent_id ?? undefined,
10090
+ plan_id: task.plan_id ?? undefined,
10091
+ task_list_id: task.task_list_id ?? undefined,
10092
+ priority: task.priority,
10093
+ assigned_to: task.assigned_to ?? undefined,
10094
+ tags: task.tags,
10095
+ metadata: task.metadata,
10096
+ retry_count: task.retry_count + 1,
10097
+ max_retries: task.max_retries,
10098
+ reason: reason ?? undefined,
10099
+ task_type: task.task_type ?? undefined
10100
+ }, store);
10101
+ return { task: failed, retryTask };
10102
+ }
10103
+ async function patchTask(task, patch, store) {
10104
+ const updated = {
10105
+ ...task,
10106
+ ...patch,
10107
+ version: task.version + 1,
10108
+ updated_at: new Date().toISOString()
10109
+ };
10110
+ await store.upsert("tasks", updated);
10111
+ return updated;
10112
+ }
10113
+ async function listTasks2(filter, store) {
10114
+ let tasks = await store.list("tasks");
10115
+ tasks = tasks.filter((task) => taskMatchesFilter(task, filter));
10116
+ tasks.sort((a, b) => priorityRank(a.priority) - priorityRank(b.priority) || a.created_at.localeCompare(b.created_at));
10117
+ const offset = filter.offset ?? 0;
10118
+ const limit = filter.limit ?? tasks.length;
10119
+ return tasks.slice(offset, offset + limit);
10120
+ }
10121
+ async function getNextTask2(filters, store) {
10122
+ return (await listTasks2({ ...filters, status: "pending", limit: 1 }, store))[0] ?? null;
10123
+ }
10124
+ async function claimNextTask2(agentId, filters, store) {
10125
+ const task = await getNextTask2(filters, store);
10126
+ return task ? startTask2(task.id, agentId, store) : null;
10127
+ }
10128
+ async function getActiveWork2(filters, store) {
10129
+ const tasks = await listTasks2({ ...filters, status: "in_progress" }, store);
10130
+ return tasks.map((task) => ({
10131
+ id: task.id,
10132
+ short_id: task.short_id,
10133
+ title: task.title,
10134
+ priority: task.priority,
10135
+ assigned_to: task.assigned_to,
10136
+ locked_by: task.locked_by,
10137
+ locked_at: task.locked_at,
10138
+ updated_at: task.updated_at
10139
+ }));
10140
+ }
10141
+ async function getChangedSince(since, filters, store) {
10142
+ return (await listTasks2(filters ?? {}, store)).filter((task) => task.updated_at > since);
10143
+ }
10144
+ async function createProject2(input, store, context) {
10145
+ const timestamp = new Date().toISOString();
10146
+ const project = {
10147
+ id: randomUUID2(),
10148
+ name: input.name,
10149
+ path: input.path,
10150
+ description: input.description ?? null,
10151
+ task_list_id: input.task_list_id ?? `todos-${slugify2(input.name)}`,
10152
+ task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
10153
+ task_counter: 0,
10154
+ created_at: timestamp,
10155
+ updated_at: timestamp
10156
+ };
10157
+ return store.upsert("projects", project, context);
10158
+ }
10159
+ async function updateProject2(id, input, store) {
10160
+ const project = await requireRecord("projects", id, store);
10161
+ const updated = { ...project, ...definedPatch(input), updated_at: new Date().toISOString() };
10162
+ return store.upsert("projects", updated);
10163
+ }
10164
+ async function createPlan2(input, store, context) {
10165
+ const timestamp = new Date().toISOString();
10166
+ return store.upsert("plans", {
10167
+ id: randomUUID2(),
10168
+ project_id: input.project_id ?? context?.projectId ?? null,
10169
+ task_list_id: input.task_list_id ?? context?.taskListId ?? null,
10170
+ agent_id: input.agent_id ?? context?.agentId ?? null,
10171
+ name: input.name,
10172
+ description: input.description ?? null,
10173
+ status: input.status ?? "active",
10174
+ created_at: timestamp,
10175
+ updated_at: timestamp
10176
+ }, context);
10177
+ }
10178
+ async function updatePlan2(id, input, store) {
10179
+ const plan = await requireRecord("plans", id, store);
10180
+ return store.upsert("plans", { ...plan, ...definedPatch(input), updated_at: new Date().toISOString() });
10181
+ }
10182
+ async function registerAgent2(input, store, context) {
10183
+ const existing = (await store.list("agents")).find((agent2) => agent2.name === input.name && agent2.status !== "archived");
10184
+ if (existing && !input.force && existing.session_id && existing.session_id !== input.session_id) {
10185
+ return { conflict: true, message: `Agent name '${input.name}' is already active` };
10186
+ }
10187
+ const timestamp = new Date().toISOString();
10188
+ const agent = {
10189
+ id: existing?.id ?? randomUUID2().slice(0, 8),
10190
+ name: input.name,
10191
+ description: input.description ?? existing?.description ?? null,
10192
+ role: input.role ?? existing?.role ?? null,
10193
+ title: input.title ?? existing?.title ?? null,
10194
+ level: input.level ?? existing?.level ?? null,
10195
+ permissions: input.permissions ?? existing?.permissions ?? [],
10196
+ reports_to: input.reports_to ?? existing?.reports_to ?? null,
10197
+ org_id: input.org_id ?? existing?.org_id ?? null,
10198
+ capabilities: input.capabilities ?? existing?.capabilities ?? [],
10199
+ status: "active",
10200
+ metadata: input.metadata ?? existing?.metadata ?? {},
10201
+ created_at: existing?.created_at ?? timestamp,
10202
+ last_seen_at: timestamp,
10203
+ session_id: input.session_id ?? context?.sessionId ?? existing?.session_id ?? null,
10204
+ working_dir: input.working_dir ?? existing?.working_dir ?? null,
10205
+ active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null
10206
+ };
10207
+ return store.upsert("agents", agent, context);
10208
+ }
10209
+ async function updateAgent2(id, input, store) {
10210
+ const agent = await store.get("agents", id);
10211
+ if (!agent)
10212
+ return null;
10213
+ return store.upsert("agents", {
10214
+ ...agent,
10215
+ ...definedPatch(input),
10216
+ permissions: input.permissions ?? agent.permissions,
10217
+ capabilities: input.capabilities ?? agent.capabilities,
10218
+ metadata: input.metadata ?? agent.metadata,
10219
+ last_seen_at: new Date().toISOString()
10220
+ });
10221
+ }
10222
+ async function createTaskList2(input, store, context) {
10223
+ const timestamp = new Date().toISOString();
10224
+ return store.upsert("task_lists", {
10225
+ id: randomUUID2(),
10226
+ project_id: input.project_id ?? context?.projectId ?? null,
10227
+ slug: input.slug ?? slugify2(input.name),
10228
+ name: input.name,
10229
+ description: input.description ?? null,
10230
+ metadata: input.metadata ?? {},
10231
+ created_at: timestamp,
10232
+ updated_at: timestamp
10233
+ }, context);
10234
+ }
10235
+ async function updateTaskList2(id, input, store) {
10236
+ const list = await requireRecord("task_lists", id, store);
10237
+ return store.upsert("task_lists", {
10238
+ ...list,
10239
+ ...definedPatch(input),
10240
+ metadata: input.metadata ?? list.metadata,
10241
+ updated_at: new Date().toISOString()
10242
+ });
10243
+ }
10244
+ async function createTemplate2(input, store, context) {
10245
+ const timestamp = new Date().toISOString();
10246
+ return store.upsert("templates", {
10247
+ id: randomUUID2(),
10248
+ name: input.name,
10249
+ title_pattern: input.title_pattern,
10250
+ description: input.description ?? null,
10251
+ priority: input.priority ?? "medium",
10252
+ tags: input.tags ?? [],
10253
+ variables: input.variables ?? [],
10254
+ version: 1,
10255
+ project_id: input.project_id ?? context?.projectId ?? null,
10256
+ plan_id: input.plan_id ?? null,
10257
+ metadata: input.metadata ?? {},
10258
+ created_at: timestamp
10259
+ }, context);
10260
+ }
10261
+ async function updateTemplate2(id, input, store) {
10262
+ const template = await store.get("templates", id);
10263
+ if (!template)
10264
+ return null;
10265
+ return store.upsert("templates", {
10266
+ ...template,
10267
+ ...definedPatch(input),
10268
+ tags: input.tags ?? template.tags,
10269
+ variables: input.variables ?? template.variables,
10270
+ metadata: input.metadata ?? template.metadata,
10271
+ version: template.version + 1
10272
+ });
10273
+ }
10274
+ async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context) {
10275
+ const entry2 = {
10276
+ id: randomUUID2(),
10277
+ task_id: taskId,
10278
+ action,
10279
+ field: field ?? null,
10280
+ old_value: oldValue ?? null,
10281
+ new_value: newValue ?? null,
10282
+ agent_id: agentId ?? context?.agentId ?? null,
10283
+ created_at: new Date().toISOString()
10284
+ };
10285
+ return store.upsert("audit_history", entry2, context);
10286
+ }
10287
+ async function addComment2(input, store, context) {
10288
+ const comment = {
10289
+ id: randomUUID2(),
10290
+ task_id: input.task_id,
10291
+ agent_id: input.agent_id ?? context?.agentId ?? null,
10292
+ session_id: input.session_id ?? context?.sessionId ?? null,
10293
+ content: input.content,
10294
+ type: input.type ?? "comment",
10295
+ progress_pct: input.progress_pct ?? null,
10296
+ created_at: new Date().toISOString()
10297
+ };
10298
+ return store.upsert("comments", comment, context);
10299
+ }
10300
+ async function exportSnapshot(store) {
10301
+ return {
10302
+ exportedAt: new Date().toISOString(),
10303
+ source: "postgres",
10304
+ tasks: await store.list("tasks"),
10305
+ projects: await store.list("projects"),
10306
+ plans: await store.list("plans"),
10307
+ agents: await store.list("agents"),
10308
+ taskLists: await store.list("task_lists"),
10309
+ templates: await store.list("templates"),
10310
+ auditHistory: await store.list("audit_history")
10311
+ };
10312
+ }
10313
+ async function importSnapshot(snapshot, store, context) {
10314
+ const result = { inserted: 0, updated: 0, skipped: 0, errors: [] };
10315
+ const entries = [
10316
+ ...snapshot.tasks.map((row) => ["tasks", row]),
10317
+ ...snapshot.projects.map((row) => ["projects", row]),
10318
+ ...snapshot.plans.map((row) => ["plans", row]),
10319
+ ...snapshot.agents.map((row) => ["agents", row]),
10320
+ ...snapshot.taskLists.map((row) => ["task_lists", row]),
10321
+ ...snapshot.templates.map((row) => ["templates", row]),
10322
+ ...snapshot.auditHistory.map((row) => ["audit_history", row])
10323
+ ];
10324
+ for (const [type, row] of entries) {
10325
+ try {
10326
+ const existing = await store.get(type, row.id);
10327
+ await store.upsert(type, row, context);
10328
+ if (existing)
10329
+ result.updated += 1;
10330
+ else
10331
+ result.inserted += 1;
10332
+ } catch (error) {
10333
+ result.errors.push(error instanceof Error ? error.message : String(error));
10334
+ }
10335
+ }
10336
+ return result;
10337
+ }
10338
+ async function requireRecord(type, id, store) {
10339
+ const record = await store.get(type, id);
10340
+ if (!record)
10341
+ throw new Error(`${type} record not found: ${id}`);
10342
+ return record;
10343
+ }
10344
+ async function nextTaskShortId2(projectId, store, context) {
10345
+ const project = await store.get("projects", projectId);
10346
+ if (!project?.task_prefix)
10347
+ return null;
10348
+ const counter = project.task_counter + 1;
10349
+ await store.upsert("projects", {
10350
+ ...project,
10351
+ task_counter: counter,
10352
+ updated_at: new Date().toISOString()
10353
+ }, context);
10354
+ return `${project.task_prefix}-${String(counter).padStart(5, "0")}`;
10355
+ }
10356
+ async function generateProjectPrefix(name, store) {
10357
+ const base = (name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/).filter(Boolean).slice(0, 3).map((word) => word[0]).join("") || name.slice(0, 3) || "TOD").toUpperCase();
10358
+ const existing = new Set((await store.list("projects")).map((project) => project.task_prefix).filter(Boolean));
10359
+ let candidate = base;
10360
+ let suffix = 1;
10361
+ while (existing.has(candidate)) {
10362
+ suffix += 1;
10363
+ candidate = `${base}${suffix}`;
10364
+ }
10365
+ return candidate;
10366
+ }
10367
+ function taskMatchesFilter(task, filter) {
10368
+ if (filter.ids && !filter.ids.includes(task.id))
10369
+ return false;
10370
+ if (filter.project_id !== undefined && task.project_id !== filter.project_id)
10371
+ return false;
10372
+ if (filter.parent_id !== undefined && task.parent_id !== filter.parent_id)
10373
+ return false;
10374
+ if (filter.plan_id !== undefined && task.plan_id !== filter.plan_id)
10375
+ return false;
10376
+ if (filter.task_list_id !== undefined && task.task_list_id !== filter.task_list_id)
10377
+ return false;
10378
+ if (filter.status !== undefined && !matchesOne(task.status, filter.status))
10379
+ return false;
10380
+ if (filter.priority !== undefined && !matchesOne(task.priority, filter.priority))
10381
+ return false;
10382
+ if (filter.assigned_to !== undefined && task.assigned_to !== filter.assigned_to)
10383
+ return false;
10384
+ if (filter.agent_id !== undefined && task.agent_id !== filter.agent_id)
10385
+ return false;
10386
+ if (filter.session_id !== undefined && task.session_id !== filter.session_id)
10387
+ return false;
10388
+ if (filter.tags?.length && !filter.tags.every((tag) => task.tags.includes(tag)))
10389
+ return false;
10390
+ if (filter.has_recurrence !== undefined && Boolean(task.recurrence_rule) !== filter.has_recurrence)
10391
+ return false;
10392
+ if (filter.include_subtasks !== true && task.parent_id)
10393
+ return false;
10394
+ if (filter.task_type !== undefined && !matchesOne(task.task_type ?? "", filter.task_type))
10395
+ return false;
10396
+ return true;
10397
+ }
10398
+ function matchesOne(value, expected) {
10399
+ return Array.isArray(expected) ? expected.includes(value) : value === expected;
10400
+ }
10401
+ function priorityRank(priority) {
10402
+ return { critical: 0, high: 1, medium: 2, low: 3 }[priority];
10403
+ }
10404
+ function slugify2(value) {
10405
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "todos";
10406
+ }
10407
+ function definedPatch(value) {
10408
+ return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined));
10409
+ }
10410
+ function payloadRecord2(value) {
10411
+ if (typeof value === "string")
10412
+ return JSON.parse(value);
10413
+ if (value && typeof value === "object" && !Array.isArray(value))
10414
+ return value;
10415
+ throw new Error("Postgres storage payload must be a JSON object");
10416
+ }
10417
+ function stringValue2(value) {
10418
+ return typeof value === "string" && value ? value : null;
10419
+ }
10420
+ function numberValue2(value) {
10421
+ return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
10422
+ }
10423
+
10424
+ // src/storage/factory.ts
10425
+ function createTodosStorageAdapter(options = {}) {
10426
+ const config = options.config ?? loadTodosStorageConfig(options.env);
10427
+ if (config.mode === "local")
10428
+ return createLocalSqliteTodosStorageAdapter(options.local);
10429
+ assertTodosRemoteStorageConfig(config);
10430
+ const adapter = config.mode === "hybrid" ? options.hybridAdapter ?? createImplicitHybridAdapter(options) ?? options.remoteAdapter : options.remoteAdapter ?? createImplicitPostgresAdapter(options);
10431
+ if (!adapter) {
10432
+ throw new Error(`${config.mode} storage requires a repo-native remote adapter. ` + "Pass remoteAdapter/hybridAdapter after wiring Postgres RDS and S3 support.");
10433
+ }
10434
+ assertRemoteAdapterCapabilities(adapter, config.mode);
10435
+ return adapter;
10436
+ }
10437
+ function createImplicitPostgresAdapter(options) {
10438
+ if (!options.postgresClient)
10439
+ return null;
10440
+ return createPostgresTodosStorageAdapter({
10441
+ client: options.postgresClient,
10442
+ ...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
10443
+ });
10444
+ }
10445
+ function createImplicitHybridAdapter(options) {
10446
+ if (!options.postgresClient && !options.postgresSyncStore)
10447
+ return null;
10448
+ return createHybridTodosStorageAdapter({
10449
+ ...options.hybrid ?? {},
10450
+ local: options.local,
10451
+ postgresClient: options.postgresClient,
10452
+ syncStore: options.postgresSyncStore
10453
+ });
10454
+ }
10455
+ function assertRemoteAdapterCapabilities(adapter, mode) {
10456
+ if (!adapter.capabilities.remotePersistence) {
10457
+ throw new Error(`${mode} storage adapter must set capabilities.remotePersistence=true`);
10458
+ }
10459
+ if (mode === "hybrid" && !adapter.capabilities.localPersistence) {
10460
+ throw new Error("hybrid storage adapter must also set capabilities.localPersistence=true");
10461
+ }
10462
+ }
10463
+ // src/storage/s3-artifacts.ts
10464
+ import { createHash as createHash3, createHmac } from "crypto";
10465
+ function createTodosS3ArtifactStore(options) {
10466
+ const requestFetch = options.fetch ?? fetch;
10467
+ const now2 = options.now ?? (() => new Date);
10468
+ return {
10469
+ objectKey: (relativePath) => buildS3ObjectKey(options.config, relativePath),
10470
+ objectUrl: (relativePath) => buildS3ObjectUrl(options.config, buildS3ObjectKey(options.config, relativePath)),
10471
+ async putObject(input) {
10472
+ const key = buildS3ObjectKey(options.config, input.key);
10473
+ const url = buildS3ObjectUrl(options.config, key);
10474
+ const body = normalizeBody(input.body);
10475
+ const headers = {
10476
+ "content-type": input.contentType ?? "application/octet-stream"
10477
+ };
10478
+ for (const [name, value] of Object.entries(input.metadata ?? {})) {
10479
+ headers[`x-amz-meta-${name.toLowerCase()}`] = value;
10480
+ }
10481
+ const signed = signAwsV4Request({
10482
+ method: "PUT",
10483
+ url,
10484
+ region: options.config.region ?? "us-east-1",
10485
+ service: "s3",
10486
+ headers,
10487
+ body,
10488
+ credentials: options.credentials,
10489
+ now: now2()
10490
+ });
10491
+ const response = await requestFetch(url, { method: "PUT", headers: signed.headers, body });
10492
+ if (!response.ok)
10493
+ throw new Error(`S3 put failed with HTTP ${response.status}`);
10494
+ return {
10495
+ bucket: options.config.bucket,
10496
+ key,
10497
+ url: url.toString(),
10498
+ etag: response.headers.get("etag") ?? undefined
10499
+ };
10500
+ },
10501
+ async getObject(relativePath) {
10502
+ const url = buildS3ObjectUrl(options.config, buildS3ObjectKey(options.config, relativePath));
10503
+ const signed = signAwsV4Request({
10504
+ method: "GET",
10505
+ url,
10506
+ region: options.config.region ?? "us-east-1",
10507
+ service: "s3",
10508
+ headers: {},
10509
+ credentials: options.credentials,
10510
+ now: now2()
10511
+ });
10512
+ const response = await requestFetch(url, { method: "GET", headers: signed.headers });
10513
+ if (!response.ok)
10514
+ throw new Error(`S3 get failed with HTTP ${response.status}`);
10515
+ return response;
10516
+ },
10517
+ async deleteObject(relativePath) {
10518
+ const url = buildS3ObjectUrl(options.config, buildS3ObjectKey(options.config, relativePath));
10519
+ const signed = signAwsV4Request({
10520
+ method: "DELETE",
10521
+ url,
10522
+ region: options.config.region ?? "us-east-1",
10523
+ service: "s3",
10524
+ headers: {},
10525
+ credentials: options.credentials,
10526
+ now: now2()
10527
+ });
10528
+ const response = await requestFetch(url, { method: "DELETE", headers: signed.headers });
10529
+ if (!response.ok && response.status !== 404)
10530
+ throw new Error(`S3 delete failed with HTTP ${response.status}`);
10531
+ }
10532
+ };
10533
+ }
10534
+ function buildS3ObjectKey(config, relativePath) {
10535
+ const prefix = normalizeS3Prefix(config.prefix);
10536
+ const key = normalizeS3Key(relativePath);
10537
+ return `${prefix}${key}`;
10538
+ }
10539
+ function buildS3ObjectUrl(config, key) {
10540
+ const encodedKey = encodeS3Path(key);
10541
+ if (config.endpoint || config.forcePathStyle) {
10542
+ const base = new URL(config.endpoint ?? `https://s3.${config.region ?? "us-east-1"}.amazonaws.com`);
10543
+ base.pathname = `${trimTrailingSlash(base.pathname)}/${encodeURIComponent(config.bucket)}/${encodedKey}`;
10544
+ return base;
10545
+ }
10546
+ return new URL(`https://${config.bucket}.s3.${config.region ?? "us-east-1"}.amazonaws.com/${encodedKey}`);
10547
+ }
10548
+ function signAwsV4Request(input) {
10549
+ const amzDate = toAmzDate(input.now);
10550
+ const dateStamp = amzDate.slice(0, 8);
10551
+ const bodyHash = sha256Hex(input.body ?? new Uint8Array);
10552
+ const headers = normalizeHeaders({
10553
+ ...input.headers ?? {},
10554
+ host: input.url.host,
10555
+ "x-amz-content-sha256": bodyHash,
10556
+ "x-amz-date": amzDate,
10557
+ ...input.credentials.sessionToken ? { "x-amz-security-token": input.credentials.sessionToken } : {}
10558
+ });
10559
+ const signedHeaders = Object.keys(headers).sort().join(";");
10560
+ const canonicalHeaders = Object.keys(headers).sort().map((name) => `${name}:${headers[name]}
10561
+ `).join("");
10562
+ const canonicalRequest = [
10563
+ input.method.toUpperCase(),
10564
+ input.url.pathname || "/",
10565
+ canonicalQuery(input.url),
10566
+ canonicalHeaders,
10567
+ signedHeaders,
10568
+ bodyHash
10569
+ ].join(`
10570
+ `);
10571
+ const credentialScope = `${dateStamp}/${input.region}/${input.service}/aws4_request`;
10572
+ const stringToSign = [
10573
+ "AWS4-HMAC-SHA256",
10574
+ amzDate,
10575
+ credentialScope,
10576
+ sha256Hex(canonicalRequest)
10577
+ ].join(`
10578
+ `);
10579
+ const signingKey = getSigningKey(input.credentials.secretAccessKey, dateStamp, input.region, input.service);
10580
+ const signature = hmacHex(signingKey, stringToSign);
10581
+ return {
10582
+ headers: {
10583
+ ...headers,
10584
+ authorization: [
10585
+ `AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${credentialScope}`,
10586
+ `SignedHeaders=${signedHeaders}`,
10587
+ `Signature=${signature}`
10588
+ ].join(", ")
10589
+ },
10590
+ canonicalRequest,
10591
+ stringToSign
10592
+ };
10593
+ }
10594
+ function normalizeBody(body) {
10595
+ if (typeof body === "string")
10596
+ return Buffer.from(body);
10597
+ if (body instanceof Uint8Array)
10598
+ return body;
10599
+ if (body instanceof ArrayBuffer)
10600
+ return new Uint8Array(body);
10601
+ throw new Error("S3 body must be a string, Uint8Array, Buffer, or ArrayBuffer");
10602
+ }
10603
+ function normalizeS3Prefix(value) {
10604
+ const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
10605
+ if (!normalized || normalized.includes(".."))
10606
+ throw new Error("Invalid S3 object key");
10607
+ return normalized.endsWith("/") ? normalized : `${normalized}/`;
10608
+ }
10609
+ function normalizeS3Key(value) {
10610
+ const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
10611
+ if (!normalized || normalized.includes("..") || normalized.endsWith("/"))
10612
+ throw new Error("Invalid S3 object key");
10613
+ return normalized;
10614
+ }
10615
+ function encodeS3Path(key) {
10616
+ return key.split("/").filter(Boolean).map((part) => encodeURIComponent(part)).join("/");
10617
+ }
10618
+ function trimTrailingSlash(value) {
10619
+ return value.replace(/\/+$/, "");
10620
+ }
10621
+ function canonicalQuery(url) {
10622
+ return [...url.searchParams.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
10623
+ }
10624
+ function normalizeHeaders(headers) {
10625
+ const entries = Object.entries(headers).map(([name, value]) => [
10626
+ name.toLowerCase(),
10627
+ value.trim().replace(/\s+/g, " ")
10628
+ ]);
10629
+ entries.sort((left, right) => left[0].localeCompare(right[0]));
10630
+ return Object.fromEntries(entries);
10631
+ }
10632
+ function toAmzDate(date) {
10633
+ return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
10634
+ }
10635
+ function sha256Hex(value) {
10636
+ return createHash3("sha256").update(value).digest("hex");
10637
+ }
10638
+ function hmac(key, value) {
10639
+ return createHmac("sha256", key).update(value).digest();
10640
+ }
10641
+ function hmacHex(key, value) {
10642
+ return createHmac("sha256", key).update(value).digest("hex");
10643
+ }
10644
+ function getSigningKey(secretAccessKey, dateStamp, region, service) {
10645
+ const dateKey = hmac(`AWS4${secretAccessKey}`, dateStamp);
10646
+ const dateRegionKey = hmac(dateKey, region);
10647
+ const dateRegionServiceKey = hmac(dateRegionKey, service);
10648
+ return hmac(dateRegionServiceKey, "aws4_request");
10649
+ }
10650
+ // src/storage/s3-artifact-sync.ts
10651
+ init_database();
10652
+ async function uploadRunArtifactsToS3(options) {
10653
+ const db = options.db ?? getDatabase();
10654
+ const now2 = options.now ?? (() => new Date);
10655
+ const result = emptyResult();
10656
+ for (const artifact of listRunArtifacts(db, options.filter)) {
10657
+ try {
10658
+ const metadata = artifact.metadata;
10659
+ if (!options.filter?.includeAlreadySynced && remoteRef(metadata)) {
10660
+ result.skipped += 1;
10661
+ continue;
10662
+ }
10663
+ const content = exportStoredArtifactContent({
10664
+ id: artifact.id,
10665
+ path: artifact.path,
10666
+ size_bytes: artifact.size_bytes,
10667
+ sha256: artifact.sha256,
10668
+ metadata
10669
+ });
10670
+ if (!content) {
10671
+ result.skipped += 1;
10672
+ continue;
10673
+ }
10674
+ const ref = await options.store.putObject({
10675
+ key: content.relative_path,
10676
+ body: Buffer.from(content.base64, "base64"),
10677
+ contentType: mediaType(metadata) ?? "application/octet-stream",
10678
+ metadata: {
10679
+ artifact_id: artifact.id,
10680
+ run_id: artifact.run_id,
10681
+ task_id: artifact.task_id,
10682
+ sha256: content.sha256
10683
+ }
10684
+ });
10685
+ const remote = {
10686
+ provider: "s3",
10687
+ bucket: ref.bucket,
10688
+ key: ref.key,
10689
+ relative_path: content.relative_path,
10690
+ url: ref.url,
10691
+ sha256: content.sha256,
10692
+ size_bytes: content.size_bytes,
10693
+ uploaded_at: now2().toISOString()
10694
+ };
10695
+ updateArtifactMetadata(db, artifact.id, {
10696
+ ...metadata,
10697
+ remote_artifact_store: remote
10698
+ });
10699
+ result.uploaded += 1;
10700
+ result.artifacts.push({
10701
+ id: artifact.id,
10702
+ run_id: artifact.run_id,
10703
+ task_id: artifact.task_id,
10704
+ key: ref.key,
10705
+ sha256: content.sha256,
10706
+ size_bytes: content.size_bytes
10707
+ });
10708
+ } catch (error) {
10709
+ result.errors.push(`${artifact.id}: ${error instanceof Error ? error.message : String(error)}`);
10710
+ }
10711
+ }
10712
+ return result;
10713
+ }
10714
+ function planRunArtifactsS3Sync(options) {
10715
+ const db = options.db ?? getDatabase();
10716
+ const plan = {
10717
+ direction: options.direction,
10718
+ dry_run: true,
10719
+ no_network: true,
10720
+ total: 0,
10721
+ uploadable: 0,
10722
+ downloadable: 0,
10723
+ skipped: 0,
10724
+ errors: [],
10725
+ artifacts: []
10726
+ };
10727
+ for (const artifact of listRunArtifacts(db, options.filter)) {
10728
+ plan.total += 1;
10729
+ try {
10730
+ const metadata = artifact.metadata;
10731
+ const remote = remoteRef(metadata);
10732
+ const integrity = verifyStoredArtifact({
10733
+ id: artifact.id,
10734
+ path: artifact.path,
10735
+ size_bytes: artifact.size_bytes,
10736
+ sha256: artifact.sha256,
10737
+ metadata
10738
+ });
10739
+ if (options.direction === "upload") {
10740
+ if (remote && !options.filter?.includeAlreadySynced) {
10741
+ plan.skipped += 1;
10742
+ plan.artifacts.push(planArtifact(artifact, "already_remote", remote));
10743
+ } else if (integrity.status === "ok") {
10744
+ plan.uploadable += 1;
10745
+ plan.artifacts.push(planArtifact(artifact, "uploadable", remote));
10746
+ } else {
10747
+ plan.skipped += 1;
10748
+ plan.artifacts.push(planArtifact(artifact, integrity.status === "metadata_only" ? "metadata_only" : "missing_local", remote));
10749
+ }
10750
+ } else if (!remote) {
10751
+ plan.skipped += 1;
10752
+ plan.artifacts.push(planArtifact(artifact, "missing_remote_ref", remote));
10753
+ } else if (!options.force && integrity.status === "ok") {
10754
+ plan.skipped += 1;
10755
+ plan.artifacts.push(planArtifact(artifact, "local_ok", remote));
10756
+ } else {
10757
+ plan.downloadable += 1;
10758
+ plan.artifacts.push(planArtifact(artifact, "downloadable", remote));
10759
+ }
10760
+ } catch (error) {
10761
+ plan.errors.push(`${artifact.id}: ${error instanceof Error ? error.message : String(error)}`);
10762
+ }
10763
+ }
10764
+ return plan;
10765
+ }
10766
+ async function downloadRunArtifactsFromS3(options) {
10767
+ const db = options.db ?? getDatabase();
10768
+ const now2 = options.now ?? (() => new Date);
10769
+ const result = emptyResult();
10770
+ for (const artifact of listRunArtifacts(db, options.filter)) {
10771
+ try {
10772
+ const metadata = artifact.metadata;
10773
+ const remote = remoteRef(metadata);
10774
+ if (!remote) {
10775
+ result.skipped += 1;
10776
+ continue;
10777
+ }
10778
+ const integrity = verifyStoredArtifact({
10779
+ id: artifact.id,
10780
+ path: artifact.path,
10781
+ size_bytes: artifact.size_bytes,
10782
+ sha256: artifact.sha256,
10783
+ metadata
10784
+ });
10785
+ if (!options.force && integrity.status === "ok") {
10786
+ result.skipped += 1;
10787
+ continue;
10788
+ }
10789
+ const response = await options.store.getObject(remote.relative_path);
10790
+ const bytes = Buffer.from(await response.arrayBuffer());
10791
+ const report = importStoredArtifactContent({
10792
+ artifact_id: artifact.id,
10793
+ relative_path: remote.relative_path,
10794
+ sha256: remote.sha256,
10795
+ size_bytes: remote.size_bytes,
10796
+ base64: bytes.toString("base64")
10797
+ });
10798
+ if (report.status !== "ok")
10799
+ throw new Error(report.message);
10800
+ updateArtifactMetadata(db, artifact.id, {
10801
+ ...metadata,
10802
+ remote_artifact_store: {
10803
+ ...remote,
10804
+ downloaded_at: now2().toISOString()
10805
+ }
10806
+ });
10807
+ result.downloaded += 1;
10808
+ result.artifacts.push({
10809
+ id: artifact.id,
10810
+ run_id: artifact.run_id,
10811
+ task_id: artifact.task_id,
10812
+ key: remote.key,
10813
+ sha256: remote.sha256,
10814
+ size_bytes: remote.size_bytes
10815
+ });
10816
+ } catch (error) {
10817
+ result.errors.push(`${artifact.id}: ${error instanceof Error ? error.message : String(error)}`);
10818
+ }
10819
+ }
10820
+ return result;
10821
+ }
10822
+ function emptyResult() {
10823
+ return { uploaded: 0, downloaded: 0, skipped: 0, errors: [], artifacts: [] };
10824
+ }
10825
+ function planArtifact(artifact, status, remote) {
10826
+ return {
10827
+ id: artifact.id,
10828
+ run_id: artifact.run_id,
10829
+ task_id: artifact.task_id,
10830
+ status,
10831
+ sha256: artifact.sha256,
10832
+ size_bytes: artifact.size_bytes,
10833
+ ...remote ? { remote_key: remote.key } : {}
10834
+ };
10835
+ }
10836
+ function listRunArtifacts(db, filter = {}) {
10837
+ const conditions = [];
10838
+ const values = [];
10839
+ if (filter.runId) {
10840
+ conditions.push("run_id = ?");
10841
+ values.push(filter.runId);
10842
+ }
10843
+ if (filter.taskId) {
10844
+ conditions.push("task_id = ?");
10845
+ values.push(filter.taskId);
10846
+ }
10847
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
10848
+ const limit = filter.limit && filter.limit > 0 ? " LIMIT ?" : "";
10849
+ const rows = db.query(`SELECT * FROM task_run_artifacts ${where} ORDER BY created_at, id${limit}`).all(...limit ? [...values, String(filter.limit)] : values);
10850
+ return rows.map((row) => ({ ...row, metadata: parseMetadata2(row.metadata) }));
10851
+ }
10852
+ function updateArtifactMetadata(db, id, metadata) {
10853
+ db.run("UPDATE task_run_artifacts SET metadata = ? WHERE id = ?", [JSON.stringify(metadata), id]);
10854
+ }
10855
+ function parseMetadata2(value) {
10856
+ if (!value)
10857
+ return {};
10858
+ if (typeof value === "object" && !Array.isArray(value))
10859
+ return value;
10860
+ if (typeof value !== "string")
10861
+ return {};
10862
+ try {
10863
+ const parsed = JSON.parse(value);
10864
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
10865
+ } catch {
10866
+ return {};
10867
+ }
10868
+ }
10869
+ function remoteRef(metadata) {
10870
+ const value = metadata["remote_artifact_store"];
10871
+ if (!value || typeof value !== "object" || Array.isArray(value))
10872
+ return null;
10873
+ const record = value;
10874
+ if (record.provider !== "s3")
10875
+ return null;
10876
+ if (typeof record.bucket !== "string" || typeof record.key !== "string" || typeof record.relative_path !== "string" || typeof record.url !== "string" || typeof record.sha256 !== "string" || typeof record.size_bytes !== "number") {
10877
+ return null;
10878
+ }
10879
+ return record;
10880
+ }
10881
+ function mediaType(metadata) {
10882
+ const store = metadata["artifact_store"];
10883
+ if (!store || typeof store !== "object" || Array.isArray(store))
10884
+ return null;
10885
+ const value = store["media_type"];
10886
+ return typeof value === "string" ? value : null;
10887
+ }
8879
10888
  export {
8880
- createLocalSqliteTodosStorageAdapter
10889
+ uploadRunArtifactsToS3,
10890
+ signAwsV4Request,
10891
+ postgresTodosSyncSchemaSql,
10892
+ planRunArtifactsS3Sync,
10893
+ parseStorageMode,
10894
+ loadTodosStorageConfig,
10895
+ loadStorageConfig,
10896
+ isTodosRemoteStorageEnabled,
10897
+ importSqliteTodosStorageSnapshot,
10898
+ getTodosStorageMode,
10899
+ getTodosStorageEnvName,
10900
+ getTodosStorageDatabaseUrl,
10901
+ getTodosStorageDatabaseEnv,
10902
+ getStorageMode,
10903
+ getStorageDatabaseUrl,
10904
+ getStorageDatabaseEnv,
10905
+ getCanonicalTodosRdsConfig,
10906
+ exportSqliteTodosStorageSnapshot,
10907
+ downloadRunArtifactsFromS3,
10908
+ createTodosStorageAdapter,
10909
+ createTodosS3ArtifactStore,
10910
+ createPostgresTodosSyncStore,
10911
+ createPostgresTodosStorageAdapter,
10912
+ createLocalSqliteTodosStorageAdapter,
10913
+ createHybridTodosStorageAdapter,
10914
+ buildS3ObjectUrl,
10915
+ buildS3ObjectKey,
10916
+ assertTodosRemoteStorageConfig,
10917
+ TODOS_STORAGE_TABLES,
10918
+ TODOS_STORAGE_FALLBACK_ENV,
10919
+ TODOS_STORAGE_ENV,
10920
+ STORAGE_TABLES,
10921
+ CANONICAL_TODOS_RDS_RUNTIME_PATH,
10922
+ CANONICAL_TODOS_RDS_DATABASE,
10923
+ CANONICAL_TODOS_RDS_CLUSTER
8881
10924
  };