@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/index.js CHANGED
@@ -1223,6 +1223,15 @@ function ensureSchema(db) {
1223
1223
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1224
1224
  tag TEXT NOT NULL, PRIMARY KEY (task_id, tag)
1225
1225
  )`);
1226
+ ensureTable("tags", `
1227
+ CREATE TABLE tags (
1228
+ id TEXT PRIMARY KEY,
1229
+ name TEXT NOT NULL UNIQUE,
1230
+ color TEXT,
1231
+ description TEXT,
1232
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1233
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1234
+ )`);
1226
1235
  ensureTable("task_dependencies", `
1227
1236
  CREATE TABLE task_dependencies (
1228
1237
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
@@ -1436,6 +1445,36 @@ function ensureSchema(db) {
1436
1445
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_task ON task_git_refs(task_id)");
1437
1446
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_lookup ON task_git_refs(ref_type, name)");
1438
1447
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_url ON task_git_refs(url)");
1448
+ ensureTable("task_commits", `
1449
+ CREATE TABLE task_commits (
1450
+ id TEXT PRIMARY KEY,
1451
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1452
+ sha TEXT NOT NULL,
1453
+ message TEXT,
1454
+ author TEXT,
1455
+ files_changed TEXT,
1456
+ committed_at TEXT,
1457
+ branch TEXT,
1458
+ pr_url TEXT,
1459
+ pr_number INTEGER,
1460
+ pr_state TEXT,
1461
+ ci_snapshot TEXT,
1462
+ release_tag TEXT,
1463
+ repo_path TEXT,
1464
+ traceability TEXT DEFAULT '{}',
1465
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1466
+ UNIQUE(task_id, sha)
1467
+ )`);
1468
+ ensureColumn("task_commits", "branch", "TEXT");
1469
+ ensureColumn("task_commits", "pr_url", "TEXT");
1470
+ ensureColumn("task_commits", "pr_number", "INTEGER");
1471
+ ensureColumn("task_commits", "pr_state", "TEXT");
1472
+ ensureColumn("task_commits", "ci_snapshot", "TEXT");
1473
+ ensureColumn("task_commits", "release_tag", "TEXT");
1474
+ ensureColumn("task_commits", "repo_path", "TEXT");
1475
+ ensureColumn("task_commits", "traceability", "TEXT DEFAULT '{}'");
1476
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_commits_task ON task_commits(task_id)");
1477
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_commits_sha ON task_commits(sha)");
1439
1478
  ensureTable("task_verifications", `
1440
1479
  CREATE TABLE task_verifications (
1441
1480
  id TEXT PRIMARY KEY,
@@ -1620,6 +1659,9 @@ function ensureSchema(db) {
1620
1659
  ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
1621
1660
  ensureColumn("tasks", "retry_after", "TEXT");
1622
1661
  ensureColumn("tasks", "sla_minutes", "INTEGER");
1662
+ ensureColumn("tasks", "scheduled_start_at", "TEXT");
1663
+ ensureColumn("tasks", "priority_score", "INTEGER");
1664
+ ensureColumn("tasks", "priority_reason", "TEXT");
1623
1665
  ensureColumn("tasks", "archived_at", "TEXT");
1624
1666
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
1625
1667
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
@@ -1742,6 +1784,7 @@ function ensureSchema(db) {
1742
1784
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug)");
1743
1785
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag)");
1744
1786
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id)");
1787
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)");
1745
1788
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
1746
1789
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
1747
1790
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
@@ -1879,6 +1922,217 @@ function ensureSchema(db) {
1879
1922
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_cycles_dates ON cycles(start_date, end_date)");
1880
1923
  ensureColumn("tasks", "cycle_id", "TEXT REFERENCES cycles(id) ON DELETE SET NULL");
1881
1924
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_cycle ON tasks(cycle_id) WHERE cycle_id IS NOT NULL");
1925
+ ensureTable("labels", `
1926
+ CREATE TABLE labels (
1927
+ id TEXT PRIMARY KEY,
1928
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
1929
+ name TEXT NOT NULL,
1930
+ color TEXT,
1931
+ description TEXT,
1932
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1933
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1934
+ UNIQUE(project_id, name)
1935
+ )`);
1936
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_labels_project ON labels(project_id)");
1937
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_labels_name ON labels(name)");
1938
+ ensureTable("task_labels", `
1939
+ CREATE TABLE task_labels (
1940
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1941
+ label_id TEXT NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
1942
+ PRIMARY KEY (task_id, label_id)
1943
+ )`);
1944
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_labels_task ON task_labels(task_id)");
1945
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_labels_label ON task_labels(label_id)");
1946
+ ensureTable("custom_field_definitions", `
1947
+ CREATE TABLE custom_field_definitions (
1948
+ id TEXT PRIMARY KEY,
1949
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
1950
+ name TEXT NOT NULL,
1951
+ slug TEXT NOT NULL,
1952
+ field_type TEXT NOT NULL CHECK(field_type IN ('text', 'number', 'boolean', 'date', 'enum')),
1953
+ options TEXT DEFAULT '[]',
1954
+ required INTEGER NOT NULL DEFAULT 0,
1955
+ default_value TEXT,
1956
+ sort_order INTEGER NOT NULL DEFAULT 0,
1957
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1958
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1959
+ UNIQUE(project_id, slug)
1960
+ )`);
1961
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_custom_fields_project ON custom_field_definitions(project_id)");
1962
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_custom_fields_slug ON custom_field_definitions(slug)");
1963
+ ensureTable("task_custom_field_values", `
1964
+ CREATE TABLE task_custom_field_values (
1965
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1966
+ field_id TEXT NOT NULL REFERENCES custom_field_definitions(id) ON DELETE CASCADE,
1967
+ value TEXT,
1968
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1969
+ PRIMARY KEY (task_id, field_id)
1970
+ )`);
1971
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_custom_values_task ON task_custom_field_values(task_id)");
1972
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_custom_values_field ON task_custom_field_values(field_id)");
1973
+ ensureTable("saved_views", `
1974
+ CREATE TABLE saved_views (
1975
+ id TEXT PRIMARY KEY,
1976
+ name TEXT NOT NULL UNIQUE,
1977
+ slug TEXT NOT NULL UNIQUE,
1978
+ entity_type TEXT NOT NULL DEFAULT 'task',
1979
+ filters TEXT NOT NULL DEFAULT '{}',
1980
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1981
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1982
+ )`);
1983
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_saved_views_slug ON saved_views(slug)");
1984
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_saved_views_entity ON saved_views(entity_type)");
1985
+ ensureTable("decision_records", `
1986
+ CREATE TABLE decision_records (
1987
+ id TEXT PRIMARY KEY,
1988
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1989
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1990
+ plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1991
+ agent_id TEXT,
1992
+ sequence_num INTEGER NOT NULL,
1993
+ short_ref TEXT NOT NULL UNIQUE,
1994
+ title TEXT NOT NULL,
1995
+ status TEXT NOT NULL DEFAULT 'proposed' CHECK(status IN ('proposed', 'accepted', 'deprecated', 'superseded', 'rejected')),
1996
+ context TEXT,
1997
+ decision TEXT NOT NULL,
1998
+ consequences TEXT,
1999
+ alternatives TEXT DEFAULT '[]',
2000
+ tags TEXT DEFAULT '[]',
2001
+ supersedes_id TEXT REFERENCES decision_records(id) ON DELETE SET NULL,
2002
+ superseded_by_id TEXT REFERENCES decision_records(id) ON DELETE SET NULL,
2003
+ metadata TEXT DEFAULT '{}',
2004
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2005
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2006
+ )`);
2007
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_project ON decision_records(project_id)");
2008
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_task ON decision_records(task_id)");
2009
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_plan ON decision_records(plan_id)");
2010
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_status ON decision_records(status)");
2011
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_short_ref ON decision_records(short_ref)");
2012
+ ensureTable("knowledge_snapshots", `
2013
+ CREATE TABLE knowledge_snapshots (
2014
+ id TEXT PRIMARY KEY,
2015
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
2016
+ title TEXT NOT NULL,
2017
+ summary TEXT,
2018
+ content_hash TEXT NOT NULL,
2019
+ snapshot TEXT NOT NULL,
2020
+ decision_ids TEXT DEFAULT '[]',
2021
+ topics TEXT DEFAULT '[]',
2022
+ source TEXT NOT NULL DEFAULT 'auto' CHECK(source IN ('manual', 'auto', 'import')),
2023
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2024
+ )`);
2025
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_knowledge_snapshots_project ON knowledge_snapshots(project_id)");
2026
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_knowledge_snapshots_hash ON knowledge_snapshots(content_hash)");
2027
+ ensureTable("verification_records", `
2028
+ CREATE TABLE verification_records (
2029
+ id TEXT PRIMARY KEY,
2030
+ task_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
2031
+ provider_name TEXT NOT NULL,
2032
+ provider_type TEXT NOT NULL,
2033
+ status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
2034
+ summary TEXT,
2035
+ evidence TEXT DEFAULT '{}',
2036
+ artifact_id TEXT,
2037
+ started_at TEXT,
2038
+ completed_at TEXT,
2039
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2040
+ )`);
2041
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_verification_records_task ON verification_records(task_id)");
2042
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_verification_records_status ON verification_records(status)");
2043
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_verification_records_created ON verification_records(created_at)");
2044
+ ensureTable("task_leases", `
2045
+ CREATE TABLE task_leases (
2046
+ task_id TEXT PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE,
2047
+ agent_id TEXT NOT NULL,
2048
+ acquired_at TEXT NOT NULL,
2049
+ expires_at TEXT NOT NULL,
2050
+ heartbeat_at TEXT,
2051
+ steal_count INTEGER NOT NULL DEFAULT 0,
2052
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2053
+ )`);
2054
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_leases_agent ON task_leases(agent_id)");
2055
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_leases_expires ON task_leases(expires_at)");
2056
+ ensureTable("reminder_preferences", `
2057
+ CREATE TABLE reminder_preferences (
2058
+ id TEXT PRIMARY KEY,
2059
+ due_soon_hours INTEGER NOT NULL DEFAULT 24,
2060
+ sla_warning_minutes INTEGER NOT NULL DEFAULT 30,
2061
+ enabled INTEGER NOT NULL DEFAULT 1,
2062
+ desktop_notify INTEGER NOT NULL DEFAULT 0,
2063
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2064
+ )`);
2065
+ ensureTable("notification_reminders", `
2066
+ CREATE TABLE notification_reminders (
2067
+ id TEXT PRIMARY KEY,
2068
+ task_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
2069
+ reminder_type TEXT NOT NULL CHECK(reminder_type IN ('due_soon', 'due_overdue', 'sla_warning', 'sla_breach', 'custom')),
2070
+ title TEXT NOT NULL,
2071
+ message TEXT,
2072
+ trigger_at TEXT NOT NULL,
2073
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'fired', 'dismissed', 'snoozed')),
2074
+ snoozed_until TEXT,
2075
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
2076
+ agent_id TEXT,
2077
+ priority TEXT NOT NULL DEFAULT 'medium',
2078
+ metadata TEXT DEFAULT '{}',
2079
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2080
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
2081
+ fired_at TEXT,
2082
+ dismissed_at TEXT
2083
+ )`);
2084
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_task ON notification_reminders(task_id)");
2085
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_status ON notification_reminders(status)");
2086
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_trigger ON notification_reminders(trigger_at)");
2087
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_project ON notification_reminders(project_id)");
2088
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_agent ON notification_reminders(agent_id)");
2089
+ ensureTable("run_records", `
2090
+ CREATE TABLE run_records (
2091
+ id TEXT PRIMARY KEY,
2092
+ agent_run_id TEXT,
2093
+ agent_id TEXT,
2094
+ objective TEXT,
2095
+ plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
2096
+ claimed_task_ids TEXT DEFAULT '[]',
2097
+ commands TEXT DEFAULT '[]',
2098
+ stdout_summary TEXT,
2099
+ stderr_summary TEXT,
2100
+ files_touched TEXT DEFAULT '[]',
2101
+ verification_results TEXT DEFAULT '[]',
2102
+ artifact_ids TEXT DEFAULT '[]',
2103
+ status_transitions TEXT DEFAULT '[]',
2104
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'failed', 'archived')),
2105
+ replay_bundle TEXT,
2106
+ metadata TEXT DEFAULT '{}',
2107
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
2108
+ completed_at TEXT,
2109
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2110
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2111
+ )`);
2112
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_agent_run ON run_records(agent_run_id)");
2113
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_agent ON run_records(agent_id)");
2114
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_plan ON run_records(plan_id)");
2115
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_status ON run_records(status)");
2116
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_started ON run_records(started_at)");
2117
+ ensureTable("activity_log", `
2118
+ CREATE TABLE activity_log (
2119
+ id TEXT PRIMARY KEY,
2120
+ entity_type TEXT NOT NULL,
2121
+ entity_id TEXT NOT NULL,
2122
+ action TEXT NOT NULL,
2123
+ field TEXT,
2124
+ old_value TEXT,
2125
+ new_value TEXT,
2126
+ actor_id TEXT,
2127
+ session_id TEXT,
2128
+ machine_id TEXT,
2129
+ metadata TEXT DEFAULT '{}',
2130
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2131
+ )`);
2132
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_activity_log_entity ON activity_log(entity_type, entity_id)");
2133
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_activity_log_actor ON activity_log(actor_id)");
2134
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_activity_log_action ON activity_log(action)");
2135
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_activity_log_created ON activity_log(created_at)");
1882
2136
  ensureTable("api_keys", `
1883
2137
  CREATE TABLE api_keys (
1884
2138
  id TEXT PRIMARY KEY,
@@ -2315,11 +2569,17 @@ function ensureDir(filePath) {
2315
2569
  }
2316
2570
  }
2317
2571
  function getDatabase(dbPath) {
2318
- if (_db)
2319
- return _db;
2320
2572
  const path = dbPath || getDbPath();
2573
+ if (_db && _dbPath === path)
2574
+ return _db;
2575
+ if (_db && _dbPath !== path) {
2576
+ _db.close();
2577
+ _db = null;
2578
+ _dbPath = null;
2579
+ }
2321
2580
  ensureDir(path);
2322
2581
  _db = new Database(path);
2582
+ _dbPath = path;
2323
2583
  _db.run("PRAGMA journal_mode = WAL");
2324
2584
  _db.run("PRAGMA busy_timeout = 5000");
2325
2585
  _db.run("PRAGMA foreign_keys = ON");
@@ -2332,10 +2592,12 @@ function closeDatabase() {
2332
2592
  if (_db) {
2333
2593
  _db.close();
2334
2594
  _db = null;
2595
+ _dbPath = null;
2335
2596
  }
2336
2597
  }
2337
2598
  function resetDatabase() {
2338
2599
  _db = null;
2600
+ _dbPath = null;
2339
2601
  }
2340
2602
  function now() {
2341
2603
  return new Date().toISOString();
@@ -2396,7 +2658,7 @@ function resolvePartialId(db, table, partialId) {
2396
2658
  }
2397
2659
  return null;
2398
2660
  }
2399
- var LOCK_EXPIRY_MINUTES = 30, _db = null, ALLOWED_TABLES;
2661
+ var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
2400
2662
  var init_database = __esm(() => {
2401
2663
  init_schema();
2402
2664
  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 },
@@ -9738,7 +10001,7 @@ function storeArtifactFile(input) {
9738
10001
  }
9739
10002
  return { sourcePath, localPath, contentHash, mimeType, sizeBytes: buffer.length };
9740
10003
  }
9741
- function deleteStoredArtifactFile(localPath, storageMode, _dbPath) {
10004
+ function deleteStoredArtifactFile(localPath, storageMode, _dbPath2) {
9742
10005
  if (storageMode === "reference")
9743
10006
  return false;
9744
10007
  if (!localPath || !existsSync7(localPath))
@@ -9993,11 +10256,19 @@ function getTaskVerifications(taskId, db) {
9993
10256
  }
9994
10257
  function getTaskTraceability(taskId, db) {
9995
10258
  const d = db || getDatabase();
10259
+ const commits = getTaskCommits(taskId, d);
10260
+ const gitRefs = getTaskGitRefs(taskId, d);
9996
10261
  return {
9997
10262
  task_id: taskId,
9998
- commits: getTaskCommits(taskId, d),
9999
- git_refs: getTaskGitRefs(taskId, d),
10000
- verifications: getTaskVerifications(taskId, d)
10263
+ commits,
10264
+ git_refs: gitRefs,
10265
+ verifications: getTaskVerifications(taskId, d),
10266
+ branches: Array.from(new Set([
10267
+ ...commits.map((commit) => commit.branch).filter((branch) => Boolean(branch)),
10268
+ ...gitRefs.filter((ref) => ref.ref_type === "branch").map((ref) => ref.name)
10269
+ ])).sort(),
10270
+ release_tags: Array.from(new Set(commits.map((commit) => commit.release_tag).filter((tag) => Boolean(tag)))).sort(),
10271
+ pull_requests: commits.filter((commit) => commit.pr_url).map((commit) => ({ url: commit.pr_url, state: commit.pr_state, number: commit.pr_number }))
10001
10272
  };
10002
10273
  }
10003
10274
 
@@ -17057,6 +17328,16 @@ function parseClock(value) {
17057
17328
  throw new Error("quiet hours must use HH:MM-HH:MM");
17058
17329
  return Number(match[1]) * 60 + Number(match[2]);
17059
17330
  }
17331
+ function parseQuietHours(value, timezone = "local") {
17332
+ if (!value)
17333
+ return;
17334
+ const [start, end] = value.split("-", 2).map((part) => part.trim());
17335
+ if (!start || !end)
17336
+ throw new Error("quiet hours must use HH:MM-HH:MM");
17337
+ parseClock(start);
17338
+ parseClock(end);
17339
+ return { start, end, timezone };
17340
+ }
17060
17341
  function isQuietTime(quiet, timestamp2) {
17061
17342
  if (!quiet)
17062
17343
  return false;
@@ -18979,6 +19260,168 @@ var TODOS_REGISTRY = createTodosRegistry({
18979
19260
  generatedAt: "1970-01-01T00:00:00.000Z"
18980
19261
  });
18981
19262
 
19263
+ // src/storage/config.ts
19264
+ var TODOS_STORAGE_TABLES = [
19265
+ "todos_sync_records",
19266
+ "todos_sync_cursors"
19267
+ ];
19268
+ var STORAGE_TABLES = TODOS_STORAGE_TABLES;
19269
+ var TODOS_STORAGE_ENV = {
19270
+ mode: "HASNA_TODOS_STORAGE_MODE",
19271
+ databaseUrl: "HASNA_TODOS_DATABASE_URL",
19272
+ databaseSsl: "HASNA_TODOS_DATABASE_SSL",
19273
+ databaseSchema: "HASNA_TODOS_DATABASE_SCHEMA",
19274
+ s3Bucket: "HASNA_TODOS_S3_BUCKET",
19275
+ s3Prefix: "HASNA_TODOS_S3_PREFIX",
19276
+ awsRegion: "HASNA_TODOS_AWS_REGION",
19277
+ s3Endpoint: "HASNA_TODOS_S3_ENDPOINT",
19278
+ s3ForcePathStyle: "HASNA_TODOS_S3_FORCE_PATH_STYLE",
19279
+ s3AccessKeyId: "HASNA_TODOS_S3_ACCESS_KEY_ID",
19280
+ s3SecretAccessKey: "HASNA_TODOS_S3_SECRET_ACCESS_KEY",
19281
+ s3SessionToken: "HASNA_TODOS_S3_SESSION_TOKEN",
19282
+ syncBatchSize: "HASNA_TODOS_SYNC_BATCH_SIZE",
19283
+ syncDryRun: "HASNA_TODOS_SYNC_DRY_RUN"
19284
+ };
19285
+ var TODOS_STORAGE_FALLBACK_ENV = {
19286
+ mode: "TODOS_STORAGE_MODE",
19287
+ databaseUrl: "TODOS_DATABASE_URL",
19288
+ databaseSsl: "TODOS_DATABASE_SSL",
19289
+ databaseSchema: "TODOS_DATABASE_SCHEMA",
19290
+ s3Bucket: "TODOS_S3_BUCKET",
19291
+ s3Prefix: "TODOS_S3_PREFIX",
19292
+ awsRegion: "TODOS_AWS_REGION",
19293
+ s3Endpoint: "TODOS_S3_ENDPOINT",
19294
+ s3ForcePathStyle: "TODOS_S3_FORCE_PATH_STYLE",
19295
+ s3AccessKeyId: "TODOS_S3_ACCESS_KEY_ID",
19296
+ s3SecretAccessKey: "TODOS_S3_SECRET_ACCESS_KEY",
19297
+ s3SessionToken: "TODOS_S3_SESSION_TOKEN",
19298
+ syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
19299
+ syncDryRun: "TODOS_SYNC_DRY_RUN"
19300
+ };
19301
+ var CANONICAL_TODOS_RDS_CLUSTER = "hasna-xyz-infra-apps-prod-postgres";
19302
+ var CANONICAL_TODOS_RDS_DATABASE = "todos";
19303
+ var CANONICAL_TODOS_RDS_RUNTIME_PATH = "hasna/xyz/opensource/todos/prod/rds";
19304
+ function getCanonicalTodosRdsConfig() {
19305
+ return {
19306
+ cluster: CANONICAL_TODOS_RDS_CLUSTER,
19307
+ database: CANONICAL_TODOS_RDS_DATABASE,
19308
+ runtimeSecretPath: CANONICAL_TODOS_RDS_RUNTIME_PATH,
19309
+ primaryEnv: TODOS_STORAGE_ENV.databaseUrl,
19310
+ fallbackEnv: TODOS_STORAGE_FALLBACK_ENV.databaseUrl
19311
+ };
19312
+ }
19313
+ function loadTodosStorageConfig(env = process.env) {
19314
+ const mode = getTodosStorageMode(env);
19315
+ const databaseUrl = getTodosStorageDatabaseUrl(env);
19316
+ const bucket = readStorageEnv(env, "s3Bucket").value;
19317
+ const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
19318
+ const region = readStorageEnv(env, "awsRegion").value;
19319
+ const endpoint = readStorageEnv(env, "s3Endpoint").value;
19320
+ const schema = readStorageEnv(env, "databaseSchema").value;
19321
+ return {
19322
+ service: "todos",
19323
+ mode,
19324
+ ...databaseUrl ? {
19325
+ database: {
19326
+ provider: "postgres",
19327
+ url: databaseUrl,
19328
+ ssl: parseBoolean(readStorageEnv(env, "databaseSsl").value, true),
19329
+ ...schema ? { schema } : {}
19330
+ }
19331
+ } : {},
19332
+ ...bucket ? {
19333
+ objectStorage: {
19334
+ provider: "s3",
19335
+ bucket,
19336
+ prefix,
19337
+ ...region ? { region } : {},
19338
+ ...endpoint ? { endpoint } : {},
19339
+ forcePathStyle: parseBoolean(readStorageEnv(env, "s3ForcePathStyle").value, false)
19340
+ }
19341
+ } : {},
19342
+ sync: {
19343
+ batchSize: parsePositiveInteger(readStorageEnv(env, "syncBatchSize").value, 500),
19344
+ dryRun: parseBoolean(readStorageEnv(env, "syncDryRun").value, false)
19345
+ }
19346
+ };
19347
+ }
19348
+ function loadStorageConfig(env = process.env) {
19349
+ return loadTodosStorageConfig(env);
19350
+ }
19351
+ function isTodosRemoteStorageEnabled(config) {
19352
+ return config.mode === "remote" || config.mode === "hybrid";
19353
+ }
19354
+ function assertTodosRemoteStorageConfig(config) {
19355
+ if (!isTodosRemoteStorageEnabled(config))
19356
+ return;
19357
+ if (!config.database?.url) {
19358
+ throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
19359
+ }
19360
+ }
19361
+ function parseStorageMode(value) {
19362
+ const normalized = clean(value)?.toLowerCase();
19363
+ if (!normalized)
19364
+ return "local";
19365
+ if (normalized === "local" || normalized === "remote" || normalized === "hybrid")
19366
+ return normalized;
19367
+ throw new Error(`${TODOS_STORAGE_ENV.mode} must be local, remote, or hybrid`);
19368
+ }
19369
+ function getTodosStorageMode(env = process.env) {
19370
+ return parseStorageMode(readStorageEnv(env, "mode").value);
19371
+ }
19372
+ function getStorageMode(env = process.env) {
19373
+ return getTodosStorageMode(env);
19374
+ }
19375
+ function getTodosStorageDatabaseEnv(env = process.env) {
19376
+ return readStorageEnv(env, "databaseUrl").name;
19377
+ }
19378
+ function getStorageDatabaseEnv(env = process.env) {
19379
+ return getTodosStorageDatabaseEnv(env);
19380
+ }
19381
+ function getTodosStorageDatabaseUrl(env = process.env) {
19382
+ return readStorageEnv(env, "databaseUrl").value;
19383
+ }
19384
+ function getStorageDatabaseUrl(env = process.env) {
19385
+ return getTodosStorageDatabaseUrl(env);
19386
+ }
19387
+ function getTodosStorageEnvName(env, key) {
19388
+ return readStorageEnv(env, key).name;
19389
+ }
19390
+ function readStorageEnv(env, key) {
19391
+ const primaryName = TODOS_STORAGE_ENV[key];
19392
+ const primaryValue = clean(env[primaryName]);
19393
+ if (primaryValue !== undefined)
19394
+ return { name: primaryName, value: primaryValue };
19395
+ const fallbackName = TODOS_STORAGE_FALLBACK_ENV[key];
19396
+ const fallbackValue = clean(env[fallbackName]);
19397
+ if (fallbackValue !== undefined)
19398
+ return { name: fallbackName, value: fallbackValue };
19399
+ return { name: primaryName };
19400
+ }
19401
+ function clean(value) {
19402
+ const trimmed = value?.trim();
19403
+ return trimmed ? trimmed : undefined;
19404
+ }
19405
+ function parseBoolean(value, fallback) {
19406
+ const normalized = clean(value)?.toLowerCase();
19407
+ if (!normalized)
19408
+ return fallback;
19409
+ if (["1", "true", "yes", "on"].includes(normalized))
19410
+ return true;
19411
+ if (["0", "false", "no", "off"].includes(normalized))
19412
+ return false;
19413
+ throw new Error(`Expected boolean env value, got ${value}`);
19414
+ }
19415
+ function parsePositiveInteger(value, fallback) {
19416
+ const normalized = clean(value);
19417
+ if (!normalized)
19418
+ return fallback;
19419
+ const parsed = Number.parseInt(normalized, 10);
19420
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
19421
+ throw new Error(`${TODOS_STORAGE_ENV.syncBatchSize} must be a positive integer`);
19422
+ }
19423
+ return parsed;
19424
+ }
18982
19425
  // src/db/agents.ts
18983
19426
  init_database();
18984
19427
 
@@ -19693,6 +20136,260 @@ function ensureTaskList(name, slug, projectId, db) {
19693
20136
 
19694
20137
  // src/storage/local-sqlite.ts
19695
20138
  init_database();
20139
+
20140
+ // src/storage/sqlite-snapshot.ts
20141
+ init_database();
20142
+ var PROJECT_COLUMNS = [
20143
+ "id",
20144
+ "name",
20145
+ "path",
20146
+ "description",
20147
+ "task_list_id",
20148
+ "task_prefix",
20149
+ "task_counter",
20150
+ "created_at",
20151
+ "updated_at",
20152
+ "machine_id",
20153
+ "synced_at"
20154
+ ];
20155
+ var TASK_LIST_COLUMNS = [
20156
+ "id",
20157
+ "project_id",
20158
+ "slug",
20159
+ "name",
20160
+ "description",
20161
+ "metadata",
20162
+ "created_at",
20163
+ "updated_at",
20164
+ "machine_id",
20165
+ "synced_at"
20166
+ ];
20167
+ var PLAN_COLUMNS = [
20168
+ "id",
20169
+ "project_id",
20170
+ "task_list_id",
20171
+ "agent_id",
20172
+ "name",
20173
+ "description",
20174
+ "status",
20175
+ "created_at",
20176
+ "updated_at",
20177
+ "machine_id",
20178
+ "synced_at"
20179
+ ];
20180
+ var AGENT_COLUMNS = [
20181
+ "id",
20182
+ "name",
20183
+ "description",
20184
+ "role",
20185
+ "title",
20186
+ "level",
20187
+ "permissions",
20188
+ "capabilities",
20189
+ "reports_to",
20190
+ "org_id",
20191
+ "metadata",
20192
+ "status",
20193
+ "created_at",
20194
+ "last_seen_at",
20195
+ "session_id",
20196
+ "working_dir",
20197
+ "active_project_id",
20198
+ "machine_id",
20199
+ "synced_at"
20200
+ ];
20201
+ var TEMPLATE_COLUMNS = [
20202
+ "id",
20203
+ "name",
20204
+ "title_pattern",
20205
+ "description",
20206
+ "priority",
20207
+ "tags",
20208
+ "variables",
20209
+ "project_id",
20210
+ "plan_id",
20211
+ "metadata",
20212
+ "version",
20213
+ "created_at",
20214
+ "machine_id",
20215
+ "synced_at"
20216
+ ];
20217
+ var TASK_COLUMNS = [
20218
+ "id",
20219
+ "short_id",
20220
+ "project_id",
20221
+ "parent_id",
20222
+ "plan_id",
20223
+ "task_list_id",
20224
+ "title",
20225
+ "description",
20226
+ "status",
20227
+ "priority",
20228
+ "agent_id",
20229
+ "assigned_to",
20230
+ "session_id",
20231
+ "working_dir",
20232
+ "tags",
20233
+ "metadata",
20234
+ "version",
20235
+ "locked_by",
20236
+ "locked_at",
20237
+ "created_at",
20238
+ "updated_at",
20239
+ "started_at",
20240
+ "completed_at",
20241
+ "due_at",
20242
+ "estimated_minutes",
20243
+ "actual_minutes",
20244
+ "requires_approval",
20245
+ "approved_by",
20246
+ "approved_at",
20247
+ "recurrence_rule",
20248
+ "recurrence_parent_id",
20249
+ "spawns_template_id",
20250
+ "confidence",
20251
+ "reason",
20252
+ "spawned_from_session",
20253
+ "assigned_by",
20254
+ "assigned_from_project",
20255
+ "task_type",
20256
+ "cost_tokens",
20257
+ "cost_usd",
20258
+ "delegated_from",
20259
+ "delegation_depth",
20260
+ "retry_count",
20261
+ "max_retries",
20262
+ "retry_after",
20263
+ "sla_minutes",
20264
+ "runner_id",
20265
+ "runner_started_at",
20266
+ "runner_completed_at",
20267
+ "current_step",
20268
+ "total_steps",
20269
+ "cycle_id",
20270
+ "machine_id",
20271
+ "synced_at",
20272
+ "archived_at"
20273
+ ];
20274
+ var AUDIT_COLUMNS = [
20275
+ "id",
20276
+ "task_id",
20277
+ "action",
20278
+ "field",
20279
+ "old_value",
20280
+ "new_value",
20281
+ "agent_id",
20282
+ "created_at",
20283
+ "machine_id"
20284
+ ];
20285
+ var JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables"]);
20286
+ var BOOLEAN_COLUMNS = new Set(["requires_approval"]);
20287
+ function exportSqliteTodosStorageSnapshot(db) {
20288
+ const d = db ?? getDatabase();
20289
+ return {
20290
+ exportedAt: new Date().toISOString(),
20291
+ source: "sqlite",
20292
+ tasks: listTasks({ include_archived: true }, d),
20293
+ projects: listProjects(d),
20294
+ plans: listPlans(undefined, d),
20295
+ agents: listAgents({ include_archived: true }, d),
20296
+ taskLists: listTaskLists(undefined, d),
20297
+ templates: listTemplates(d),
20298
+ auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d)
20299
+ };
20300
+ }
20301
+ function importSqliteTodosStorageSnapshot(snapshot, db) {
20302
+ const d = db ?? getDatabase();
20303
+ const result = {
20304
+ inserted: 0,
20305
+ updated: 0,
20306
+ skipped: 0,
20307
+ errors: []
20308
+ };
20309
+ const applyRows = (table, columns, rows, updateClockColumn, afterUpsert) => {
20310
+ for (const row of rows) {
20311
+ try {
20312
+ const record = asRecord2(row);
20313
+ const state = upsertById(d, table, columns, record, updateClockColumn);
20314
+ if (state === "inserted")
20315
+ result.inserted += 1;
20316
+ else if (state === "updated")
20317
+ result.updated += 1;
20318
+ else
20319
+ result.skipped += 1;
20320
+ afterUpsert?.(record, state !== "skipped");
20321
+ } catch (error) {
20322
+ result.errors.push(error instanceof Error ? error.message : String(error));
20323
+ }
20324
+ }
20325
+ };
20326
+ applyRows("projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
20327
+ applyRows("agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
20328
+ applyRows("task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
20329
+ applyRows("plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
20330
+ applyRows("task_templates", TEMPLATE_COLUMNS, snapshot.templates);
20331
+ applyRows("tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
20332
+ if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
20333
+ replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
20334
+ }
20335
+ });
20336
+ applyRows("task_history", AUDIT_COLUMNS, snapshot.auditHistory);
20337
+ return result;
20338
+ }
20339
+ function upsertById(db, table, columns, row, updateClockColumn) {
20340
+ const id = row["id"];
20341
+ if (typeof id !== "string" || !id)
20342
+ throw new Error(`${table} row is missing id`);
20343
+ const presentColumns = columns.filter((column) => (column in row));
20344
+ if (!presentColumns.includes("id"))
20345
+ presentColumns.unshift("id");
20346
+ const existing = existsById2(db, table, id);
20347
+ const placeholders = presentColumns.map(() => "?").join(", ");
20348
+ const values = presentColumns.map((column) => valueForColumn(column, row[column]));
20349
+ const updateColumns = presentColumns.filter((column) => column !== "id");
20350
+ const updateSet = updateColumns.map((column) => `${column} = excluded.${column}`).join(", ");
20351
+ const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
20352
+ const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})
20353
+ ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})`;
20354
+ const changes = db.run(sql, values).changes;
20355
+ if (changes === 0)
20356
+ return "skipped";
20357
+ return existing ? "updated" : "inserted";
20358
+ }
20359
+ function existsById2(db, table, id) {
20360
+ return Boolean(db.query(`SELECT id FROM ${table} WHERE id = ?`).get(id));
20361
+ }
20362
+ function valueForColumn(column, value) {
20363
+ if (BOOLEAN_COLUMNS.has(column))
20364
+ return value ? 1 : 0;
20365
+ if (JSON_COLUMNS.has(column))
20366
+ return JSON.stringify(value ?? (column === "tags" || column === "permissions" || column === "capabilities" || column === "variables" ? [] : {}));
20367
+ return value === undefined ? null : value;
20368
+ }
20369
+ function asRecord2(value) {
20370
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
20371
+ throw new Error("snapshot rows must be objects");
20372
+ }
20373
+ return value;
20374
+ }
20375
+ function sortedTasks2(tasks) {
20376
+ const byId = new Map(tasks.map((task2) => [task2.id, task2]));
20377
+ const seen = new Set;
20378
+ const result = [];
20379
+ const visit = (task2) => {
20380
+ if (seen.has(task2.id))
20381
+ return;
20382
+ if (task2.parent_id && byId.has(task2.parent_id))
20383
+ visit(byId.get(task2.parent_id));
20384
+ seen.add(task2.id);
20385
+ result.push(task2);
20386
+ };
20387
+ for (const task2 of tasks)
20388
+ visit(task2);
20389
+ return result;
20390
+ }
20391
+
20392
+ // src/storage/local-sqlite.ts
19696
20393
  function createLocalSqliteTodosStorageAdapter(options = {}) {
19697
20394
  const database = () => options.db ?? getDatabase();
19698
20395
  let adapter;
@@ -19765,7 +20462,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
19765
20462
  getRecentActivity: (limit) => getRecentActivity(limit, database())
19766
20463
  },
19767
20464
  sync: {
19768
- getTasksChangedSince: (since, filters) => getTasksChangedSince(since, filters, database())
20465
+ getTasksChangedSince: (since, filters) => getTasksChangedSince(since, filters, database()),
20466
+ exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
20467
+ importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
19769
20468
  },
19770
20469
  transaction: (fn) => {
19771
20470
  const tx = database().transaction(() => fn(adapter));
@@ -19774,6 +20473,1326 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
19774
20473
  };
19775
20474
  return adapter;
19776
20475
  }
20476
+
20477
+ // src/storage/postgres-sync.ts
20478
+ var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
20479
+ var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
20480
+ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE, cursorTableName = DEFAULT_TODOS_POSTGRES_CURSOR_TABLE) {
20481
+ assertSafeIdentifier(tableName);
20482
+ assertSafeIdentifier(cursorTableName);
20483
+ return [
20484
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
20485
+ service text NOT NULL,
20486
+ object_type text NOT NULL,
20487
+ object_id text NOT NULL,
20488
+ payload jsonb NOT NULL,
20489
+ updated_at timestamptz NOT NULL,
20490
+ deleted_at timestamptz,
20491
+ source_machine_id text,
20492
+ version integer,
20493
+ PRIMARY KEY (service, object_type, object_id)
20494
+ )`,
20495
+ `CREATE INDEX IF NOT EXISTS ${tableName}_updated_idx ON ${tableName} (service, updated_at)`,
20496
+ `CREATE TABLE IF NOT EXISTS ${cursorTableName} (
20497
+ service text NOT NULL,
20498
+ cursor_name text NOT NULL,
20499
+ value text NOT NULL,
20500
+ updated_at timestamptz NOT NULL DEFAULT now(),
20501
+ PRIMARY KEY (service, cursor_name)
20502
+ )`
20503
+ ];
20504
+ }
20505
+
20506
+ class PostgresTodosSyncStore {
20507
+ client;
20508
+ service;
20509
+ sourceMachineId;
20510
+ tableName;
20511
+ cursorTableName;
20512
+ constructor(client, options = {}) {
20513
+ this.client = client;
20514
+ this.service = options.service ?? "todos";
20515
+ this.sourceMachineId = options.sourceMachineId;
20516
+ this.tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
20517
+ this.cursorTableName = options.cursorTableName ?? DEFAULT_TODOS_POSTGRES_CURSOR_TABLE;
20518
+ assertSafeIdentifier(this.tableName);
20519
+ assertSafeIdentifier(this.cursorTableName);
20520
+ }
20521
+ async ensureSchema() {
20522
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
20523
+ await this.client.query(sql);
20524
+ }
20525
+ }
20526
+ async pushSnapshot(snapshot, context = {}) {
20527
+ const result = { records: 0, objectTypes: {} };
20528
+ const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
20529
+ for (const entry of snapshotEntries(snapshot)) {
20530
+ await this.client.query(`INSERT INTO ${this.tableName} (
20531
+ service, object_type, object_id, payload, updated_at,
20532
+ deleted_at, source_machine_id, version
20533
+ ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
20534
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
20535
+ payload = EXCLUDED.payload,
20536
+ updated_at = EXCLUDED.updated_at,
20537
+ deleted_at = EXCLUDED.deleted_at,
20538
+ source_machine_id = EXCLUDED.source_machine_id,
20539
+ version = EXCLUDED.version
20540
+ WHERE ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
20541
+ this.service,
20542
+ entry.type,
20543
+ entry.id,
20544
+ JSON.stringify(entry.payload),
20545
+ entry.updatedAt,
20546
+ entry.deletedAt,
20547
+ sourceMachineId,
20548
+ entry.version
20549
+ ]);
20550
+ result.records += 1;
20551
+ result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
20552
+ }
20553
+ return result;
20554
+ }
20555
+ async pullSnapshot(options = {}) {
20556
+ const params = [this.service];
20557
+ const filters = ["service = $1", "deleted_at IS NULL"];
20558
+ if (options.since) {
20559
+ params.push(options.since);
20560
+ filters.push(`updated_at > $${params.length}::timestamptz`);
20561
+ }
20562
+ if (options.objectTypes?.length) {
20563
+ params.push(options.objectTypes);
20564
+ filters.push(`object_type = ANY($${params.length}::text[])`);
20565
+ }
20566
+ const response = await this.client.query(`SELECT object_type, payload FROM ${this.tableName}
20567
+ WHERE ${filters.join(" AND ")}
20568
+ ORDER BY updated_at ASC, object_type ASC, object_id ASC`, params);
20569
+ return rowsToSnapshot(response.rows);
20570
+ }
20571
+ async getCursor(name) {
20572
+ const result = await this.client.query(`SELECT value FROM ${this.cursorTableName} WHERE service = $1 AND cursor_name = $2`, [this.service, name]);
20573
+ return result.rows[0]?.value ?? null;
20574
+ }
20575
+ async setCursor(name, value) {
20576
+ await this.client.query(`INSERT INTO ${this.cursorTableName} (service, cursor_name, value, updated_at)
20577
+ VALUES ($1, $2, $3, now())
20578
+ ON CONFLICT (service, cursor_name) DO UPDATE SET
20579
+ value = EXCLUDED.value,
20580
+ updated_at = EXCLUDED.updated_at`, [this.service, name, value]);
20581
+ }
20582
+ }
20583
+ function createPostgresTodosSyncStore(client, options = {}) {
20584
+ return new PostgresTodosSyncStore(client, options);
20585
+ }
20586
+ function snapshotEntries(snapshot) {
20587
+ return [
20588
+ ...snapshot.tasks.map((payload) => entry("tasks", payload, snapshot.exportedAt)),
20589
+ ...snapshot.projects.map((payload) => entry("projects", payload, snapshot.exportedAt)),
20590
+ ...snapshot.plans.map((payload) => entry("plans", payload, snapshot.exportedAt)),
20591
+ ...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
20592
+ ...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
20593
+ ...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
20594
+ ...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt))
20595
+ ];
20596
+ }
20597
+ function entry(type, payload, fallbackUpdatedAt) {
20598
+ const id = payload["id"];
20599
+ if (typeof id !== "string" || !id)
20600
+ throw new Error(`${type} record is missing a stable id`);
20601
+ return {
20602
+ type,
20603
+ id,
20604
+ payload,
20605
+ updatedAt: stringValue(payload["updated_at"]) ?? stringValue(payload["created_at"]) ?? fallbackUpdatedAt,
20606
+ deletedAt: stringValue(payload["deleted_at"]),
20607
+ version: numberValue2(payload["version"])
20608
+ };
20609
+ }
20610
+ function rowsToSnapshot(rows) {
20611
+ const snapshot = {
20612
+ exportedAt: new Date().toISOString(),
20613
+ source: "postgres",
20614
+ tasks: [],
20615
+ projects: [],
20616
+ plans: [],
20617
+ agents: [],
20618
+ taskLists: [],
20619
+ templates: [],
20620
+ auditHistory: []
20621
+ };
20622
+ for (const row of rows) {
20623
+ const payload = payloadRecord(row.payload);
20624
+ if (row.object_type === "tasks")
20625
+ snapshot.tasks.push(payload);
20626
+ else if (row.object_type === "projects")
20627
+ snapshot.projects.push(payload);
20628
+ else if (row.object_type === "plans")
20629
+ snapshot.plans.push(payload);
20630
+ else if (row.object_type === "agents")
20631
+ snapshot.agents.push(payload);
20632
+ else if (row.object_type === "task_lists")
20633
+ snapshot.taskLists.push(payload);
20634
+ else if (row.object_type === "templates")
20635
+ snapshot.templates.push(payload);
20636
+ else if (row.object_type === "audit_history")
20637
+ snapshot.auditHistory.push(payload);
20638
+ }
20639
+ return snapshot;
20640
+ }
20641
+ function payloadRecord(value) {
20642
+ if (typeof value === "string")
20643
+ return JSON.parse(value);
20644
+ if (value && typeof value === "object" && !Array.isArray(value))
20645
+ return value;
20646
+ throw new Error("Postgres sync payload must be a JSON object");
20647
+ }
20648
+ function stringValue(value) {
20649
+ return typeof value === "string" && value ? value : null;
20650
+ }
20651
+ function numberValue2(value) {
20652
+ return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
20653
+ }
20654
+ function assertSafeIdentifier(value) {
20655
+ if (!/^[a-z_][a-z0-9_]*$/i.test(value))
20656
+ throw new Error(`Unsafe Postgres identifier: ${value}`);
20657
+ }
20658
+
20659
+ // src/storage/hybrid.ts
20660
+ function createHybridTodosStorageAdapter(options) {
20661
+ const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
20662
+ const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
20663
+ if (!syncStore)
20664
+ throw new Error("hybrid storage requires a Postgres sync store or query client");
20665
+ if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
20666
+ throw new Error("hybrid storage requires local snapshot export/import support");
20667
+ }
20668
+ const exportSnapshot = async (context) => await local.sync.exportSnapshot(context);
20669
+ const importSnapshot = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
20670
+ const remote = {
20671
+ ensureSchema: () => syncStore.ensureSchema(),
20672
+ async pushSnapshot(context) {
20673
+ const snapshot = await exportSnapshot(context);
20674
+ return syncStore.pushSnapshot(snapshot, context);
20675
+ },
20676
+ async pullSnapshot(options2, context) {
20677
+ const snapshot = await syncStore.pullSnapshot(options2);
20678
+ return importSnapshot(snapshot, context);
20679
+ },
20680
+ async syncOnce(options2, context) {
20681
+ const pulled = await remote.pullSnapshot(options2, context);
20682
+ const pushed = await remote.pushSnapshot(context);
20683
+ return { pulled, pushed };
20684
+ }
20685
+ };
20686
+ return {
20687
+ ...local,
20688
+ kind: "hybrid",
20689
+ capabilities: {
20690
+ ...local.capabilities,
20691
+ localPersistence: true,
20692
+ remotePersistence: true,
20693
+ sync: true
20694
+ },
20695
+ sync: {
20696
+ ...local.sync,
20697
+ exportSnapshot,
20698
+ importSnapshot
20699
+ },
20700
+ remote
20701
+ };
20702
+ }
20703
+
20704
+ // src/storage/postgres-adapter.ts
20705
+ import { randomUUID as randomUUID2 } from "crypto";
20706
+ function createPostgresTodosStorageAdapter(options) {
20707
+ const store = new PostgresJsonRecordStore(options);
20708
+ const adapter = {
20709
+ kind: "postgres",
20710
+ capabilities: {
20711
+ localPersistence: false,
20712
+ remotePersistence: true,
20713
+ transactions: false,
20714
+ auditLog: true,
20715
+ sync: true
20716
+ },
20717
+ tasks: {
20718
+ create: (input, context) => createTask2(input, store, context),
20719
+ get: (id) => store.get("tasks", id),
20720
+ list: (filter = {}) => listTasks2(filter, store),
20721
+ count: async (filter = {}) => (await listTasks2(filter, store)).length,
20722
+ update: (id, input) => updateTask2(id, input, store),
20723
+ delete: (id, context) => store.delete("tasks", id, context),
20724
+ start: (id, agentId) => startTask2(id, agentId, store),
20725
+ complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
20726
+ fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
20727
+ claimNext: (agentId, filters) => claimNextTask2(agentId, filters, store),
20728
+ getNext: (_agentId, filters) => getNextTask2(filters, store),
20729
+ getActiveWork: (filters) => getActiveWork2(filters, store),
20730
+ getChangedSince: (since, filters) => getChangedSince(since, filters, store)
20731
+ },
20732
+ projects: {
20733
+ create: (input, context) => createProject2(input, store, context),
20734
+ get: (id) => store.get("projects", id),
20735
+ getByPath: async (path) => (await store.list("projects")).find((project) => project.path === path) ?? null,
20736
+ list: async () => (await store.list("projects")).sort((a, b) => a.name.localeCompare(b.name)),
20737
+ update: (id, input) => updateProject2(id, input, store),
20738
+ delete: (id, context) => store.delete("projects", id, context)
20739
+ },
20740
+ plans: {
20741
+ create: (input, context) => createPlan2(input, store, context),
20742
+ get: (id) => store.get("plans", id),
20743
+ list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
20744
+ update: (id, input) => updatePlan2(id, input, store),
20745
+ delete: (id, context) => store.delete("plans", id, context)
20746
+ },
20747
+ agents: {
20748
+ register: (input, context) => registerAgent2(input, store, context),
20749
+ get: (id) => store.get("agents", id),
20750
+ getByName: async (name) => (await store.list("agents")).find((agent) => agent.name === name) ?? null,
20751
+ list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
20752
+ update: (id, input) => updateAgent2(id, input, store)
20753
+ },
20754
+ taskLists: {
20755
+ create: (input, context) => createTaskList2(input, store, context),
20756
+ get: (id) => store.get("task_lists", id),
20757
+ getBySlug: async (slug, projectId) => (await store.list("task_lists")).find((list) => list.slug === slug && (projectId === undefined || list.project_id === projectId)) ?? null,
20758
+ list: async (projectId) => (await store.list("task_lists")).filter((list) => projectId === undefined || list.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
20759
+ update: (id, input) => updateTaskList2(id, input, store),
20760
+ delete: (id, context) => store.delete("task_lists", id, context)
20761
+ },
20762
+ templates: {
20763
+ create: (input, context) => createTemplate2(input, store, context),
20764
+ get: (id) => store.get("templates", id),
20765
+ list: async () => (await store.list("templates")).sort((a, b) => a.name.localeCompare(b.name)),
20766
+ update: (id, input) => updateTemplate2(id, input, store),
20767
+ delete: (id, context) => store.delete("templates", id, context),
20768
+ getWithTasks: async (id) => {
20769
+ const template = await store.get("templates", id);
20770
+ return template ? { ...template, tasks: [] } : null;
20771
+ }
20772
+ },
20773
+ audit: {
20774
+ logTaskChange: (taskId, action, field2, oldValue, newValue, agentId, context) => logTaskChange2(taskId, action, field2, oldValue, newValue, agentId, store, context),
20775
+ addComment: (input, context) => addComment2(input, store, context),
20776
+ getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
20777
+ getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
20778
+ },
20779
+ sync: {
20780
+ getTasksChangedSince: (since, filters) => getChangedSince(since, filters, store),
20781
+ exportSnapshot: () => exportSnapshot(store),
20782
+ importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
20783
+ },
20784
+ transaction: (fn) => fn(adapter)
20785
+ };
20786
+ return adapter;
20787
+ }
20788
+
20789
+ class PostgresJsonRecordStore {
20790
+ options;
20791
+ service;
20792
+ sourceMachineId;
20793
+ tableName;
20794
+ cursorTableName;
20795
+ schemaReady = null;
20796
+ constructor(options) {
20797
+ this.options = options;
20798
+ this.service = options.service ?? "todos";
20799
+ this.sourceMachineId = options.sourceMachineId;
20800
+ this.tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
20801
+ this.cursorTableName = options.cursorTableName ?? DEFAULT_TODOS_POSTGRES_CURSOR_TABLE;
20802
+ }
20803
+ async ensureSchema() {
20804
+ this.schemaReady ??= (async () => {
20805
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
20806
+ await this.options.client.query(sql);
20807
+ }
20808
+ })();
20809
+ await this.schemaReady;
20810
+ }
20811
+ async get(type, id) {
20812
+ await this.ensureSchema();
20813
+ const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
20814
+ FROM ${this.tableName}
20815
+ WHERE service = $1 AND object_type = $2 AND object_id = $3 AND deleted_at IS NULL
20816
+ LIMIT 1`, [this.service, type, id]);
20817
+ return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
20818
+ }
20819
+ async list(type) {
20820
+ return (await this.listRecords(type)).map((record) => record.payload);
20821
+ }
20822
+ async listRecords(type) {
20823
+ await this.ensureSchema();
20824
+ const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
20825
+ FROM ${this.tableName}
20826
+ WHERE service = $1 AND object_type = $2 AND deleted_at IS NULL
20827
+ ORDER BY updated_at ASC, object_id ASC`, [this.service, type]);
20828
+ return result.rows.map((row) => ({
20829
+ objectId: row.object_id,
20830
+ payload: payloadRecord2(row.payload),
20831
+ updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString()
20832
+ }));
20833
+ }
20834
+ async upsert(type, value, context = {}) {
20835
+ await this.ensureSchema();
20836
+ const updatedAt = stringValue2(value.updated_at) ?? stringValue2(value.created_at) ?? new Date().toISOString();
20837
+ await this.options.client.query(`INSERT INTO ${this.tableName} (
20838
+ service, object_type, object_id, payload, updated_at,
20839
+ deleted_at, source_machine_id, version
20840
+ ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, $7)
20841
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
20842
+ payload = EXCLUDED.payload,
20843
+ updated_at = EXCLUDED.updated_at,
20844
+ deleted_at = NULL,
20845
+ source_machine_id = EXCLUDED.source_machine_id,
20846
+ version = EXCLUDED.version`, [
20847
+ this.service,
20848
+ type,
20849
+ value.id,
20850
+ JSON.stringify(value),
20851
+ updatedAt,
20852
+ context.requestId ?? this.sourceMachineId ?? null,
20853
+ numberValue3(value.version)
20854
+ ]);
20855
+ return value;
20856
+ }
20857
+ async delete(type, id, context = {}) {
20858
+ await this.ensureSchema();
20859
+ const existing = await this.get(type, id);
20860
+ if (!existing)
20861
+ return false;
20862
+ const timestamp2 = new Date().toISOString();
20863
+ await this.options.client.query(`UPDATE ${this.tableName}
20864
+ SET deleted_at = $4::timestamptz,
20865
+ updated_at = $4::timestamptz,
20866
+ source_machine_id = $5
20867
+ WHERE service = $1 AND object_type = $2 AND object_id = $3`, [this.service, type, id, timestamp2, context.requestId ?? this.sourceMachineId ?? null]);
20868
+ return true;
20869
+ }
20870
+ async getCursor(name) {
20871
+ await this.ensureSchema();
20872
+ const result = await this.options.client.query(`SELECT value FROM ${this.cursorTableName} WHERE service = $1 AND cursor_name = $2`, [this.service, name]);
20873
+ return result.rows[0]?.value ?? null;
20874
+ }
20875
+ async setCursor(name, value) {
20876
+ await this.ensureSchema();
20877
+ await this.options.client.query(`INSERT INTO ${this.cursorTableName} (service, cursor_name, value, updated_at)
20878
+ VALUES ($1, $2, $3, now())
20879
+ ON CONFLICT (service, cursor_name) DO UPDATE SET
20880
+ value = EXCLUDED.value,
20881
+ updated_at = EXCLUDED.updated_at`, [this.service, name, value]);
20882
+ }
20883
+ }
20884
+ async function createTask2(input, store, context) {
20885
+ const timestamp2 = new Date().toISOString();
20886
+ const shortId = input.project_id ? await nextTaskShortId2(input.project_id, store, context) : null;
20887
+ const task2 = {
20888
+ id: randomUUID2(),
20889
+ short_id: shortId,
20890
+ project_id: input.project_id ?? context?.projectId ?? null,
20891
+ parent_id: input.parent_id ?? null,
20892
+ plan_id: input.plan_id ?? null,
20893
+ task_list_id: input.task_list_id ?? context?.taskListId ?? null,
20894
+ title: input.title,
20895
+ description: input.description ?? null,
20896
+ status: input.status ?? "pending",
20897
+ priority: input.priority ?? "medium",
20898
+ agent_id: input.agent_id ?? null,
20899
+ assigned_to: input.assigned_to ?? null,
20900
+ session_id: input.session_id ?? context?.sessionId ?? null,
20901
+ working_dir: input.working_dir ?? null,
20902
+ tags: input.tags ?? [],
20903
+ metadata: input.metadata ?? {},
20904
+ version: 1,
20905
+ locked_by: null,
20906
+ locked_at: null,
20907
+ created_at: timestamp2,
20908
+ updated_at: timestamp2,
20909
+ started_at: null,
20910
+ completed_at: null,
20911
+ due_at: input.due_at ?? null,
20912
+ estimated_minutes: input.estimated_minutes ?? null,
20913
+ actual_minutes: null,
20914
+ requires_approval: input.requires_approval ?? false,
20915
+ approved_by: null,
20916
+ approved_at: null,
20917
+ recurrence_rule: input.recurrence_rule ?? null,
20918
+ recurrence_parent_id: input.recurrence_parent_id ?? null,
20919
+ spawns_template_id: input.spawns_template_id ?? null,
20920
+ confidence: input.confidence ?? null,
20921
+ reason: input.reason ?? null,
20922
+ spawned_from_session: input.spawned_from_session ?? null,
20923
+ assigned_by: input.assigned_by ?? null,
20924
+ assigned_from_project: input.assigned_from_project ?? null,
20925
+ task_type: input.task_type ?? null,
20926
+ cost_tokens: 0,
20927
+ cost_usd: 0,
20928
+ delegated_from: null,
20929
+ delegation_depth: 0,
20930
+ retry_count: input.retry_count ?? 0,
20931
+ max_retries: input.max_retries ?? 0,
20932
+ retry_after: input.retry_after ?? null,
20933
+ sla_minutes: input.sla_minutes ?? null,
20934
+ runner_id: null,
20935
+ runner_started_at: null,
20936
+ runner_completed_at: null,
20937
+ current_step: null,
20938
+ total_steps: null
20939
+ };
20940
+ await store.upsert("tasks", task2, context);
20941
+ await logTaskChange2(task2.id, "created", "status", null, task2.status, task2.assigned_by ?? task2.agent_id, store, context);
20942
+ return task2;
20943
+ }
20944
+ async function updateTask2(id, input, store) {
20945
+ const existing = await requireRecord("tasks", id, store);
20946
+ if (existing.version !== input.version) {
20947
+ throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
20948
+ }
20949
+ const task2 = {
20950
+ ...existing,
20951
+ ...definedPatch(input),
20952
+ version: existing.version + 1,
20953
+ updated_at: new Date().toISOString(),
20954
+ tags: input.tags ?? existing.tags,
20955
+ metadata: input.metadata ?? existing.metadata,
20956
+ requires_approval: input.requires_approval ?? existing.requires_approval,
20957
+ task_list_id: input.task_list_id ?? existing.task_list_id
20958
+ };
20959
+ await store.upsert("tasks", task2);
20960
+ return task2;
20961
+ }
20962
+ async function startTask2(id, agentId, store) {
20963
+ const task2 = await requireRecord("tasks", id, store);
20964
+ return patchTask(task2, {
20965
+ status: "in_progress",
20966
+ assigned_to: task2.assigned_to ?? agentId,
20967
+ agent_id: task2.agent_id ?? agentId,
20968
+ locked_by: agentId,
20969
+ locked_at: new Date().toISOString(),
20970
+ started_at: task2.started_at ?? new Date().toISOString()
20971
+ }, store);
20972
+ }
20973
+ async function completeTask2(id, agentId, options, store) {
20974
+ const task2 = await requireRecord("tasks", id, store);
20975
+ return patchTask(task2, {
20976
+ status: "completed",
20977
+ assigned_to: task2.assigned_to ?? agentId ?? null,
20978
+ completed_at: options?.completed_at ?? new Date().toISOString(),
20979
+ actual_minutes: task2.actual_minutes,
20980
+ confidence: options?.confidence ?? task2.confidence
20981
+ }, store);
20982
+ }
20983
+ async function failTask2(id, agentId, reason, options, store) {
20984
+ const task2 = await requireRecord("tasks", id, store);
20985
+ const failed = await patchTask(task2, {
20986
+ status: "failed",
20987
+ assigned_to: task2.assigned_to ?? agentId ?? null,
20988
+ reason: reason ?? task2.reason,
20989
+ retry_after: options?.retry_after ?? task2.retry_after
20990
+ }, store);
20991
+ if (!options?.retry)
20992
+ return { task: failed };
20993
+ const retryTask = await createTask2({
20994
+ title: task2.title,
20995
+ description: task2.description ?? undefined,
20996
+ project_id: task2.project_id ?? undefined,
20997
+ parent_id: task2.parent_id ?? undefined,
20998
+ plan_id: task2.plan_id ?? undefined,
20999
+ task_list_id: task2.task_list_id ?? undefined,
21000
+ priority: task2.priority,
21001
+ assigned_to: task2.assigned_to ?? undefined,
21002
+ tags: task2.tags,
21003
+ metadata: task2.metadata,
21004
+ retry_count: task2.retry_count + 1,
21005
+ max_retries: task2.max_retries,
21006
+ reason: reason ?? undefined,
21007
+ task_type: task2.task_type ?? undefined
21008
+ }, store);
21009
+ return { task: failed, retryTask };
21010
+ }
21011
+ async function patchTask(task2, patch, store) {
21012
+ const updated = {
21013
+ ...task2,
21014
+ ...patch,
21015
+ version: task2.version + 1,
21016
+ updated_at: new Date().toISOString()
21017
+ };
21018
+ await store.upsert("tasks", updated);
21019
+ return updated;
21020
+ }
21021
+ async function listTasks2(filter, store) {
21022
+ let tasks = await store.list("tasks");
21023
+ tasks = tasks.filter((task2) => taskMatchesFilter(task2, filter));
21024
+ tasks.sort((a, b) => priorityRank(a.priority) - priorityRank(b.priority) || a.created_at.localeCompare(b.created_at));
21025
+ const offset = filter.offset ?? 0;
21026
+ const limit = filter.limit ?? tasks.length;
21027
+ return tasks.slice(offset, offset + limit);
21028
+ }
21029
+ async function getNextTask2(filters, store) {
21030
+ return (await listTasks2({ ...filters, status: "pending", limit: 1 }, store))[0] ?? null;
21031
+ }
21032
+ async function claimNextTask2(agentId, filters, store) {
21033
+ const task2 = await getNextTask2(filters, store);
21034
+ return task2 ? startTask2(task2.id, agentId, store) : null;
21035
+ }
21036
+ async function getActiveWork2(filters, store) {
21037
+ const tasks = await listTasks2({ ...filters, status: "in_progress" }, store);
21038
+ return tasks.map((task2) => ({
21039
+ id: task2.id,
21040
+ short_id: task2.short_id,
21041
+ title: task2.title,
21042
+ priority: task2.priority,
21043
+ assigned_to: task2.assigned_to,
21044
+ locked_by: task2.locked_by,
21045
+ locked_at: task2.locked_at,
21046
+ updated_at: task2.updated_at
21047
+ }));
21048
+ }
21049
+ async function getChangedSince(since, filters, store) {
21050
+ return (await listTasks2(filters ?? {}, store)).filter((task2) => task2.updated_at > since);
21051
+ }
21052
+ async function createProject2(input, store, context) {
21053
+ const timestamp2 = new Date().toISOString();
21054
+ const project = {
21055
+ id: randomUUID2(),
21056
+ name: input.name,
21057
+ path: input.path,
21058
+ description: input.description ?? null,
21059
+ task_list_id: input.task_list_id ?? `todos-${slugify2(input.name)}`,
21060
+ task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
21061
+ task_counter: 0,
21062
+ created_at: timestamp2,
21063
+ updated_at: timestamp2
21064
+ };
21065
+ return store.upsert("projects", project, context);
21066
+ }
21067
+ async function updateProject2(id, input, store) {
21068
+ const project = await requireRecord("projects", id, store);
21069
+ const updated = { ...project, ...definedPatch(input), updated_at: new Date().toISOString() };
21070
+ return store.upsert("projects", updated);
21071
+ }
21072
+ async function createPlan2(input, store, context) {
21073
+ const timestamp2 = new Date().toISOString();
21074
+ return store.upsert("plans", {
21075
+ id: randomUUID2(),
21076
+ project_id: input.project_id ?? context?.projectId ?? null,
21077
+ task_list_id: input.task_list_id ?? context?.taskListId ?? null,
21078
+ agent_id: input.agent_id ?? context?.agentId ?? null,
21079
+ name: input.name,
21080
+ description: input.description ?? null,
21081
+ status: input.status ?? "active",
21082
+ created_at: timestamp2,
21083
+ updated_at: timestamp2
21084
+ }, context);
21085
+ }
21086
+ async function updatePlan2(id, input, store) {
21087
+ const plan = await requireRecord("plans", id, store);
21088
+ return store.upsert("plans", { ...plan, ...definedPatch(input), updated_at: new Date().toISOString() });
21089
+ }
21090
+ async function registerAgent2(input, store, context) {
21091
+ const existing = (await store.list("agents")).find((agent2) => agent2.name === input.name && agent2.status !== "archived");
21092
+ if (existing && !input.force && existing.session_id && existing.session_id !== input.session_id) {
21093
+ return { conflict: true, message: `Agent name '${input.name}' is already active` };
21094
+ }
21095
+ const timestamp2 = new Date().toISOString();
21096
+ const agent = {
21097
+ id: existing?.id ?? randomUUID2().slice(0, 8),
21098
+ name: input.name,
21099
+ description: input.description ?? existing?.description ?? null,
21100
+ role: input.role ?? existing?.role ?? null,
21101
+ title: input.title ?? existing?.title ?? null,
21102
+ level: input.level ?? existing?.level ?? null,
21103
+ permissions: input.permissions ?? existing?.permissions ?? [],
21104
+ reports_to: input.reports_to ?? existing?.reports_to ?? null,
21105
+ org_id: input.org_id ?? existing?.org_id ?? null,
21106
+ capabilities: input.capabilities ?? existing?.capabilities ?? [],
21107
+ status: "active",
21108
+ metadata: input.metadata ?? existing?.metadata ?? {},
21109
+ created_at: existing?.created_at ?? timestamp2,
21110
+ last_seen_at: timestamp2,
21111
+ session_id: input.session_id ?? context?.sessionId ?? existing?.session_id ?? null,
21112
+ working_dir: input.working_dir ?? existing?.working_dir ?? null,
21113
+ active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null
21114
+ };
21115
+ return store.upsert("agents", agent, context);
21116
+ }
21117
+ async function updateAgent2(id, input, store) {
21118
+ const agent = await store.get("agents", id);
21119
+ if (!agent)
21120
+ return null;
21121
+ return store.upsert("agents", {
21122
+ ...agent,
21123
+ ...definedPatch(input),
21124
+ permissions: input.permissions ?? agent.permissions,
21125
+ capabilities: input.capabilities ?? agent.capabilities,
21126
+ metadata: input.metadata ?? agent.metadata,
21127
+ last_seen_at: new Date().toISOString()
21128
+ });
21129
+ }
21130
+ async function createTaskList2(input, store, context) {
21131
+ const timestamp2 = new Date().toISOString();
21132
+ return store.upsert("task_lists", {
21133
+ id: randomUUID2(),
21134
+ project_id: input.project_id ?? context?.projectId ?? null,
21135
+ slug: input.slug ?? slugify2(input.name),
21136
+ name: input.name,
21137
+ description: input.description ?? null,
21138
+ metadata: input.metadata ?? {},
21139
+ created_at: timestamp2,
21140
+ updated_at: timestamp2
21141
+ }, context);
21142
+ }
21143
+ async function updateTaskList2(id, input, store) {
21144
+ const list = await requireRecord("task_lists", id, store);
21145
+ return store.upsert("task_lists", {
21146
+ ...list,
21147
+ ...definedPatch(input),
21148
+ metadata: input.metadata ?? list.metadata,
21149
+ updated_at: new Date().toISOString()
21150
+ });
21151
+ }
21152
+ async function createTemplate2(input, store, context) {
21153
+ const timestamp2 = new Date().toISOString();
21154
+ return store.upsert("templates", {
21155
+ id: randomUUID2(),
21156
+ name: input.name,
21157
+ title_pattern: input.title_pattern,
21158
+ description: input.description ?? null,
21159
+ priority: input.priority ?? "medium",
21160
+ tags: input.tags ?? [],
21161
+ variables: input.variables ?? [],
21162
+ version: 1,
21163
+ project_id: input.project_id ?? context?.projectId ?? null,
21164
+ plan_id: input.plan_id ?? null,
21165
+ metadata: input.metadata ?? {},
21166
+ created_at: timestamp2
21167
+ }, context);
21168
+ }
21169
+ async function updateTemplate2(id, input, store) {
21170
+ const template = await store.get("templates", id);
21171
+ if (!template)
21172
+ return null;
21173
+ return store.upsert("templates", {
21174
+ ...template,
21175
+ ...definedPatch(input),
21176
+ tags: input.tags ?? template.tags,
21177
+ variables: input.variables ?? template.variables,
21178
+ metadata: input.metadata ?? template.metadata,
21179
+ version: template.version + 1
21180
+ });
21181
+ }
21182
+ async function logTaskChange2(taskId, action, field2, oldValue, newValue, agentId, store, context) {
21183
+ const entry2 = {
21184
+ id: randomUUID2(),
21185
+ task_id: taskId,
21186
+ action,
21187
+ field: field2 ?? null,
21188
+ old_value: oldValue ?? null,
21189
+ new_value: newValue ?? null,
21190
+ agent_id: agentId ?? context?.agentId ?? null,
21191
+ created_at: new Date().toISOString()
21192
+ };
21193
+ return store.upsert("audit_history", entry2, context);
21194
+ }
21195
+ async function addComment2(input, store, context) {
21196
+ const comment = {
21197
+ id: randomUUID2(),
21198
+ task_id: input.task_id,
21199
+ agent_id: input.agent_id ?? context?.agentId ?? null,
21200
+ session_id: input.session_id ?? context?.sessionId ?? null,
21201
+ content: input.content,
21202
+ type: input.type ?? "comment",
21203
+ progress_pct: input.progress_pct ?? null,
21204
+ created_at: new Date().toISOString()
21205
+ };
21206
+ return store.upsert("comments", comment, context);
21207
+ }
21208
+ async function exportSnapshot(store) {
21209
+ return {
21210
+ exportedAt: new Date().toISOString(),
21211
+ source: "postgres",
21212
+ tasks: await store.list("tasks"),
21213
+ projects: await store.list("projects"),
21214
+ plans: await store.list("plans"),
21215
+ agents: await store.list("agents"),
21216
+ taskLists: await store.list("task_lists"),
21217
+ templates: await store.list("templates"),
21218
+ auditHistory: await store.list("audit_history")
21219
+ };
21220
+ }
21221
+ async function importSnapshot(snapshot, store, context) {
21222
+ const result = { inserted: 0, updated: 0, skipped: 0, errors: [] };
21223
+ const entries = [
21224
+ ...snapshot.tasks.map((row) => ["tasks", row]),
21225
+ ...snapshot.projects.map((row) => ["projects", row]),
21226
+ ...snapshot.plans.map((row) => ["plans", row]),
21227
+ ...snapshot.agents.map((row) => ["agents", row]),
21228
+ ...snapshot.taskLists.map((row) => ["task_lists", row]),
21229
+ ...snapshot.templates.map((row) => ["templates", row]),
21230
+ ...snapshot.auditHistory.map((row) => ["audit_history", row])
21231
+ ];
21232
+ for (const [type, row] of entries) {
21233
+ try {
21234
+ const existing = await store.get(type, row.id);
21235
+ await store.upsert(type, row, context);
21236
+ if (existing)
21237
+ result.updated += 1;
21238
+ else
21239
+ result.inserted += 1;
21240
+ } catch (error) {
21241
+ result.errors.push(error instanceof Error ? error.message : String(error));
21242
+ }
21243
+ }
21244
+ return result;
21245
+ }
21246
+ async function requireRecord(type, id, store) {
21247
+ const record = await store.get(type, id);
21248
+ if (!record)
21249
+ throw new Error(`${type} record not found: ${id}`);
21250
+ return record;
21251
+ }
21252
+ async function nextTaskShortId2(projectId, store, context) {
21253
+ const project = await store.get("projects", projectId);
21254
+ if (!project?.task_prefix)
21255
+ return null;
21256
+ const counter = project.task_counter + 1;
21257
+ await store.upsert("projects", {
21258
+ ...project,
21259
+ task_counter: counter,
21260
+ updated_at: new Date().toISOString()
21261
+ }, context);
21262
+ return `${project.task_prefix}-${String(counter).padStart(5, "0")}`;
21263
+ }
21264
+ async function generateProjectPrefix(name, store) {
21265
+ 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();
21266
+ const existing = new Set((await store.list("projects")).map((project) => project.task_prefix).filter(Boolean));
21267
+ let candidate = base;
21268
+ let suffix = 1;
21269
+ while (existing.has(candidate)) {
21270
+ suffix += 1;
21271
+ candidate = `${base}${suffix}`;
21272
+ }
21273
+ return candidate;
21274
+ }
21275
+ function taskMatchesFilter(task2, filter) {
21276
+ if (filter.ids && !filter.ids.includes(task2.id))
21277
+ return false;
21278
+ if (filter.project_id !== undefined && task2.project_id !== filter.project_id)
21279
+ return false;
21280
+ if (filter.parent_id !== undefined && task2.parent_id !== filter.parent_id)
21281
+ return false;
21282
+ if (filter.plan_id !== undefined && task2.plan_id !== filter.plan_id)
21283
+ return false;
21284
+ if (filter.task_list_id !== undefined && task2.task_list_id !== filter.task_list_id)
21285
+ return false;
21286
+ if (filter.status !== undefined && !matchesOne(task2.status, filter.status))
21287
+ return false;
21288
+ if (filter.priority !== undefined && !matchesOne(task2.priority, filter.priority))
21289
+ return false;
21290
+ if (filter.assigned_to !== undefined && task2.assigned_to !== filter.assigned_to)
21291
+ return false;
21292
+ if (filter.agent_id !== undefined && task2.agent_id !== filter.agent_id)
21293
+ return false;
21294
+ if (filter.session_id !== undefined && task2.session_id !== filter.session_id)
21295
+ return false;
21296
+ if (filter.tags?.length && !filter.tags.every((tag) => task2.tags.includes(tag)))
21297
+ return false;
21298
+ if (filter.has_recurrence !== undefined && Boolean(task2.recurrence_rule) !== filter.has_recurrence)
21299
+ return false;
21300
+ if (filter.include_subtasks !== true && task2.parent_id)
21301
+ return false;
21302
+ if (filter.task_type !== undefined && !matchesOne(task2.task_type ?? "", filter.task_type))
21303
+ return false;
21304
+ return true;
21305
+ }
21306
+ function matchesOne(value, expected) {
21307
+ return Array.isArray(expected) ? expected.includes(value) : value === expected;
21308
+ }
21309
+ function priorityRank(priority) {
21310
+ return { critical: 0, high: 1, medium: 2, low: 3 }[priority];
21311
+ }
21312
+ function slugify2(value) {
21313
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "todos";
21314
+ }
21315
+ function definedPatch(value) {
21316
+ return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined));
21317
+ }
21318
+ function payloadRecord2(value) {
21319
+ if (typeof value === "string")
21320
+ return JSON.parse(value);
21321
+ if (value && typeof value === "object" && !Array.isArray(value))
21322
+ return value;
21323
+ throw new Error("Postgres storage payload must be a JSON object");
21324
+ }
21325
+ function stringValue2(value) {
21326
+ return typeof value === "string" && value ? value : null;
21327
+ }
21328
+ function numberValue3(value) {
21329
+ return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
21330
+ }
21331
+
21332
+ // src/storage/factory.ts
21333
+ function createTodosStorageAdapter(options = {}) {
21334
+ const config = options.config ?? loadTodosStorageConfig(options.env);
21335
+ if (config.mode === "local")
21336
+ return createLocalSqliteTodosStorageAdapter(options.local);
21337
+ assertTodosRemoteStorageConfig(config);
21338
+ const adapter = config.mode === "hybrid" ? options.hybridAdapter ?? createImplicitHybridAdapter(options) ?? options.remoteAdapter : options.remoteAdapter ?? createImplicitPostgresAdapter(options);
21339
+ if (!adapter) {
21340
+ throw new Error(`${config.mode} storage requires a repo-native remote adapter. ` + "Pass remoteAdapter/hybridAdapter after wiring Postgres RDS and S3 support.");
21341
+ }
21342
+ assertRemoteAdapterCapabilities(adapter, config.mode);
21343
+ return adapter;
21344
+ }
21345
+ function createImplicitPostgresAdapter(options) {
21346
+ if (!options.postgresClient)
21347
+ return null;
21348
+ return createPostgresTodosStorageAdapter({
21349
+ client: options.postgresClient,
21350
+ ...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
21351
+ });
21352
+ }
21353
+ function createImplicitHybridAdapter(options) {
21354
+ if (!options.postgresClient && !options.postgresSyncStore)
21355
+ return null;
21356
+ return createHybridTodosStorageAdapter({
21357
+ ...options.hybrid ?? {},
21358
+ local: options.local,
21359
+ postgresClient: options.postgresClient,
21360
+ syncStore: options.postgresSyncStore
21361
+ });
21362
+ }
21363
+ function assertRemoteAdapterCapabilities(adapter, mode) {
21364
+ if (!adapter.capabilities.remotePersistence) {
21365
+ throw new Error(`${mode} storage adapter must set capabilities.remotePersistence=true`);
21366
+ }
21367
+ if (mode === "hybrid" && !adapter.capabilities.localPersistence) {
21368
+ throw new Error("hybrid storage adapter must also set capabilities.localPersistence=true");
21369
+ }
21370
+ }
21371
+ // src/storage/s3-artifacts.ts
21372
+ import { createHash as createHash8, createHmac } from "crypto";
21373
+ function createTodosS3ArtifactStore(options) {
21374
+ const requestFetch = options.fetch ?? fetch;
21375
+ const now3 = options.now ?? (() => new Date);
21376
+ return {
21377
+ objectKey: (relativePath) => buildS3ObjectKey(options.config, relativePath),
21378
+ objectUrl: (relativePath) => buildS3ObjectUrl(options.config, buildS3ObjectKey(options.config, relativePath)),
21379
+ async putObject(input) {
21380
+ const key = buildS3ObjectKey(options.config, input.key);
21381
+ const url = buildS3ObjectUrl(options.config, key);
21382
+ const body = normalizeBody(input.body);
21383
+ const headers = {
21384
+ "content-type": input.contentType ?? "application/octet-stream"
21385
+ };
21386
+ for (const [name, value] of Object.entries(input.metadata ?? {})) {
21387
+ headers[`x-amz-meta-${name.toLowerCase()}`] = value;
21388
+ }
21389
+ const signed = signAwsV4Request({
21390
+ method: "PUT",
21391
+ url,
21392
+ region: options.config.region ?? "us-east-1",
21393
+ service: "s3",
21394
+ headers,
21395
+ body,
21396
+ credentials: options.credentials,
21397
+ now: now3()
21398
+ });
21399
+ const response = await requestFetch(url, { method: "PUT", headers: signed.headers, body });
21400
+ if (!response.ok)
21401
+ throw new Error(`S3 put failed with HTTP ${response.status}`);
21402
+ return {
21403
+ bucket: options.config.bucket,
21404
+ key,
21405
+ url: url.toString(),
21406
+ etag: response.headers.get("etag") ?? undefined
21407
+ };
21408
+ },
21409
+ async getObject(relativePath) {
21410
+ const url = buildS3ObjectUrl(options.config, buildS3ObjectKey(options.config, relativePath));
21411
+ const signed = signAwsV4Request({
21412
+ method: "GET",
21413
+ url,
21414
+ region: options.config.region ?? "us-east-1",
21415
+ service: "s3",
21416
+ headers: {},
21417
+ credentials: options.credentials,
21418
+ now: now3()
21419
+ });
21420
+ const response = await requestFetch(url, { method: "GET", headers: signed.headers });
21421
+ if (!response.ok)
21422
+ throw new Error(`S3 get failed with HTTP ${response.status}`);
21423
+ return response;
21424
+ },
21425
+ async deleteObject(relativePath) {
21426
+ const url = buildS3ObjectUrl(options.config, buildS3ObjectKey(options.config, relativePath));
21427
+ const signed = signAwsV4Request({
21428
+ method: "DELETE",
21429
+ url,
21430
+ region: options.config.region ?? "us-east-1",
21431
+ service: "s3",
21432
+ headers: {},
21433
+ credentials: options.credentials,
21434
+ now: now3()
21435
+ });
21436
+ const response = await requestFetch(url, { method: "DELETE", headers: signed.headers });
21437
+ if (!response.ok && response.status !== 404)
21438
+ throw new Error(`S3 delete failed with HTTP ${response.status}`);
21439
+ }
21440
+ };
21441
+ }
21442
+ function buildS3ObjectKey(config, relativePath) {
21443
+ const prefix = normalizeS3Prefix(config.prefix);
21444
+ const key = normalizeS3Key(relativePath);
21445
+ return `${prefix}${key}`;
21446
+ }
21447
+ function buildS3ObjectUrl(config, key) {
21448
+ const encodedKey = encodeS3Path(key);
21449
+ if (config.endpoint || config.forcePathStyle) {
21450
+ const base = new URL(config.endpoint ?? `https://s3.${config.region ?? "us-east-1"}.amazonaws.com`);
21451
+ base.pathname = `${trimTrailingSlash(base.pathname)}/${encodeURIComponent(config.bucket)}/${encodedKey}`;
21452
+ return base;
21453
+ }
21454
+ return new URL(`https://${config.bucket}.s3.${config.region ?? "us-east-1"}.amazonaws.com/${encodedKey}`);
21455
+ }
21456
+ function signAwsV4Request(input) {
21457
+ const amzDate = toAmzDate(input.now);
21458
+ const dateStamp = amzDate.slice(0, 8);
21459
+ const bodyHash = sha256Hex(input.body ?? new Uint8Array);
21460
+ const headers = normalizeHeaders({
21461
+ ...input.headers ?? {},
21462
+ host: input.url.host,
21463
+ "x-amz-content-sha256": bodyHash,
21464
+ "x-amz-date": amzDate,
21465
+ ...input.credentials.sessionToken ? { "x-amz-security-token": input.credentials.sessionToken } : {}
21466
+ });
21467
+ const signedHeaders = Object.keys(headers).sort().join(";");
21468
+ const canonicalHeaders = Object.keys(headers).sort().map((name) => `${name}:${headers[name]}
21469
+ `).join("");
21470
+ const canonicalRequest = [
21471
+ input.method.toUpperCase(),
21472
+ input.url.pathname || "/",
21473
+ canonicalQuery(input.url),
21474
+ canonicalHeaders,
21475
+ signedHeaders,
21476
+ bodyHash
21477
+ ].join(`
21478
+ `);
21479
+ const credentialScope = `${dateStamp}/${input.region}/${input.service}/aws4_request`;
21480
+ const stringToSign = [
21481
+ "AWS4-HMAC-SHA256",
21482
+ amzDate,
21483
+ credentialScope,
21484
+ sha256Hex(canonicalRequest)
21485
+ ].join(`
21486
+ `);
21487
+ const signingKey = getSigningKey(input.credentials.secretAccessKey, dateStamp, input.region, input.service);
21488
+ const signature = hmacHex(signingKey, stringToSign);
21489
+ return {
21490
+ headers: {
21491
+ ...headers,
21492
+ authorization: [
21493
+ `AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${credentialScope}`,
21494
+ `SignedHeaders=${signedHeaders}`,
21495
+ `Signature=${signature}`
21496
+ ].join(", ")
21497
+ },
21498
+ canonicalRequest,
21499
+ stringToSign
21500
+ };
21501
+ }
21502
+ function normalizeBody(body) {
21503
+ if (typeof body === "string")
21504
+ return Buffer.from(body);
21505
+ if (body instanceof Uint8Array)
21506
+ return body;
21507
+ if (body instanceof ArrayBuffer)
21508
+ return new Uint8Array(body);
21509
+ throw new Error("S3 body must be a string, Uint8Array, Buffer, or ArrayBuffer");
21510
+ }
21511
+ function normalizeS3Prefix(value) {
21512
+ const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
21513
+ if (!normalized || normalized.includes(".."))
21514
+ throw new Error("Invalid S3 object key");
21515
+ return normalized.endsWith("/") ? normalized : `${normalized}/`;
21516
+ }
21517
+ function normalizeS3Key(value) {
21518
+ const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
21519
+ if (!normalized || normalized.includes("..") || normalized.endsWith("/"))
21520
+ throw new Error("Invalid S3 object key");
21521
+ return normalized;
21522
+ }
21523
+ function encodeS3Path(key) {
21524
+ return key.split("/").filter(Boolean).map((part) => encodeURIComponent(part)).join("/");
21525
+ }
21526
+ function trimTrailingSlash(value) {
21527
+ return value.replace(/\/+$/, "");
21528
+ }
21529
+ function canonicalQuery(url) {
21530
+ return [...url.searchParams.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
21531
+ }
21532
+ function normalizeHeaders(headers) {
21533
+ const entries = Object.entries(headers).map(([name, value]) => [
21534
+ name.toLowerCase(),
21535
+ value.trim().replace(/\s+/g, " ")
21536
+ ]);
21537
+ entries.sort((left, right) => left[0].localeCompare(right[0]));
21538
+ return Object.fromEntries(entries);
21539
+ }
21540
+ function toAmzDate(date) {
21541
+ return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
21542
+ }
21543
+ function sha256Hex(value) {
21544
+ return createHash8("sha256").update(value).digest("hex");
21545
+ }
21546
+ function hmac(key, value) {
21547
+ return createHmac("sha256", key).update(value).digest();
21548
+ }
21549
+ function hmacHex(key, value) {
21550
+ return createHmac("sha256", key).update(value).digest("hex");
21551
+ }
21552
+ function getSigningKey(secretAccessKey, dateStamp, region, service) {
21553
+ const dateKey = hmac(`AWS4${secretAccessKey}`, dateStamp);
21554
+ const dateRegionKey = hmac(dateKey, region);
21555
+ const dateRegionServiceKey = hmac(dateRegionKey, service);
21556
+ return hmac(dateRegionServiceKey, "aws4_request");
21557
+ }
21558
+ // src/storage/s3-artifact-sync.ts
21559
+ init_database();
21560
+ async function uploadRunArtifactsToS3(options) {
21561
+ const db = options.db ?? getDatabase();
21562
+ const now3 = options.now ?? (() => new Date);
21563
+ const result = emptyResult();
21564
+ for (const artifact of listRunArtifacts(db, options.filter)) {
21565
+ try {
21566
+ const metadata = artifact.metadata;
21567
+ if (!options.filter?.includeAlreadySynced && remoteRef(metadata)) {
21568
+ result.skipped += 1;
21569
+ continue;
21570
+ }
21571
+ const content = exportStoredArtifactContent({
21572
+ id: artifact.id,
21573
+ path: artifact.path,
21574
+ size_bytes: artifact.size_bytes,
21575
+ sha256: artifact.sha256,
21576
+ metadata
21577
+ });
21578
+ if (!content) {
21579
+ result.skipped += 1;
21580
+ continue;
21581
+ }
21582
+ const ref = await options.store.putObject({
21583
+ key: content.relative_path,
21584
+ body: Buffer.from(content.base64, "base64"),
21585
+ contentType: mediaType(metadata) ?? "application/octet-stream",
21586
+ metadata: {
21587
+ artifact_id: artifact.id,
21588
+ run_id: artifact.run_id,
21589
+ task_id: artifact.task_id,
21590
+ sha256: content.sha256
21591
+ }
21592
+ });
21593
+ const remote = {
21594
+ provider: "s3",
21595
+ bucket: ref.bucket,
21596
+ key: ref.key,
21597
+ relative_path: content.relative_path,
21598
+ url: ref.url,
21599
+ sha256: content.sha256,
21600
+ size_bytes: content.size_bytes,
21601
+ uploaded_at: now3().toISOString()
21602
+ };
21603
+ updateArtifactMetadata(db, artifact.id, {
21604
+ ...metadata,
21605
+ remote_artifact_store: remote
21606
+ });
21607
+ result.uploaded += 1;
21608
+ result.artifacts.push({
21609
+ id: artifact.id,
21610
+ run_id: artifact.run_id,
21611
+ task_id: artifact.task_id,
21612
+ key: ref.key,
21613
+ sha256: content.sha256,
21614
+ size_bytes: content.size_bytes
21615
+ });
21616
+ } catch (error) {
21617
+ result.errors.push(`${artifact.id}: ${error instanceof Error ? error.message : String(error)}`);
21618
+ }
21619
+ }
21620
+ return result;
21621
+ }
21622
+ function planRunArtifactsS3Sync(options) {
21623
+ const db = options.db ?? getDatabase();
21624
+ const plan = {
21625
+ direction: options.direction,
21626
+ dry_run: true,
21627
+ no_network: true,
21628
+ total: 0,
21629
+ uploadable: 0,
21630
+ downloadable: 0,
21631
+ skipped: 0,
21632
+ errors: [],
21633
+ artifacts: []
21634
+ };
21635
+ for (const artifact of listRunArtifacts(db, options.filter)) {
21636
+ plan.total += 1;
21637
+ try {
21638
+ const metadata = artifact.metadata;
21639
+ const remote = remoteRef(metadata);
21640
+ const integrity = verifyStoredArtifact({
21641
+ id: artifact.id,
21642
+ path: artifact.path,
21643
+ size_bytes: artifact.size_bytes,
21644
+ sha256: artifact.sha256,
21645
+ metadata
21646
+ });
21647
+ if (options.direction === "upload") {
21648
+ if (remote && !options.filter?.includeAlreadySynced) {
21649
+ plan.skipped += 1;
21650
+ plan.artifacts.push(planArtifact(artifact, "already_remote", remote));
21651
+ } else if (integrity.status === "ok") {
21652
+ plan.uploadable += 1;
21653
+ plan.artifacts.push(planArtifact(artifact, "uploadable", remote));
21654
+ } else {
21655
+ plan.skipped += 1;
21656
+ plan.artifacts.push(planArtifact(artifact, integrity.status === "metadata_only" ? "metadata_only" : "missing_local", remote));
21657
+ }
21658
+ } else if (!remote) {
21659
+ plan.skipped += 1;
21660
+ plan.artifacts.push(planArtifact(artifact, "missing_remote_ref", remote));
21661
+ } else if (!options.force && integrity.status === "ok") {
21662
+ plan.skipped += 1;
21663
+ plan.artifacts.push(planArtifact(artifact, "local_ok", remote));
21664
+ } else {
21665
+ plan.downloadable += 1;
21666
+ plan.artifacts.push(planArtifact(artifact, "downloadable", remote));
21667
+ }
21668
+ } catch (error) {
21669
+ plan.errors.push(`${artifact.id}: ${error instanceof Error ? error.message : String(error)}`);
21670
+ }
21671
+ }
21672
+ return plan;
21673
+ }
21674
+ async function downloadRunArtifactsFromS3(options) {
21675
+ const db = options.db ?? getDatabase();
21676
+ const now3 = options.now ?? (() => new Date);
21677
+ const result = emptyResult();
21678
+ for (const artifact of listRunArtifacts(db, options.filter)) {
21679
+ try {
21680
+ const metadata = artifact.metadata;
21681
+ const remote = remoteRef(metadata);
21682
+ if (!remote) {
21683
+ result.skipped += 1;
21684
+ continue;
21685
+ }
21686
+ const integrity = verifyStoredArtifact({
21687
+ id: artifact.id,
21688
+ path: artifact.path,
21689
+ size_bytes: artifact.size_bytes,
21690
+ sha256: artifact.sha256,
21691
+ metadata
21692
+ });
21693
+ if (!options.force && integrity.status === "ok") {
21694
+ result.skipped += 1;
21695
+ continue;
21696
+ }
21697
+ const response = await options.store.getObject(remote.relative_path);
21698
+ const bytes = Buffer.from(await response.arrayBuffer());
21699
+ const report = importStoredArtifactContent({
21700
+ artifact_id: artifact.id,
21701
+ relative_path: remote.relative_path,
21702
+ sha256: remote.sha256,
21703
+ size_bytes: remote.size_bytes,
21704
+ base64: bytes.toString("base64")
21705
+ });
21706
+ if (report.status !== "ok")
21707
+ throw new Error(report.message);
21708
+ updateArtifactMetadata(db, artifact.id, {
21709
+ ...metadata,
21710
+ remote_artifact_store: {
21711
+ ...remote,
21712
+ downloaded_at: now3().toISOString()
21713
+ }
21714
+ });
21715
+ result.downloaded += 1;
21716
+ result.artifacts.push({
21717
+ id: artifact.id,
21718
+ run_id: artifact.run_id,
21719
+ task_id: artifact.task_id,
21720
+ key: remote.key,
21721
+ sha256: remote.sha256,
21722
+ size_bytes: remote.size_bytes
21723
+ });
21724
+ } catch (error) {
21725
+ result.errors.push(`${artifact.id}: ${error instanceof Error ? error.message : String(error)}`);
21726
+ }
21727
+ }
21728
+ return result;
21729
+ }
21730
+ function emptyResult() {
21731
+ return { uploaded: 0, downloaded: 0, skipped: 0, errors: [], artifacts: [] };
21732
+ }
21733
+ function planArtifact(artifact, status, remote) {
21734
+ return {
21735
+ id: artifact.id,
21736
+ run_id: artifact.run_id,
21737
+ task_id: artifact.task_id,
21738
+ status,
21739
+ sha256: artifact.sha256,
21740
+ size_bytes: artifact.size_bytes,
21741
+ ...remote ? { remote_key: remote.key } : {}
21742
+ };
21743
+ }
21744
+ function listRunArtifacts(db, filter = {}) {
21745
+ const conditions = [];
21746
+ const values = [];
21747
+ if (filter.runId) {
21748
+ conditions.push("run_id = ?");
21749
+ values.push(filter.runId);
21750
+ }
21751
+ if (filter.taskId) {
21752
+ conditions.push("task_id = ?");
21753
+ values.push(filter.taskId);
21754
+ }
21755
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
21756
+ const limit = filter.limit && filter.limit > 0 ? " LIMIT ?" : "";
21757
+ const rows = db.query(`SELECT * FROM task_run_artifacts ${where} ORDER BY created_at, id${limit}`).all(...limit ? [...values, String(filter.limit)] : values);
21758
+ return rows.map((row) => ({ ...row, metadata: parseMetadata4(row.metadata) }));
21759
+ }
21760
+ function updateArtifactMetadata(db, id, metadata) {
21761
+ db.run("UPDATE task_run_artifacts SET metadata = ? WHERE id = ?", [JSON.stringify(metadata), id]);
21762
+ }
21763
+ function parseMetadata4(value) {
21764
+ if (!value)
21765
+ return {};
21766
+ if (typeof value === "object" && !Array.isArray(value))
21767
+ return value;
21768
+ if (typeof value !== "string")
21769
+ return {};
21770
+ try {
21771
+ const parsed = JSON.parse(value);
21772
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
21773
+ } catch {
21774
+ return {};
21775
+ }
21776
+ }
21777
+ function remoteRef(metadata) {
21778
+ const value = metadata["remote_artifact_store"];
21779
+ if (!value || typeof value !== "object" || Array.isArray(value))
21780
+ return null;
21781
+ const record = value;
21782
+ if (record.provider !== "s3")
21783
+ return null;
21784
+ 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") {
21785
+ return null;
21786
+ }
21787
+ return record;
21788
+ }
21789
+ function mediaType(metadata) {
21790
+ const store = metadata["artifact_store"];
21791
+ if (!store || typeof store !== "object" || Array.isArray(store))
21792
+ return null;
21793
+ const value = store["media_type"];
21794
+ return typeof value === "string" ? value : null;
21795
+ }
19777
21796
  // src/sdk/types.ts
19778
21797
  class TodosAPIError extends Error {
19779
21798
  status;
@@ -20333,6 +22352,132 @@ class TodosClient {
20333
22352
  function createClient(options) {
20334
22353
  return new TodosClient(options);
20335
22354
  }
22355
+ // src/lib/native-storage-status.ts
22356
+ function getNativeStorageStatus(env = process.env) {
22357
+ const issues = [];
22358
+ const warnings = [];
22359
+ let config;
22360
+ try {
22361
+ config = loadTodosStorageConfig(env);
22362
+ } catch (error) {
22363
+ issues.push(error instanceof Error ? error.message : String(error));
22364
+ config = fallbackConfig(env);
22365
+ }
22366
+ try {
22367
+ assertTodosRemoteStorageConfig(config);
22368
+ } catch (error) {
22369
+ issues.push(error instanceof Error ? error.message : String(error));
22370
+ }
22371
+ const remoteEnabled = isTodosRemoteStorageEnabled(config);
22372
+ const remoteFieldsConfigured = Boolean(config.database || config.objectStorage);
22373
+ if (!remoteEnabled && remoteFieldsConfigured) {
22374
+ warnings.push(`${TODOS_STORAGE_ENV.mode}=local ignores configured remote storage fields`);
22375
+ }
22376
+ if (remoteEnabled && !config.objectStorage) {
22377
+ warnings.push(`${TODOS_STORAGE_ENV.s3Bucket} is not configured, so artifact sync will stay local`);
22378
+ }
22379
+ return {
22380
+ ok: issues.length === 0,
22381
+ service: "todos",
22382
+ mode: config.mode,
22383
+ local_default: config.mode === "local",
22384
+ remote_enabled: remoteEnabled,
22385
+ database: {
22386
+ configured: Boolean(config.database),
22387
+ provider: config.database?.provider ?? null,
22388
+ redacted_url: redactDatabaseUrl(config.database?.url),
22389
+ ssl: config.database?.ssl ?? null,
22390
+ schema: config.database?.schema ?? null
22391
+ },
22392
+ object_storage: {
22393
+ configured: Boolean(config.objectStorage),
22394
+ provider: config.objectStorage?.provider ?? null,
22395
+ bucket: config.objectStorage?.bucket ?? null,
22396
+ prefix: config.objectStorage?.prefix ?? null,
22397
+ region: config.objectStorage?.region ?? null,
22398
+ endpoint_configured: Boolean(config.objectStorage?.endpoint),
22399
+ force_path_style: config.objectStorage?.forcePathStyle ?? false
22400
+ },
22401
+ sync: {
22402
+ batch_size: config.sync.batchSize,
22403
+ dry_run: config.sync.dryRun
22404
+ },
22405
+ env: storageEnvStatus(env),
22406
+ canonical: getCanonicalTodosRdsConfig(),
22407
+ issues,
22408
+ warnings,
22409
+ no_network: true
22410
+ };
22411
+ }
22412
+ function getNativeStorageSyncPlan(env = process.env, options = {}) {
22413
+ const status = getNativeStorageStatus(env);
22414
+ const steps = [
22415
+ "Read local SQLite snapshot",
22416
+ status.remote_enabled ? "Prepare Postgres sync table upserts" : "Skip remote database writes in local mode",
22417
+ status.object_storage.configured ? "Plan S3 artifact object writes" : "Keep artifact storage local",
22418
+ "Report planned changes without opening network connections"
22419
+ ];
22420
+ return {
22421
+ ok: status.ok,
22422
+ service: "todos",
22423
+ dry_run: true,
22424
+ no_network: true,
22425
+ status,
22426
+ postgres: {
22427
+ required: status.remote_enabled,
22428
+ configured: status.database.configured,
22429
+ schema_sql: options.includeSchemaSql ? postgresTodosSyncSchemaSql() : []
22430
+ },
22431
+ object_storage: {
22432
+ required: status.remote_enabled,
22433
+ configured: status.object_storage.configured,
22434
+ bucket: status.object_storage.bucket,
22435
+ prefix: status.object_storage.prefix
22436
+ },
22437
+ steps
22438
+ };
22439
+ }
22440
+ function redactDatabaseUrl(value) {
22441
+ if (!value)
22442
+ return null;
22443
+ try {
22444
+ const url = new URL(value);
22445
+ if (url.username)
22446
+ url.username = "***";
22447
+ if (url.password)
22448
+ url.password = "***";
22449
+ return url.toString();
22450
+ } catch {
22451
+ return "(redacted)";
22452
+ }
22453
+ }
22454
+ function storageEnvStatus(env) {
22455
+ return Object.fromEntries(Object.entries(TODOS_STORAGE_ENV).map(([key, name]) => [
22456
+ key,
22457
+ {
22458
+ name,
22459
+ active_name: getTodosStorageEnvName(env, key),
22460
+ configured: Boolean(clean2(env[getTodosStorageEnvName(env, key)]))
22461
+ }
22462
+ ]));
22463
+ }
22464
+ function fallbackConfig(env) {
22465
+ const modeValue = clean2(env[TODOS_STORAGE_ENV.mode]);
22466
+ const mode = modeValue === "remote" || modeValue === "hybrid" ? modeValue : "local";
22467
+ return {
22468
+ service: "todos",
22469
+ mode,
22470
+ sync: {
22471
+ batchSize: 500,
22472
+ dryRun: false
22473
+ }
22474
+ };
22475
+ }
22476
+ function clean2(value) {
22477
+ const trimmed = value?.trim();
22478
+ return trimmed ? trimmed : undefined;
22479
+ }
22480
+
20336
22481
  // src/index.ts
20337
22482
  init_database();
20338
22483
 
@@ -21101,7 +23246,7 @@ function scoreHealth(scope, scopeId, db) {
21101
23246
  JOIN tasks dep ON dep.id = td.depends_on
21102
23247
  WHERE td.task_id = ? AND dep.status != 'completed'`).all(task2.id);
21103
23248
  return { id: task2.id, short_id: task2.short_id, title: redactEvidenceText(task2.title), blockers };
21104
- }).filter((entry) => entry.blockers.length > 0);
23249
+ }).filter((entry2) => entry2.blockers.length > 0);
21105
23250
  const overdue = tasks.filter((task2) => activeTaskIds.has(task2.id) && Boolean(task2.due_at && task2.due_at < generatedAt)).map((task2) => ({ id: task2.id, short_id: task2.short_id, title: redactEvidenceText(task2.title), due_at: task2.due_at }));
21106
23251
  const failedChecks = d.query(`SELECT tv.id, tv.task_id, tv.command, tv.run_at
21107
23252
  FROM task_verifications tv
@@ -21656,7 +23801,7 @@ function getProjectByPathForBootstrap(path, db) {
21656
23801
  }
21657
23802
  // src/db/api-keys.ts
21658
23803
  init_database();
21659
- import { createHash as createHash8, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
23804
+ import { createHash as createHash9, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
21660
23805
  function rowToRecord(row) {
21661
23806
  return {
21662
23807
  id: row.id,
@@ -21670,7 +23815,7 @@ function rowToRecord(row) {
21670
23815
  };
21671
23816
  }
21672
23817
  function hashApiKey(key) {
21673
- return createHash8("sha256").update(key).digest("hex");
23818
+ return createHash9("sha256").update(key).digest("hex");
21674
23819
  }
21675
23820
  function safeEqualHex(a, b) {
21676
23821
  if (a.length !== b.length)
@@ -22023,11 +24168,11 @@ function exportArtifacts(filter = {}, db, dbPath) {
22023
24168
  }));
22024
24169
  return buildArtifactExportManifest(artifacts, dbPath);
22025
24170
  }
22026
- function importArtifactFromManifestEntry(entry, db, dbPath) {
22027
- const artifact = addArtifact(entry, db, dbPath);
22028
- if (entry.content_hash && artifact.content_hash !== entry.content_hash) {
24171
+ function importArtifactFromManifestEntry(entry2, db, dbPath) {
24172
+ const artifact = addArtifact(entry2, db, dbPath);
24173
+ if (entry2.content_hash && artifact.content_hash !== entry2.content_hash) {
22029
24174
  purgeArtifact(artifact.id, db, dbPath);
22030
- throw new Error(`Content hash mismatch for ${entry.name}: expected ${entry.content_hash}, got ${artifact.content_hash}`);
24175
+ throw new Error(`Content hash mismatch for ${entry2.name}: expected ${entry2.content_hash}, got ${artifact.content_hash}`);
22031
24176
  }
22032
24177
  return artifact;
22033
24178
  }
@@ -22038,11 +24183,16 @@ var FORBIDDEN_HOSTED_HOSTS = [
22038
24183
  "www.todos.md",
22039
24184
  "preview.todos.md",
22040
24185
  "pay.hasna.tools",
22041
- "platform-todos"
24186
+ ["platform", "todos"].join("-")
22042
24187
  ];
24188
+ var PRIVATE_PLATFORM_ORG = ["hasna", "studio"].join("");
24189
+ var HOSTED_TODOS_PACKAGE = ["platform", "todos"].join("-");
22043
24190
  var FORBIDDEN_WEB_PATTERNS = [
22044
24191
  { name: "hosted todos.md API", pattern: /https?:\/\/(?:www\.)?todos\.md/i },
22045
- { name: "platform-todos package", pattern: /@hasnastudio\/platform-todos|hasnastudio\/platform-todos/i },
24192
+ {
24193
+ name: "hosted platform package",
24194
+ pattern: new RegExp(`@${PRIVATE_PLATFORM_ORG}/${HOSTED_TODOS_PACKAGE}|${PRIVATE_PLATFORM_ORG}/${HOSTED_TODOS_PACKAGE}`, "i")
24195
+ },
22046
24196
  { name: "browser sign-in flow", pattern: /\/sign-?in\b|\/login\b.*(?:oauth|session|auth)/i },
22047
24197
  { name: "Stripe billing UI", pattern: /\bstripe\.(?:com|js)\b|\bcheckout\.sessions?\b/i },
22048
24198
  { name: "hosted OAuth redirect", pattern: /oauth.*redirect.*todos\.md/i }
@@ -22095,7 +24245,7 @@ function getHeadlessBoundaryManifest() {
22095
24245
  notes: [
22096
24246
  "Use todos CLI or todos-mcp for agent workflows.",
22097
24247
  "todos serve exposes a local-only REST API on 127.0.0.1 \u2014 not a hosted SaaS.",
22098
- "Cloud sync via @hasna/cloud is explicit opt-in from CLI/MCP, never from the dashboard."
24248
+ "Remote sync is explicit opt-in from CLI/MCP, never from the dashboard."
22099
24249
  ]
22100
24250
  };
22101
24251
  }
@@ -22832,10 +24982,10 @@ async function runVerificationProvider(input, db) {
22832
24982
  }
22833
24983
  return result;
22834
24984
  }
22835
- function verificationRowToRecord(row) {
24985
+ function taskVerificationRowToRecord(row) {
22836
24986
  return {
22837
24987
  id: String(row["id"]),
22838
- task_id: String(row["task_id"]),
24988
+ task_id: row["task_id"] ?? null,
22839
24989
  command: String(row["command"] ?? ""),
22840
24990
  status: row["status"] ?? "unknown",
22841
24991
  output_summary: row["output_summary"] ?? null,
@@ -22853,13 +25003,65 @@ function verificationRowToRecord(row) {
22853
25003
  }
22854
25004
  };
22855
25005
  }
25006
+ function portableVerificationRowToRecord(row) {
25007
+ let evidence = {};
25008
+ try {
25009
+ evidence = row["evidence"] ? JSON.parse(String(row["evidence"])) : {};
25010
+ } catch {
25011
+ evidence = {};
25012
+ }
25013
+ return {
25014
+ id: String(row["id"]),
25015
+ task_id: row["task_id"] ?? null,
25016
+ command: String(row["provider_name"] ?? "manual"),
25017
+ status: row["status"] ?? "unknown",
25018
+ output_summary: row["summary"] ?? null,
25019
+ artifact_path: row["artifact_id"] ?? null,
25020
+ agent_id: evidence["agent_id"] ?? null,
25021
+ run_at: String(row["completed_at"] ?? row["started_at"] ?? row["created_at"] ?? ""),
25022
+ created_at: String(row["created_at"] ?? ""),
25023
+ evidence: {
25024
+ ...evidence,
25025
+ provider_name: row["provider_name"] ?? evidence["provider_name"] ?? null,
25026
+ provider_type: row["provider_type"] ?? evidence["provider_type"] ?? "local",
25027
+ status: row["status"] ?? evidence["status"] ?? "unknown",
25028
+ summary: row["summary"] ?? evidence["summary"] ?? null,
25029
+ artifact_id: row["artifact_id"] ?? evidence["artifact_id"] ?? null
25030
+ }
25031
+ };
25032
+ }
22856
25033
  function getVerificationRecord(id, db) {
22857
25034
  const d = db || getDatabase();
25035
+ try {
25036
+ const portable = d.query("SELECT * FROM verification_records WHERE id = ?").get(id);
25037
+ if (portable)
25038
+ return portableVerificationRowToRecord(portable);
25039
+ } catch {}
22858
25040
  const row = d.query("SELECT * FROM task_verifications WHERE id = ?").get(id);
22859
- return row ? verificationRowToRecord(row) : null;
25041
+ return row ? taskVerificationRowToRecord(row) : null;
22860
25042
  }
22861
25043
  function listVerificationRecords(filter = {}, db) {
22862
25044
  const d = db || getDatabase();
25045
+ const records = [];
25046
+ try {
25047
+ const conditions2 = [];
25048
+ const params2 = [];
25049
+ if (filter.task_id) {
25050
+ conditions2.push("task_id = ?");
25051
+ params2.push(filter.task_id);
25052
+ }
25053
+ if (filter.status) {
25054
+ conditions2.push("status = ?");
25055
+ params2.push(filter.status);
25056
+ }
25057
+ if (filter.provider) {
25058
+ conditions2.push("provider_name = ?");
25059
+ params2.push(filter.provider);
25060
+ }
25061
+ const where2 = conditions2.length > 0 ? `WHERE ${conditions2.join(" AND ")}` : "";
25062
+ const rows2 = d.query(`SELECT * FROM verification_records ${where2} ORDER BY COALESCE(completed_at, started_at, created_at) DESC, created_at DESC`).all(...params2);
25063
+ records.push(...rows2.map(portableVerificationRowToRecord));
25064
+ } catch {}
22863
25065
  const conditions = [];
22864
25066
  const params = [];
22865
25067
  if (filter.task_id) {
@@ -22875,9 +25077,21 @@ function listVerificationRecords(filter = {}, db) {
22875
25077
  params.push(filter.status);
22876
25078
  }
22877
25079
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
22878
- const limitClause = filter.limit && filter.limit > 0 ? ` LIMIT ${Math.floor(filter.limit)}` : "";
22879
- const rows = d.query(`SELECT * FROM task_verifications ${where} ORDER BY run_at DESC, created_at DESC${limitClause}`).all(...params);
22880
- return rows.map(verificationRowToRecord);
25080
+ const rows = d.query(`SELECT * FROM task_verifications ${where} ORDER BY run_at DESC, created_at DESC`).all(...params);
25081
+ records.push(...rows.map(taskVerificationRowToRecord));
25082
+ let filtered = records;
25083
+ if (filter.agent_id) {
25084
+ filtered = filtered.filter((record) => record.agent_id === filter.agent_id || record.evidence.agent_id === filter.agent_id);
25085
+ }
25086
+ if (filter.run_record_id) {
25087
+ filtered = filtered.filter((record) => record.evidence.run_record_id === filter.run_record_id);
25088
+ }
25089
+ filtered.sort((a, b) => {
25090
+ const at = Date.parse(a.run_at || a.created_at || "");
25091
+ const bt = Date.parse(b.run_at || b.created_at || "");
25092
+ return (Number.isFinite(bt) ? bt : 0) - (Number.isFinite(at) ? at : 0);
25093
+ });
25094
+ return filter.limit && filter.limit > 0 ? filtered.slice(0, Math.floor(filter.limit)) : filtered;
22881
25095
  }
22882
25096
  // src/lib/verification-evidence.ts
22883
25097
  init_database();
@@ -22915,24 +25129,24 @@ function toPortableEvidence(record) {
22915
25129
  id: record.id,
22916
25130
  task_id: record.task_id,
22917
25131
  run_record_id: ev.run_record_id ?? null,
22918
- agent_id: ev.agent_id ?? verifier.agent_id ?? null,
22919
- provider_name: record.provider_name,
22920
- provider_type: record.provider_type,
25132
+ agent_id: record.agent_id ?? ev.agent_id ?? verifier.agent_id ?? null,
25133
+ provider_name: ev.provider_name ?? ev.provider ?? record.command,
25134
+ provider_type: ev.provider_type ?? "local",
22921
25135
  status: record.status,
22922
- summary: record.summary,
25136
+ summary: ev.summary ?? record.output_summary ?? record.command,
22923
25137
  confidence: typeof ev.confidence === "number" ? ev.confidence : null,
22924
25138
  commands: ev.commands ?? [],
22925
25139
  test_results: ev.test_results ?? [],
22926
25140
  links: ev.links ?? [],
22927
25141
  artifact_ids: [
22928
25142
  ...ev.artifact_ids ?? [],
22929
- ...record.artifact_id ? [record.artifact_id] : []
25143
+ ...record.artifact_path ? [record.artifact_path] : []
22930
25144
  ],
22931
25145
  log_excerpt: ev.log_excerpt ?? ev.stdout?.slice(-2000) ?? null,
22932
25146
  screenshot_paths: ev.screenshot_paths ?? [],
22933
25147
  verifier,
22934
- started_at: record.started_at,
22935
- completed_at: record.completed_at,
25148
+ started_at: record.run_at,
25149
+ completed_at: record.run_at,
22936
25150
  created_at: record.created_at,
22937
25151
  metadata: ev.metadata ?? {}
22938
25152
  };
@@ -23316,7 +25530,10 @@ function buildResourceSnapshot(uri, staleMs = DEFAULT_STALE_MS) {
23316
25530
  };
23317
25531
  }
23318
25532
  function isSnapshotStale(snapshot, now3 = new Date) {
23319
- return now3.toISOString() > snapshot.stale_after;
25533
+ const staleAt = Date.parse(snapshot.stale_after);
25534
+ if (!Number.isFinite(staleAt))
25535
+ return true;
25536
+ return now3.getTime() >= staleAt;
23320
25537
  }
23321
25538
  function subscribeResource(uri, agentId) {
23322
25539
  const key = `${uri}:${agentId || "*"}`;
@@ -23655,7 +25872,7 @@ async function runNextAgentDispatch(input = {}, db) {
23655
25872
  return { run_id: next.run.id, task_id: next.run.task_id, command, dry_run: false, status, exit_code: exitCode, output_summary: outputSummary };
23656
25873
  }
23657
25874
  function listAgentRuns(filter = {}, db) {
23658
- let runs = listAgentRunQueue(db);
25875
+ let runs = listAgentRunQueue(db).map(toAgentRun);
23659
25876
  if (filter.task_id)
23660
25877
  runs = runs.filter((r) => r.task_id === filter.task_id);
23661
25878
  if (filter.status)
@@ -23667,11 +25884,12 @@ function listAgentRuns(filter = {}, db) {
23667
25884
  return runs;
23668
25885
  }
23669
25886
  function retryAgentRun(runId, db) {
23670
- return retryAgentRunDispatch(runId, db);
25887
+ return toAgentRun(retryAgentRunDispatch(runId, db));
23671
25888
  }
23672
25889
  var AGENT_RUN_SCHEMA_VERSION = "todos.agent_run.v1";
23673
25890
  function toAgentRun(item) {
23674
25891
  const md = item.run.metadata || {};
25892
+ const maxRetries = typeof md["agent_run_max_retries"] === "number" ? md["agent_run_max_retries"] : 3;
23675
25893
  return {
23676
25894
  id: item.run.id,
23677
25895
  task_id: item.run.task_id,
@@ -23679,7 +25897,10 @@ function toAgentRun(item) {
23679
25897
  adapter: item.dispatcher.adapter ?? null,
23680
25898
  status: item.dispatcher.state,
23681
25899
  command: item.dispatcher.command ?? null,
25900
+ error: item.dispatcher.last_error ?? null,
23682
25901
  evidence: md["agent_run_evidence"] ?? {},
25902
+ retry_count: Math.max(0, item.dispatcher.attempt - 1),
25903
+ max_retries: maxRetries,
23683
25904
  queued_at: item.dispatcher.queued_at,
23684
25905
  started_at: item.dispatcher.started_at ?? null,
23685
25906
  completed_at: item.dispatcher.completed_at ?? null,
@@ -24016,11 +26237,11 @@ function looksLikeFile(value) {
24016
26237
  }
24017
26238
  function parseMention(raw) {
24018
26239
  const input = raw.trim();
24019
- const clean = input.startsWith("@") ? input.slice(1) : input;
24020
- if (/^#\d+$/.test(clean)) {
24021
- return { input, kind: "pull_request", target: clean.slice(1), explicit: true };
26240
+ const clean3 = input.startsWith("@") ? input.slice(1) : input;
26241
+ if (/^#\d+$/.test(clean3)) {
26242
+ return { input, kind: "pull_request", target: clean3.slice(1), explicit: true };
24022
26243
  }
24023
- const prefixMatch = clean.match(/^([a-z_]+):(.*)$/i);
26244
+ const prefixMatch = clean3.match(/^([a-z_]+):(.*)$/i);
24024
26245
  if (prefixMatch && PREFIXES[prefixMatch[1].toLowerCase()]) {
24025
26246
  const kind = PREFIXES[prefixMatch[1].toLowerCase()];
24026
26247
  const target = prefixMatch[2].trim();
@@ -24030,13 +26251,13 @@ function parseMention(raw) {
24030
26251
  }
24031
26252
  return { input, kind, target, explicit: true };
24032
26253
  }
24033
- if (looksLikeCommit(clean))
24034
- return { input, kind: "commit", target: clean, explicit: false };
24035
- if (looksLikeFile(clean)) {
24036
- const parsed = parseLineAnchor(clean);
26254
+ if (looksLikeCommit(clean3))
26255
+ return { input, kind: "commit", target: clean3, explicit: false };
26256
+ if (looksLikeFile(clean3)) {
26257
+ const parsed = parseLineAnchor(clean3);
24037
26258
  return { input, kind: "file", target: parsed.path, line: parsed.line, explicit: false };
24038
26259
  }
24039
- return { input, kind: "unknown", target: clean, explicit: false };
26260
+ return { input, kind: "unknown", target: clean3, explicit: false };
24040
26261
  }
24041
26262
  function runGit2(root, args) {
24042
26263
  try {
@@ -24090,20 +26311,20 @@ function resolveFile(parsed, workspace) {
24090
26311
  function walkSourceFiles(root, current = root, files = []) {
24091
26312
  if (files.length >= 5000)
24092
26313
  return files;
24093
- for (const entry of readdirSync2(current, { withFileTypes: true })) {
24094
- if (entry.name.startsWith(".") && ![".github"].includes(entry.name)) {
24095
- if (SKIP_DIRS.has(entry.name))
26314
+ for (const entry2 of readdirSync2(current, { withFileTypes: true })) {
26315
+ if (entry2.name.startsWith(".") && ![".github"].includes(entry2.name)) {
26316
+ if (SKIP_DIRS.has(entry2.name))
24096
26317
  continue;
24097
26318
  }
24098
- const absolutePath = join11(current, entry.name);
24099
- if (entry.isDirectory()) {
24100
- if (!SKIP_DIRS.has(entry.name))
26319
+ const absolutePath = join11(current, entry2.name);
26320
+ if (entry2.isDirectory()) {
26321
+ if (!SKIP_DIRS.has(entry2.name))
24101
26322
  walkSourceFiles(root, absolutePath, files);
24102
26323
  continue;
24103
26324
  }
24104
- if (!entry.isFile())
26325
+ if (!entry2.isFile())
24105
26326
  continue;
24106
- const extension = `.${basename3(entry.name).split(".").pop() || ""}`;
26327
+ const extension = `.${basename3(entry2.name).split(".").pop() || ""}`;
24107
26328
  if (SOURCE_EXTENSIONS.has(extension) && statSync4(absolutePath).size <= 512 * 1024) {
24108
26329
  files.push(absolutePath);
24109
26330
  }
@@ -24420,7 +26641,7 @@ function getTaskLabels(taskId, db) {
24420
26641
  // src/db/custom-fields.ts
24421
26642
  init_database();
24422
26643
  var CUSTOM_FIELD_TYPES = ["text", "number", "boolean", "date", "enum"];
24423
- function slugify2(name) {
26644
+ function slugify3(name) {
24424
26645
  return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
24425
26646
  }
24426
26647
  function rowToDef(row) {
@@ -24441,7 +26662,7 @@ function createCustomFieldDefinition(input, db) {
24441
26662
  const d = db || getDatabase();
24442
26663
  const id = uuid();
24443
26664
  const ts = now();
24444
- const slug = slugify2(input.name);
26665
+ const slug = slugify3(input.name);
24445
26666
  d.run(`INSERT INTO custom_field_definitions (
24446
26667
  id, project_id, name, slug, field_type, options, required, default_value, sort_order, created_at, updated_at
24447
26668
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
@@ -24463,7 +26684,7 @@ function getCustomFieldDefinition(idOrSlug, db) {
24463
26684
  const d = db || getDatabase();
24464
26685
  let row = d.query("SELECT * FROM custom_field_definitions WHERE id = ?").get(idOrSlug);
24465
26686
  if (!row)
24466
- row = d.query("SELECT * FROM custom_field_definitions WHERE slug = ?").get(slugify2(idOrSlug));
26687
+ row = d.query("SELECT * FROM custom_field_definitions WHERE slug = ?").get(slugify3(idOrSlug));
24467
26688
  return row ? rowToDef(row) : null;
24468
26689
  }
24469
26690
  function listCustomFieldDefinitions(projectId, db) {
@@ -25157,11 +27378,10 @@ function acquireTaskLease(taskId, agentId, ttlMinutes = DEFAULT_LEASE_MINUTES, d
25157
27378
  const d = db || getDatabase();
25158
27379
  const lock = lockTask(taskId, agentId, d);
25159
27380
  if (!lock.success) {
25160
- const task2 = getTask(taskId, d);
25161
27381
  const existing = d.query("SELECT * FROM task_leases WHERE task_id = ?").get(taskId);
25162
27382
  return {
25163
27383
  success: false,
25164
- conflict: formatLockConflict(taskId, lock.locked_by, lock.locked_at, existing?.expires_at ?? null)
27384
+ conflict: formatLockConflict(taskId, lock.locked_by ?? "unknown", lock.locked_at ?? null, existing?.expires_at ?? null)
25165
27385
  };
25166
27386
  }
25167
27387
  const ts = now();
@@ -25230,7 +27450,7 @@ function stealTaskLease(taskId, agentId, options = {}, db) {
25230
27450
  if (stolen?.id === taskId)
25231
27451
  acquired = true;
25232
27452
  else
25233
- return { success: false, conflict: formatLockConflict(taskId, lock.locked_by, lock.locked_at, lease?.expires_at ?? null) };
27453
+ return { success: false, conflict: formatLockConflict(taskId, lock.locked_by ?? "unknown", lock.locked_at ?? null, lease?.expires_at ?? null) };
25234
27454
  }
25235
27455
  if (!acquired) {
25236
27456
  return { success: false, conflict: formatLockConflict(taskId, previous ?? "unknown", task2.locked_at) };
@@ -25375,19 +27595,19 @@ function getParityReport() {
25375
27595
  let cliOnly = 0;
25376
27596
  let mcpOnly = 0;
25377
27597
  let documentedGaps = 0;
25378
- for (const entry of CLI_MCP_PARITY_MANIFEST) {
25379
- byDomain[entry.domain] = byDomain[entry.domain] ?? { matched: 0, gaps: 0 };
25380
- if (entry.cli && entry.mcp) {
27598
+ for (const entry2 of CLI_MCP_PARITY_MANIFEST) {
27599
+ byDomain[entry2.domain] = byDomain[entry2.domain] ?? { matched: 0, gaps: 0 };
27600
+ if (entry2.cli && entry2.mcp) {
25381
27601
  matched++;
25382
- byDomain[entry.domain].matched++;
25383
- } else if (entry.cli && !entry.mcp) {
27602
+ byDomain[entry2.domain].matched++;
27603
+ } else if (entry2.cli && !entry2.mcp) {
25384
27604
  cliOnly++;
25385
27605
  documentedGaps++;
25386
- byDomain[entry.domain].gaps++;
25387
- } else if (!entry.cli && entry.mcp) {
27606
+ byDomain[entry2.domain].gaps++;
27607
+ } else if (!entry2.cli && entry2.mcp) {
25388
27608
  mcpOnly++;
25389
27609
  }
25390
- if (entry.gap)
27610
+ if (entry2.gap)
25391
27611
  documentedGaps++;
25392
27612
  }
25393
27613
  return {
@@ -25404,16 +27624,16 @@ function getParityReport() {
25404
27624
  function validateParityManifest() {
25405
27625
  const issues = [];
25406
27626
  const seen = new Set;
25407
- for (const entry of CLI_MCP_PARITY_MANIFEST) {
25408
- const key = `${entry.domain}:${entry.operation}`;
27627
+ for (const entry2 of CLI_MCP_PARITY_MANIFEST) {
27628
+ const key = `${entry2.domain}:${entry2.operation}`;
25409
27629
  if (seen.has(key))
25410
27630
  issues.push(`Duplicate parity entry: ${key}`);
25411
27631
  seen.add(key);
25412
- if (!entry.cli && !entry.mcp && !entry.gap) {
27632
+ if (!entry2.cli && !entry2.mcp && !entry2.gap) {
25413
27633
  issues.push(`Entry ${key} has no cli, mcp, or gap documentation`);
25414
27634
  }
25415
- if (entry.cli && !entry.mcp && !entry.gap || !entry.cli && entry.mcp && !entry.gap) {
25416
- if (!entry.notes) {
27635
+ if (entry2.cli && !entry2.mcp && !entry2.gap || !entry2.cli && entry2.mcp && !entry2.gap) {
27636
+ if (!entry2.notes) {
25417
27637
  issues.push(`Entry ${key} is one-sided without gap or notes`);
25418
27638
  }
25419
27639
  }
@@ -25471,7 +27691,7 @@ var PROFILE_META = {
25471
27691
  },
25472
27692
  admin: {
25473
27693
  name: "admin",
25474
- description: "Full access including migrations and cloud bridge tools",
27694
+ description: "Full access including migrations and storage bridge tools",
25475
27695
  surfaces: ["cli", "mcp", "sdk", "http"],
25476
27696
  allows_mutations: true,
25477
27697
  allows_admin: true
@@ -25694,7 +27914,6 @@ var AGENT_ADAPTER_DOCS = {
25694
27914
  display_name: "OpenAI Codex CLI",
25695
27915
  install: {
25696
27916
  bun: "bun install -g @hasna/todos",
25697
- npm: "npm install -g @hasna/todos",
25698
27917
  verify: "todos --version && which todos-mcp"
25699
27918
  },
25700
27919
  mcp: {
@@ -25972,7 +28191,7 @@ function getAdapterDocsFingerprint() {
25972
28191
  init_database();
25973
28192
  import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
25974
28193
  import { basename as basename4 } from "path";
25975
- import { createHash as createHash9 } from "crypto";
28194
+ import { createHash as createHash10 } from "crypto";
25976
28195
  init_secret_redaction();
25977
28196
  var INBOX_INTAKE_SCHEMA = "todos.inbox_intake.v1";
25978
28197
  var INTAKE_SOURCE_TYPES = [
@@ -25985,7 +28204,7 @@ var INTAKE_SOURCE_TYPES = [
25985
28204
  ];
25986
28205
  var INTAKE_TRIAGE_STATUSES = ["preview", "triaged", "duplicate", "created"];
25987
28206
  function fingerprint2(text) {
25988
- return createHash9("sha256").update(text).digest("hex").slice(0, 16);
28207
+ return createHash10("sha256").update(text).digest("hex").slice(0, 16);
25989
28208
  }
25990
28209
  function loadRawContent(input) {
25991
28210
  if (input.github_url) {
@@ -26136,7 +28355,7 @@ ${parsed.failures.map((f) => `- ${f}`).join(`
26136
28355
  return {
26137
28356
  title: titleOverride ?? raw.split(/\n/)[0].slice(0, 200),
26138
28357
  description: raw.includes(`
26139
- `) ? raw.slice(0, 8000) : undefined,
28358
+ `) ? raw.slice(0, 8000) : "",
26140
28359
  extra: {}
26141
28360
  };
26142
28361
  }
@@ -26153,10 +28372,9 @@ function findIntakeDuplicate(createInput, sourceFingerprint, db) {
26153
28372
  return { task_id: t.id, short_id: t.short_id, title: t.title, score: 1 };
26154
28373
  }
26155
28374
  }
26156
- const candidates = findDuplicateCandidates({ min_score: 0.75, limit: 5 }, db);
28375
+ const candidates = findDuplicateCandidates({ threshold: 0.75, limit: 5 }, db);
26157
28376
  for (const c of candidates) {
26158
- const otherId = c.task_a_id;
26159
- const other = getTask(otherId, db);
28377
+ const other = c.primary_task;
26160
28378
  if (!other)
26161
28379
  continue;
26162
28380
  const normNew = createInput.title.toLowerCase();
@@ -26655,7 +28873,7 @@ var JIRA_PRIORITY = {
26655
28873
  low: "low",
26656
28874
  lowest: "low"
26657
28875
  };
26658
- function asRecord2(value) {
28876
+ function asRecord3(value) {
26659
28877
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
26660
28878
  }
26661
28879
  function labelNames2(labels) {
@@ -26664,7 +28882,7 @@ function labelNames2(labels) {
26664
28882
  return labels.map((label) => {
26665
28883
  if (typeof label === "string")
26666
28884
  return label;
26667
- const rec = asRecord2(label);
28885
+ const rec = asRecord3(label);
26668
28886
  return typeof rec?.name === "string" ? rec.name : null;
26669
28887
  }).filter((name) => !!name);
26670
28888
  }
@@ -26690,7 +28908,7 @@ function priorityFromLinear(value) {
26690
28908
  return "medium";
26691
28909
  }
26692
28910
  function priorityFromJira(value) {
26693
- const rec = asRecord2(value);
28911
+ const rec = asRecord3(value);
26694
28912
  const name = typeof rec?.name === "string" ? rec.name.toLowerCase() : "";
26695
28913
  return JIRA_PRIORITY[name] ?? "medium";
26696
28914
  }
@@ -26732,10 +28950,10 @@ function normalizeLinearIssue(raw) {
26732
28950
  const title = typeof raw.title === "string" ? raw.title.trim() : "";
26733
28951
  if (!identifier || !title)
26734
28952
  return null;
26735
- const stateRec = asRecord2(raw.state);
28953
+ const stateRec = asRecord3(raw.state);
26736
28954
  const state = typeof raw.state === "string" ? raw.state : typeof stateRec?.name === "string" ? stateRec.name : undefined;
26737
28955
  const url = typeof raw.url === "string" ? raw.url : undefined;
26738
- const labelSource = asRecord2(raw.labels);
28956
+ const labelSource = asRecord3(raw.labels);
26739
28957
  const labels = labelNames2(Array.isArray(labelSource?.nodes) ? labelSource.nodes : raw.labels);
26740
28958
  return {
26741
28959
  source: "linear",
@@ -26758,16 +28976,16 @@ function normalizeLinearIssue(raw) {
26758
28976
  }
26759
28977
  function normalizeJiraIssue(raw) {
26760
28978
  const key = typeof raw.key === "string" ? raw.key.trim() : "";
26761
- const fields = asRecord2(raw.fields);
28979
+ const fields = asRecord3(raw.fields);
26762
28980
  if (!key || !fields)
26763
28981
  return null;
26764
28982
  const summary = typeof fields.summary === "string" ? fields.summary.trim() : "";
26765
28983
  if (!summary)
26766
28984
  return null;
26767
- const statusRec = asRecord2(fields.status);
28985
+ const statusRec = asRecord3(fields.status);
26768
28986
  const status = typeof statusRec?.name === "string" ? statusRec.name : undefined;
26769
28987
  const labels = labelNames2(fields.labels);
26770
- const description = typeof fields.description === "string" ? fields.description.slice(0, 8000) : typeof asRecord2(fields.description)?.content === "string" ? String(asRecord2(fields.description)?.content).slice(0, 8000) : undefined;
28988
+ const description = typeof fields.description === "string" ? fields.description.slice(0, 8000) : typeof asRecord3(fields.description)?.content === "string" ? String(asRecord3(fields.description)?.content).slice(0, 8000) : undefined;
26771
28989
  return {
26772
28990
  source: "jira",
26773
28991
  external_ref: `jira:${key}`,
@@ -26781,13 +28999,13 @@ function normalizeJiraIssue(raw) {
26781
28999
  raw_metadata: {
26782
29000
  jira_key: key,
26783
29001
  jira_status: status,
26784
- jira_priority: asRecord2(fields.priority)?.name,
29002
+ jira_priority: asRecord3(fields.priority)?.name,
26785
29003
  jira_id: raw.id
26786
29004
  }
26787
29005
  };
26788
29006
  }
26789
29007
  function looksLikeJiraIssue(raw) {
26790
- return typeof raw.key === "string" && !!asRecord2(raw.fields)?.summary;
29008
+ return typeof raw.key === "string" && !!asRecord3(raw.fields)?.summary;
26791
29009
  }
26792
29010
  function looksLikeLinearIssue(raw) {
26793
29011
  if (typeof raw.identifier !== "string")
@@ -26814,17 +29032,17 @@ function detectIssueExportSource(data) {
26814
29032
  }
26815
29033
  function extractIssueRecords(data) {
26816
29034
  if (Array.isArray(data)) {
26817
- return data.map(asRecord2).filter((item) => !!item);
29035
+ return data.map(asRecord3).filter((item) => !!item);
26818
29036
  }
26819
- const root = asRecord2(data);
29037
+ const root = asRecord3(data);
26820
29038
  if (!root)
26821
29039
  return [];
26822
- const dataNode = asRecord2(root.data);
26823
- const issuesNode = asRecord2(root.issues);
26824
- const nestedIssues = asRecord2(dataNode?.issues);
29040
+ const dataNode = asRecord3(root.data);
29041
+ const issuesNode = asRecord3(root.issues);
29042
+ const nestedIssues = asRecord3(dataNode?.issues);
26825
29043
  const nodes = nestedIssues?.nodes ?? issuesNode?.nodes ?? root.issues ?? dataNode?.issues;
26826
29044
  if (Array.isArray(nodes)) {
26827
- return nodes.map(asRecord2).filter((item) => !!item);
29045
+ return nodes.map(asRecord3).filter((item) => !!item);
26828
29046
  }
26829
29047
  if (looksLikeGitHubIssue(root) || looksLikeLinearIssue(root) || looksLikeJiraIssue(root)) {
26830
29048
  return [root];
@@ -26880,9 +29098,9 @@ function findIssueDuplicate(createInput, db) {
26880
29098
  return { task_id: task2.id, short_id: task2.short_id, title: task2.title, score: 1 };
26881
29099
  }
26882
29100
  }
26883
- const candidates = findDuplicateCandidates({ min_score: 0.85, limit: 5 }, db);
29101
+ const candidates = findDuplicateCandidates({ threshold: 0.85, limit: 5 }, db);
26884
29102
  for (const candidate of candidates) {
26885
- const other = getTask(candidate.task_a_id, db);
29103
+ const other = candidate.primary_task;
26886
29104
  if (!other)
26887
29105
  continue;
26888
29106
  if (other.title.toLowerCase() === createInput.title.toLowerCase()) {
@@ -27260,7 +29478,10 @@ import { existsSync as existsSync17, readFileSync as readFileSync15, readdirSync
27260
29478
  import { join as join13, relative as relative5 } from "path";
27261
29479
  var RELEASE_CHECK_SCHEMA = "todos.release_check.v1";
27262
29480
  var FORBIDDEN_DIST_PATTERNS = [
27263
- { id: "platform_todos", pattern: /platform-todos|@hasnastudio\/platform-todos/i },
29481
+ {
29482
+ id: "hosted_platform_todos",
29483
+ pattern: new RegExp(`${["platform", "todos"].join("-")}|@${["hasna", "studio"].join("")}/${["platform", "todos"].join("-")}`, "i")
29484
+ },
27264
29485
  { id: "stripe", pattern: /\bstripe\.(?:com|js)\b|checkout\.sessions/i },
27265
29486
  { id: "hardcoded_aws_key", pattern: /\bAKIA[0-9A-Z]{16}\b/ },
27266
29487
  { id: "openai_key", pattern: /\bsk-[a-zA-Z0-9]{20,}\b/ }
@@ -27275,12 +29496,12 @@ function readPackageJson3(root) {
27275
29496
  function walkFiles(dir, acc = []) {
27276
29497
  if (!existsSync17(dir))
27277
29498
  return acc;
27278
- for (const entry of readdirSync3(dir)) {
27279
- const full = join13(dir, entry);
29499
+ for (const entry2 of readdirSync3(dir)) {
29500
+ const full = join13(dir, entry2);
27280
29501
  const st = statSync5(full);
27281
29502
  if (st.isDirectory())
27282
29503
  walkFiles(full, acc);
27283
- else if (/\.(js|mjs|cjs|json|d\.ts)$/.test(entry))
29504
+ else if (/\.(js|mjs|cjs|json|d\.ts)$/.test(entry2))
27284
29505
  acc.push(full);
27285
29506
  }
27286
29507
  return acc;
@@ -27419,7 +29640,7 @@ todos-mcp --help 2>&1 | head -1 || true
27419
29640
  - publishConfig public access
27420
29641
  - prepublishOnly build hook
27421
29642
 
27422
- Cloud sync via \`@hasna/cloud\` is explicit opt-in \u2014 not required for local-only usage.
29643
+ Remote sync is explicit opt-in \u2014 not required for local-only usage.
27423
29644
  `;
27424
29645
  }
27425
29646
  function runReleaseChecks(options = {}) {
@@ -27656,12 +29877,13 @@ function backupDatabase(outputPath, sourcePath) {
27656
29877
  throw new Error(`Database not found: ${source9}`);
27657
29878
  mkdirSync12(dirname11(outputPath), { recursive: true });
27658
29879
  closeDatabase();
27659
- const src = new Database3(source9, { readonly: true });
29880
+ const src = new Database3(source9);
27660
29881
  try {
27661
- src.exec("PRAGMA wal_checkpoint(FULL)");
29882
+ src.exec("PRAGMA wal_checkpoint(TRUNCATE)");
27662
29883
  } catch {}
29884
+ const image = src.serialize();
27663
29885
  src.close();
27664
- copyFileSync(source9, outputPath);
29886
+ writeFileSync10(outputPath, image);
27665
29887
  const method = "file_copy";
27666
29888
  const bytes = statSync6(outputPath).size;
27667
29889
  return {
@@ -27980,8 +30202,8 @@ var SCHEMA_CONTRACT_FIXTURES = {
27980
30202
  project: {
27981
30203
  schema_version: "todos.project.v1",
27982
30204
  id: "proj-001",
27983
- name: "open-todos",
27984
- path: "/tmp/open-todos",
30205
+ name: "todos-example",
30206
+ path: "/tmp/todos-example",
27985
30207
  task_counter: 0,
27986
30208
  created_at: "2026-01-01T00:00:00.000Z",
27987
30209
  updated_at: "2026-01-01T00:00:00.000Z"
@@ -28286,7 +30508,7 @@ function listReadyScheduledTasks(filters = {}, db) {
28286
30508
  return !start || start <= ts;
28287
30509
  });
28288
30510
  }
28289
- function getAgentSafeQueue(agentId, filters = {}, db) {
30511
+ function getAgentSafeQueue(_agentId, filters = {}, db) {
28290
30512
  const d = db || getDatabase();
28291
30513
  const ts = Date.now();
28292
30514
  const ready = listReadyScheduledTasks({ project_id: filters.project_id }, d);
@@ -28507,7 +30729,7 @@ function searchTasks(options, projectId, taskListId, db) {
28507
30729
 
28508
30730
  // src/lib/saved-views.ts
28509
30731
  var SAVED_VIEWS_SCHEMA = "todos.saved_views.v1";
28510
- function slugify3(name) {
30732
+ function slugify4(name) {
28511
30733
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
28512
30734
  }
28513
30735
  function rowToView(row) {
@@ -28526,7 +30748,7 @@ function createSavedView(input, db) {
28526
30748
  const d = db || getDatabase();
28527
30749
  const id = uuid();
28528
30750
  const ts = now();
28529
- const slug = input.slug ?? slugify3(input.name);
30751
+ const slug = input.slug ?? slugify4(input.name);
28530
30752
  d.run(`INSERT INTO saved_views (id, name, slug, entity_type, filters, created_at, updated_at)
28531
30753
  VALUES (?, ?, ?, ?, ?, ?, ?)`, [id, input.name, slug, input.entity_type ?? "task", JSON.stringify(input.filters ?? {}), ts, ts]);
28532
30754
  return getSavedView(id, d);
@@ -28604,12 +30826,13 @@ function unifiedSearch(input = {}, db) {
28604
30826
  }
28605
30827
  if (types.includes("run") || types.includes("all")) {
28606
30828
  for (const r of listAgentRuns({ limit: 100 }, d)) {
28607
- if (query && !r.adapter.toLowerCase().includes(query.toLowerCase()) && !r.id.includes(query))
30829
+ const adapter = r.adapter ?? "custom";
30830
+ if (query && !adapter.toLowerCase().includes(query.toLowerCase()) && !r.id.includes(query))
28608
30831
  continue;
28609
30832
  hits.push({
28610
30833
  entity_type: "run",
28611
30834
  id: r.id,
28612
- title: `Run ${r.adapter}`,
30835
+ title: `Run ${adapter}`,
28613
30836
  snippet: r.status,
28614
30837
  score: 60,
28615
30838
  data: { status: r.status, task_id: r.task_id }
@@ -29106,6 +31329,15 @@ init_secret_redaction();
29106
31329
  var BUNDLE_SCHEMA = "todos.bundle.v1";
29107
31330
  var BUNDLE_TYPES = ["full_export", "tasks", "partial"];
29108
31331
  var MERGE_STRATEGIES = ["skip_existing", "remote_wins", "local_wins", "newest_wins"];
31332
+ function sqlValue(value) {
31333
+ if (value === undefined || value === null)
31334
+ return null;
31335
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
31336
+ return value;
31337
+ if (value instanceof Uint8Array)
31338
+ return value;
31339
+ return JSON.stringify(value);
31340
+ }
29109
31341
  function serializeProject(project) {
29110
31342
  return {
29111
31343
  schema_version: "todos.project.v1",
@@ -29202,10 +31434,11 @@ function exportLocalBundle(options = {}, db) {
29202
31434
  verification_records: verificationRecords,
29203
31435
  artifacts
29204
31436
  };
29205
- const { data: redacted, warnings: profileWarnings } = applyExportProfile(bundleData, {
31437
+ const { data, warnings: profileWarnings } = applyExportProfile(bundleData, {
29206
31438
  profile,
29207
31439
  acknowledge_plaintext: options.acknowledge_plaintext
29208
31440
  });
31441
+ const redacted = data;
29209
31442
  warnings.push(...profileWarnings);
29210
31443
  return {
29211
31444
  schema_version: BUNDLE_SCHEMA,
@@ -29350,7 +31583,7 @@ function upsertProject(raw, d) {
29350
31583
  raw.task_counter ?? 0,
29351
31584
  raw.created_at ?? ts,
29352
31585
  raw.updated_at ?? ts
29353
- ]);
31586
+ ].map(sqlValue));
29354
31587
  return "created";
29355
31588
  }
29356
31589
  d.run(`UPDATE projects SET name = ?, path = ?, description = ?, task_list_id = ?, task_prefix = ?, task_counter = ?, updated_at = ?
@@ -29363,7 +31596,7 @@ function upsertProject(raw, d) {
29363
31596
  raw.task_counter ?? existing.task_counter,
29364
31597
  raw.updated_at ?? ts,
29365
31598
  id
29366
- ]);
31599
+ ].map(sqlValue));
29367
31600
  return "updated";
29368
31601
  }
29369
31602
  function upsertTask(raw, d) {
@@ -29399,7 +31632,7 @@ function upsertTask(raw, d) {
29399
31632
  raw.version ?? existing.version,
29400
31633
  raw.updated_at ?? now(),
29401
31634
  id
29402
- ]);
31635
+ ].map(sqlValue));
29403
31636
  return "updated";
29404
31637
  }
29405
31638
  function upsertComment(raw, d) {
@@ -29417,7 +31650,7 @@ function upsertComment(raw, d) {
29417
31650
  raw.type ?? "comment",
29418
31651
  raw.progress_pct ?? null,
29419
31652
  raw.created_at ?? now()
29420
- ]);
31653
+ ].map(sqlValue));
29421
31654
  return "created";
29422
31655
  }
29423
31656
  function importBundle(bundle, options = {}, db) {
@@ -29512,7 +31745,7 @@ function readBundleFile(path) {
29512
31745
  function getBridgeDocs() {
29513
31746
  return `# Local Import/Export/Sync Bridge
29514
31747
 
29515
- OSS @hasna/todos produces stable \`${BUNDLE_SCHEMA}\` JSON bundles for platform-todos consumption.
31748
+ OSS @hasna/todos produces stable \`${BUNDLE_SCHEMA}\` JSON bundles for hosted wrapper consumption.
29516
31749
  No automatic cloud calls are made from the OSS package.
29517
31750
 
29518
31751
  ## Bundle types
@@ -29989,7 +32222,7 @@ function summarizeTask2(t) {
29989
32222
  title: t.title,
29990
32223
  status: t.status,
29991
32224
  priority: t.priority,
29992
- assigned_to: t.assigned_to
32225
+ assigned_to: t.assigned_to ?? null
29993
32226
  };
29994
32227
  }
29995
32228
  function buildHandoffPacket(input = {}, db) {
@@ -30891,8 +33124,6 @@ var BUILTIN_TEMPLATES = [
30891
33124
  },
30892
33125
  {
30893
33126
  name: "open-source-project",
30894
- version: 1,
30895
- category: "project",
30896
33127
  description: "Full open-source project bootstrap \u2014 scaffold to publish",
30897
33128
  category: "open-source",
30898
33129
  version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
@@ -30918,7 +33149,7 @@ var BUILTIN_TEMPLATES = [
30918
33149
  },
30919
33150
  {
30920
33151
  name: "release",
30921
- version: 1,
33152
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
30922
33153
  category: "ops",
30923
33154
  description: "Version release workflow \u2014 test, changelog, publish, verify",
30924
33155
  variables: [
@@ -30935,7 +33166,7 @@ var BUILTIN_TEMPLATES = [
30935
33166
  },
30936
33167
  {
30937
33168
  name: "docs-refresh",
30938
- version: 1,
33169
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
30939
33170
  category: "workflow",
30940
33171
  description: "Documentation refresh \u2014 audit, update, verify links",
30941
33172
  variables: [{ name: "scope", required: true, description: "Docs scope (README, API, AGENTS.md)" }],
@@ -30948,7 +33179,7 @@ var BUILTIN_TEMPLATES = [
30948
33179
  },
30949
33180
  {
30950
33181
  name: "migration",
30951
- version: 1,
33182
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
30952
33183
  category: "ops",
30953
33184
  description: "Schema/data migration workflow",
30954
33185
  variables: [
@@ -30965,7 +33196,7 @@ var BUILTIN_TEMPLATES = [
30965
33196
  },
30966
33197
  {
30967
33198
  name: "incident-response",
30968
- version: 1,
33199
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
30969
33200
  category: "ops",
30970
33201
  description: "Incident triage, mitigation, postmortem",
30971
33202
  variables: [{ name: "incident", required: true, description: "Incident summary" }],
@@ -31036,9 +33267,9 @@ function exportBuiltinTemplateFiles() {
31036
33267
  function writeBuiltinTemplateFiles(directory) {
31037
33268
  mkdirSync15(directory, { recursive: true });
31038
33269
  const files = [];
31039
- for (const entry of exportBuiltinTemplateFiles()) {
31040
- const path = join15(directory, entry.filename);
31041
- writeFileSync13(path, `${JSON.stringify(entry.template, null, 2)}
33270
+ for (const entry2 of exportBuiltinTemplateFiles()) {
33271
+ const path = join15(directory, entry2.filename);
33272
+ writeFileSync13(path, `${JSON.stringify(entry2.template, null, 2)}
31042
33273
  `, "utf-8");
31043
33274
  files.push(path);
31044
33275
  }
@@ -31160,8 +33391,14 @@ function getTemplateLibraryDocs() {
31160
33391
  Marketplace-free bundled workflows \u2014 no network required.
31161
33392
 
31162
33393
  ## Categories
31163
- - **workflow** \u2014 bug-fix, feature, docs-refresh
31164
- - **ops** \u2014 release, migration, incident-response, security-audit
33394
+ - **bug-fix** \u2014 defect reproduction, diagnosis, test, implementation, release
33395
+ - **feature** \u2014 feature-implementation
33396
+ - **security** \u2014 security-review
33397
+ - **release** \u2014 release verification and publishing
33398
+ - **migration** \u2014 schema and data migration
33399
+ - **incident** \u2014 incident and incident-response
33400
+ - **docs** \u2014 docs-refresh
33401
+ - **qa** \u2014 QA matrix, checks, defects, signoff
31165
33402
  - **project** \u2014 open-source-project bootstrap
31166
33403
 
31167
33404
  ## CLI
@@ -31264,8 +33501,8 @@ function buildMachineTopologyReport(db) {
31264
33501
  schema_version: MACHINE_TOPOLOGY_SCHEMA,
31265
33502
  id: m.id,
31266
33503
  name: m.name,
31267
- hostname: m.hostname,
31268
- platform: m.platform,
33504
+ hostname: m.hostname ?? m.name,
33505
+ platform: m.platform ?? "unknown",
31269
33506
  last_seen_at: m.last_seen_at,
31270
33507
  is_local: m.id === localId,
31271
33508
  active_agents: agents.filter((a) => !a.stale && a.machine_id === m.id).length,
@@ -31313,9 +33550,9 @@ todos machines topology # full diagnostic report
31313
33550
  `;
31314
33551
  }
31315
33552
  // src/lib/environment-snapshots.ts
31316
- import { createHash as createHash10 } from "crypto";
33553
+ import { createHash as createHash11 } from "crypto";
31317
33554
  import { existsSync as existsSync19, readFileSync as readFileSync19, statSync as statSync7 } from "fs";
31318
- import { hostname as hostname3, platform as platform2, arch } from "os";
33555
+ import { hostname as hostname3, platform, arch } from "os";
31319
33556
  import { dirname as dirname15, join as join16, resolve as resolve14 } from "path";
31320
33557
  import { tmpdir as tmpdir2 } from "os";
31321
33558
  init_database();
@@ -31338,7 +33575,7 @@ var CONFIG_FILES = [
31338
33575
  "dashboard/vite.config.ts"
31339
33576
  ];
31340
33577
  function sha2565(value) {
31341
- return createHash10("sha256").update(value).digest("hex");
33578
+ return createHash11("sha256").update(value).digest("hex");
31342
33579
  }
31343
33580
  function fileRecord(root, relativePath) {
31344
33581
  const path = join16(root, relativePath);
@@ -31473,7 +33710,7 @@ function captureEnvironmentSnapshot(input = {}) {
31473
33710
  schema_version: 1,
31474
33711
  captured_at: input.now ? new Date(input.now).toISOString() : new Date().toISOString(),
31475
33712
  root,
31476
- machine: { hostname: hostname3(), platform: platform2(), arch: arch() },
33713
+ machine: { hostname: hostname3(), platform: platform(), arch: arch() },
31477
33714
  target: {
31478
33715
  task_id: input.task_id ?? null,
31479
33716
  run_id: input.run_id ?? null,
@@ -31562,7 +33799,7 @@ function diffFiles(left, right) {
31562
33799
  const leftMap = keyed(left);
31563
33800
  const rightMap = keyed(right);
31564
33801
  const paths = [...new Set([...leftMap.keys(), ...rightMap.keys()])].sort((a, b) => a.localeCompare(b));
31565
- return paths.map((path) => ({ path, left_sha256: leftMap.get(path)?.sha256 ?? null, right_sha256: rightMap.get(path)?.sha256 ?? null })).filter((entry) => entry.left_sha256 !== entry.right_sha256);
33802
+ return paths.map((path) => ({ path, left_sha256: leftMap.get(path)?.sha256 ?? null, right_sha256: rightMap.get(path)?.sha256 ?? null })).filter((entry2) => entry2.left_sha256 !== entry2.right_sha256);
31566
33803
  }
31567
33804
  function compareEnvironmentSnapshots(left, right) {
31568
33805
  const warnings = [];
@@ -31588,7 +33825,7 @@ function compareEnvironmentSnapshotFiles(leftPath, rightPath) {
31588
33825
  }
31589
33826
  // src/lib/decision-records.ts
31590
33827
  init_database();
31591
- import { createHash as createHash11 } from "crypto";
33828
+ import { createHash as createHash12 } from "crypto";
31592
33829
  import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync15 } from "fs";
31593
33830
  import { dirname as dirname16, join as join17 } from "path";
31594
33831
  var DECISION_RECORD_SCHEMA = "todos.decision_record.v1";
@@ -31644,7 +33881,7 @@ function rowToDecisionRecord(row) {
31644
33881
  }
31645
33882
  function stableSnapshotHash(payload) {
31646
33883
  const { captured_at: _capturedAt, ...rest } = payload;
31647
- return createHash11("sha256").update(JSON.stringify(rest)).digest("hex");
33884
+ return createHash12("sha256").update(JSON.stringify(rest)).digest("hex");
31648
33885
  }
31649
33886
  function createDecisionRecord(input, db) {
31650
33887
  const d = db || getDatabase();
@@ -32024,7 +34261,7 @@ function buildReportExportData(input, db) {
32024
34261
  let taskId = input.task_id ?? null;
32025
34262
  switch (input.kind) {
32026
34263
  case "project": {
32027
- const project = projectId ? getProject(projectId, d) : listProjects(undefined, d)[0];
34264
+ const project = projectId ? getProject(projectId, d) : listProjects(d)[0];
32028
34265
  if (!project)
32029
34266
  throw new Error("Project not found");
32030
34267
  projectId = project.id;
@@ -32112,7 +34349,7 @@ function buildReportExportData(input, db) {
32112
34349
  break;
32113
34350
  }
32114
34351
  case "roadmap": {
32115
- const project = projectId ? getProject(projectId, d) : listProjects(undefined, d)[0];
34352
+ const project = projectId ? getProject(projectId, d) : listProjects(d)[0];
32116
34353
  if (!project)
32117
34354
  throw new Error("Project not found");
32118
34355
  projectId = project.id;
@@ -32747,7 +34984,7 @@ function applyFailureTriage(input, db) {
32747
34984
  }
32748
34985
  if (action === "escalate") {
32749
34986
  const meta = { ...task2.metadata, _triage_escalated: { at: now(), reason, classification } };
32750
- const escalated = updateTask(task2.id, { priority: "urgent", metadata: meta, version: task2.version }, d);
34987
+ const escalated = updateTask(task2.id, { priority: "critical", metadata: meta, version: task2.version }, d);
32751
34988
  return { schema_version: FAILURE_TRIAGE_SCHEMA, action, classification, playbook, task: escalated, escalated: true };
32752
34989
  }
32753
34990
  }
@@ -32971,7 +35208,7 @@ function storePath(cwd) {
32971
35208
  function versionsDir(cwd) {
32972
35209
  return join19(storeDir(cwd), "versions");
32973
35210
  }
32974
- function slugify4(name) {
35211
+ function slugify5(name) {
32975
35212
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
32976
35213
  }
32977
35214
  function emptyStore2() {
@@ -33010,7 +35247,7 @@ function createUserScaffold(input, db, cwd) {
33010
35247
  const store = loadUserScaffoldStore(cwd);
33011
35248
  const id = uuid();
33012
35249
  const ts = now();
33013
- const slug = slugify4(input.name);
35250
+ const slug = slugify5(input.name);
33014
35251
  if (Object.values(store.scaffolds).some((s) => s.slug === slug)) {
33015
35252
  throw new Error(`Scaffold slug already exists: ${slug}`);
33016
35253
  }
@@ -33047,8 +35284,8 @@ function updateUserScaffold(idOrSlug, updates, db, cwd) {
33047
35284
  if (!existing)
33048
35285
  throw new Error(`Scaffold not found: ${idOrSlug}`);
33049
35286
  snapshotVersion(existing, cwd);
33050
- if (updates.name && slugify4(updates.name) !== existing.slug) {
33051
- const newSlug = slugify4(updates.name);
35287
+ if (updates.name && slugify5(updates.name) !== existing.slug) {
35288
+ const newSlug = slugify5(updates.name);
33052
35289
  if (Object.values(store.scaffolds).some((s) => s.id !== existing.id && s.slug === newSlug)) {
33053
35290
  throw new Error(`Scaffold slug conflict: ${newSlug}`);
33054
35291
  }
@@ -33244,7 +35481,7 @@ import { mkdtempSync } from "fs";
33244
35481
  import { join as join20 } from "path";
33245
35482
  import { tmpdir as tmpdir3 } from "os";
33246
35483
  var AGENT_WORKFLOW_DEMO_SCHEMA = "todos.agent_workflow_demo.v1";
33247
- var DEMO_DEFAULT_AGENT = "demo-agent";
35484
+ var DEMO_DEFAULT_AGENT = "demoagent";
33248
35485
  var DEMO_DEFAULT_PROJECT = "Agent Workflow Demo";
33249
35486
  var DEMO_PROJECT_PATH = "/tmp/todos-agent-workflow-demo";
33250
35487
  function pushStep(steps, action, detail, status = "ok") {
@@ -33350,7 +35587,7 @@ function runAgentWorkflowDemo(options = {}) {
33350
35587
  agent_id: agent.id,
33351
35588
  evidence: { demo: true, source: "agent_workflow_demo" }
33352
35589
  }, d);
33353
- pushStep(steps, "enqueue_agent_run", `Queued agent run on task ${third.short_id ?? third.id.slice(0, 8)}`);
35590
+ pushStep(steps, "enqueue_agent_run", `Queued agent run ${queuedRun.id.slice(0, 8)} on task ${third.short_id ?? third.id.slice(0, 8)}`);
33354
35591
  const claimedRun = claimNextAgentRun(agent.id, { adapter: demoAdapter }, d);
33355
35592
  if (!claimedRun)
33356
35593
  throw new Error("Expected queued agent run");
@@ -33399,7 +35636,7 @@ function runAgentWorkflowDemo(options = {}) {
33399
35636
  id: completedRun.id,
33400
35637
  status: completedRun.status,
33401
35638
  task_id: completedRun.task_id,
33402
- adapter: completedRun.adapter
35639
+ adapter: completedRun.adapter ?? demoAdapter
33403
35640
  },
33404
35641
  run_record: {
33405
35642
  id: finishedRecord.id,
@@ -33877,13 +36114,13 @@ var ALL_MCP_TOOLS = [
33877
36114
  "sync_todos_md",
33878
36115
  "task_context",
33879
36116
  "template_history",
33880
- "todos_cloud_conflicts",
33881
- "todos_cloud_feedback",
33882
- "todos_cloud_pull",
33883
- "todos_cloud_push",
33884
- "todos_cloud_status",
33885
36117
  "todos_inbox",
33886
36118
  "todos_retro",
36119
+ "todos_storage_conflicts",
36120
+ "todos_storage_feedback",
36121
+ "todos_storage_pull",
36122
+ "todos_storage_push",
36123
+ "todos_storage_status",
33887
36124
  "trust_workspace",
33888
36125
  "unarchive_agent",
33889
36126
  "unarchive_task",
@@ -34021,10 +36258,10 @@ var MCP_GROUP_DEFS = [
34021
36258
  match: (t) => /sandbox|trust|secret|crypto|verification|policy_pack|redact|scan_/.test(t)
34022
36259
  },
34023
36260
  {
34024
- id: "cloud",
34025
- name: "Cloud bridge",
34026
- description: "Optional cloud sync tools (admin profile).",
34027
- match: (t) => t.startsWith("todos_cloud_") || t === "sync_all" || t === "migrate_pg"
36261
+ id: "storage",
36262
+ name: "Storage bridge",
36263
+ description: "Optional storage sync tools (admin profile).",
36264
+ match: (t) => t.startsWith("todos_storage_") || t === "sync_all" || t === "migrate_pg"
34028
36265
  },
34029
36266
  {
34030
36267
  id: "workflow",
@@ -34249,7 +36486,7 @@ function normalizeFeatureManifest(manifest, generatedAt = "2026-01-01T00:00:00.0
34249
36486
  mcp: {
34250
36487
  total_tools: manifest.mcp.total_tools,
34251
36488
  tools_for_profile_count: manifest.mcp.tools_for_profile_count,
34252
- tool_group_counts: Object.fromEntries(manifest.mcp.tool_groups.map((g) => [g.id, g.tools.length]).sort(([a], [b]) => a.localeCompare(b)))
36489
+ tool_group_counts: Object.fromEntries(manifest.mcp.tool_groups.map((g) => [g.id, g.tools.length]).sort(([a], [b]) => String(a).localeCompare(String(b))))
34253
36490
  },
34254
36491
  profiles: manifest.profiles.map((p) => p.name),
34255
36492
  env_vars: manifest.env_vars.map((e) => e.name),
@@ -34321,7 +36558,7 @@ Set \`TODOS_PROFILE\` to control which MCP tools load:
34321
36558
  | agent_safe | Claim/complete workflow without destructive ops |
34322
36559
  | standard | Default minus admin/template/webhook tools |
34323
36560
  | full | All local tools |
34324
- | admin | Includes migrations and cloud bridge |
36561
+ | admin | Includes migrations and storage bridge |
34325
36562
 
34326
36563
  Schema: \`${FEATURE_MANIFEST_SCHEMA}\`
34327
36564
  `;
@@ -34843,8 +37080,8 @@ function getLeaderboard(opts, db) {
34843
37080
  }
34844
37081
  entries.sort((a, b) => b.composite_score - a.composite_score);
34845
37082
  const limit = opts?.limit || 20;
34846
- return entries.slice(0, limit).map((entry, idx) => ({
34847
- ...entry,
37083
+ return entries.slice(0, limit).map((entry2, idx) => ({
37084
+ ...entry2,
34848
37085
  rank: idx + 1
34849
37086
  }));
34850
37087
  }
@@ -36115,7 +38352,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
36115
38352
  }
36116
38353
  // src/lib/extract.ts
36117
38354
  import { existsSync as existsSync24, readFileSync as readFileSync23, statSync as statSync8 } from "fs";
36118
- import { createHash as createHash12 } from "crypto";
38355
+ import { createHash as createHash13 } from "crypto";
36119
38356
  import { relative as relative6, resolve as resolve15, join as join23 } from "path";
36120
38357
  var EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
36121
38358
  var DEFAULT_EXTENSIONS = new Set([
@@ -36180,7 +38417,7 @@ var SKIP_DIRS2 = new Set([
36180
38417
  ".parcel-cache"
36181
38418
  ]);
36182
38419
  function stableHash(value) {
36183
- return createHash12("sha256").update(value).digest("hex");
38420
+ return createHash13("sha256").update(value).digest("hex");
36184
38421
  }
36185
38422
  function normalizePathForMatch(value) {
36186
38423
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
@@ -36305,19 +38542,19 @@ function collectFiles(basePath, extensions, excludes, respectGitignore) {
36305
38542
  const files = [];
36306
38543
  const ignorePatterns = respectGitignore ? readGitignorePatterns(basePath) : [];
36307
38544
  const allExcludes = [...ignorePatterns, ...excludes];
36308
- for (const entry of glob.scanSync({ cwd: basePath, onlyFiles: true, dot: false })) {
36309
- const parts = entry.split("/");
38545
+ for (const entry2 of glob.scanSync({ cwd: basePath, onlyFiles: true, dot: false })) {
38546
+ const parts = entry2.split("/");
36310
38547
  if (parts.some((p) => SKIP_DIRS2.has(p)))
36311
38548
  continue;
36312
- if (matchesAnyPattern(entry, allExcludes))
38549
+ if (matchesAnyPattern(entry2, allExcludes))
36313
38550
  continue;
36314
- const dotIdx = entry.lastIndexOf(".");
38551
+ const dotIdx = entry2.lastIndexOf(".");
36315
38552
  if (dotIdx === -1)
36316
38553
  continue;
36317
- const ext = entry.slice(dotIdx);
38554
+ const ext = entry2.slice(dotIdx);
36318
38555
  if (!extensions.has(ext))
36319
38556
  continue;
36320
- files.push(entry);
38557
+ files.push(entry2);
36321
38558
  }
36322
38559
  return files.sort();
36323
38560
  }
@@ -37068,7 +39305,7 @@ function renderWorkflowStatesMarkdown(states = listWorkflowStates()) {
37068
39305
  }
37069
39306
  // src/lib/agent-replay-simulator.ts
37070
39307
  init_redaction();
37071
- import { createHash as createHash13 } from "crypto";
39308
+ import { createHash as createHash14 } from "crypto";
37072
39309
  import { readFileSync as readFileSync24 } from "fs";
37073
39310
  function isObject(value) {
37074
39311
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -37090,7 +39327,7 @@ function stable2(value) {
37090
39327
  return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable2(value[key])]));
37091
39328
  }
37092
39329
  function fingerprint3(value) {
37093
- return createHash13("sha256").update(JSON.stringify(stable2(value))).digest("hex");
39330
+ return createHash14("sha256").update(JSON.stringify(stable2(value))).digest("hex");
37094
39331
  }
37095
39332
  function unpackFixture(input) {
37096
39333
  if (!isObject(input))
@@ -37329,7 +39566,7 @@ function renderAgentReplaySimulationMarkdown(simulation) {
37329
39566
  }
37330
39567
  // src/lib/local-extensions.ts
37331
39568
  init_config();
37332
- import { createHash as createHash14, createVerify } from "crypto";
39569
+ import { createHash as createHash15, createVerify } from "crypto";
37333
39570
  import { existsSync as existsSync25, readdirSync as readdirSync5, readFileSync as readFileSync25, statSync as statSync9 } from "fs";
37334
39571
  import { basename as basename5, join as join24, resolve as resolve16 } from "path";
37335
39572
  init_redaction();
@@ -37416,7 +39653,7 @@ function parseJson(path) {
37416
39653
  return JSON.parse(readFileSync25(path, "utf8"));
37417
39654
  }
37418
39655
  function sha2566(bytes) {
37419
- return `sha256:${createHash14("sha256").update(bytes).digest("hex")}`;
39656
+ return `sha256:${createHash15("sha256").update(bytes).digest("hex")}`;
37420
39657
  }
37421
39658
  function compareVersions(a, b) {
37422
39659
  const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -37716,11 +39953,11 @@ function projectExtensionSources(projectPath) {
37716
39953
  ];
37717
39954
  const extensionDir = join24(root, ".todos", "extensions");
37718
39955
  if (existsSync25(extensionDir)) {
37719
- for (const entry of readdirSync5(extensionDir)) {
37720
- if (entry.startsWith("."))
39956
+ for (const entry2 of readdirSync5(extensionDir)) {
39957
+ if (entry2.startsWith("."))
37721
39958
  continue;
37722
- const full = join24(extensionDir, entry);
37723
- if (statSync9(full).isDirectory() || entry.endsWith(".json"))
39959
+ const full = join24(extensionDir, entry2);
39960
+ if (statSync9(full).isDirectory() || entry2.endsWith(".json"))
37724
39961
  candidates.push(full);
37725
39962
  }
37726
39963
  }
@@ -38297,16 +40534,16 @@ function getExpiredArtifactFileCandidates(input, generatedAt, db, deletedRunIds)
38297
40534
  }
38298
40535
  const candidates = [];
38299
40536
  for (const [relativePath, entries] of byPath) {
38300
- const retainedEntries = entries.filter((entry) => !deletedRunIds.has(entry.row.run_id) && (!entry.expires_at || entry.expires_at > generatedAt));
40537
+ const retainedEntries = entries.filter((entry2) => !deletedRunIds.has(entry2.row.run_id) && (!entry2.expires_at || entry2.expires_at > generatedAt));
38301
40538
  if (retainedEntries.length > 0)
38302
40539
  continue;
38303
- const removableEntries = entries.filter((entry) => deletedRunIds.has(entry.row.run_id) || Boolean(entry.expires_at && entry.expires_at <= generatedAt));
40540
+ const removableEntries = entries.filter((entry2) => deletedRunIds.has(entry2.row.run_id) || Boolean(entry2.expires_at && entry2.expires_at <= generatedAt));
38304
40541
  if (removableEntries.length === 0)
38305
40542
  continue;
38306
- const latestExpiry = removableEntries.map((entry) => entry.expires_at).filter((expiresAt) => typeof expiresAt === "string").sort().at(-1) ?? generatedAt;
40543
+ const latestExpiry = removableEntries.map((entry2) => entry2.expires_at).filter((expiresAt) => typeof expiresAt === "string").sort().at(-1) ?? generatedAt;
38307
40544
  candidates.push({
38308
40545
  relative_path: relativePath,
38309
- artifact_ids: removableEntries.map((entry) => entry.row.id).sort(),
40546
+ artifact_ids: removableEntries.map((entry2) => entry2.row.id).sort(),
38310
40547
  expires_at: latestExpiry,
38311
40548
  size_bytes: typeof removableEntries[0].store.size_bytes === "number" ? removableEntries[0].store.size_bytes : null
38312
40549
  });
@@ -39114,7 +41351,7 @@ function removeCapacityProfile(idOrAgent, projectId) {
39114
41351
  function getPlanningForecast(input = {}, db) {
39115
41352
  const startDate = input.start_date ? dateOnly(input.start_date) : todayIso();
39116
41353
  const tasks = listTasks({ project_id: input.project_id, plan_id: input.plan_id, limit: 1e4 }, db).filter((task2) => taskMatchesAgent(task2, input.agent_id));
39117
- const reportByTask = new Map(getTimeReport({ project_id: input.project_id, plan_id: input.plan_id, agent_id: input.agent_id, include_open: true }, db).map((entry) => [entry.task_id, entry]));
41354
+ const reportByTask = new Map(getTimeReport({ project_id: input.project_id, plan_id: input.plan_id, agent_id: input.agent_id, include_open: true }, db).map((entry2) => [entry2.task_id, entry2]));
39118
41355
  const profiles = listCapacityProfiles({ agent_id: input.agent_id, project_id: input.project_id });
39119
41356
  const globalProfiles = input.project_id ? listCapacityProfiles({ agent_id: input.agent_id }).filter((profile) => profile.project_id === null) : [];
39120
41357
  const capacityProfiles = profiles.length > 0 ? profiles : globalProfiles;
@@ -39142,7 +41379,7 @@ function getPlanningForecast(input = {}, db) {
39142
41379
  const estimatedMinutes = forecastTasks.reduce((sum, task2) => sum + (task2.estimated_minutes ?? 0), 0);
39143
41380
  const remainingEstimatedMinutes = forecastTasks.reduce((sum, task2) => sum + task2.remaining_estimated_minutes, 0);
39144
41381
  const actualMinutes = forecastTasks.reduce((sum, task2) => sum + (task2.actual_minutes ?? 0), 0);
39145
- const loggedMinutes = [...reportByTask.values()].reduce((sum, entry) => sum + entry.logged_minutes, 0);
41382
+ const loggedMinutes = [...reportByTask.values()].reduce((sum, entry2) => sum + entry2.logged_minutes, 0);
39146
41383
  const missingEstimateCount = forecastTasks.filter((task2) => task2.missing_estimate).length;
39147
41384
  const overdueOpenTaskCount = forecastTasks.filter((task2) => task2.overdue).length;
39148
41385
  const forecastWorkDays = capacityMinutesPerDay > 0 ? Math.ceil(remainingEstimatedMinutes / capacityMinutesPerDay) : null;
@@ -39351,7 +41588,7 @@ function fishEscape(value) {
39351
41588
  return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"");
39352
41589
  }
39353
41590
  function rootCommandNames(entries) {
39354
- return entries.filter((entry) => entry.path.length === 1).map((entry) => entry.path[0]).filter((value) => Boolean(value));
41591
+ return entries.filter((entry2) => entry2.path.length === 1).map((entry2) => entry2.path[0]).filter((value) => Boolean(value));
39355
41592
  }
39356
41593
  function optionFlags(options) {
39357
41594
  const flags = new Set;
@@ -39363,17 +41600,17 @@ function optionFlags(options) {
39363
41600
  }
39364
41601
  return [...flags].sort();
39365
41602
  }
39366
- function commandOptions(program, entry) {
39367
- return optionFlags([...program.options.map(optionEntry), ...entry.options]);
41603
+ function commandOptions(program, entry2) {
41604
+ return optionFlags([...program.options.map(optionEntry), ...entry2.options]);
39368
41605
  }
39369
41606
  function generateCompletionScript(program, shell) {
39370
41607
  const entries = collectCliCommandEntries(program);
39371
41608
  const roots = rootCommandNames(entries);
39372
41609
  if (shell === "bash") {
39373
- const rootCases = entries.filter((entry) => entry.path.length === 1).map((entry) => {
39374
- const children = entries.filter((child) => child.path.length === 2 && child.path[0] === entry.path[0]).map((child) => child.path[1]).filter((value) => Boolean(value));
39375
- const options = commandOptions(program, entry);
39376
- return ` ${entry.path[0]})
41610
+ const rootCases = entries.filter((entry2) => entry2.path.length === 1).map((entry2) => {
41611
+ const children = entries.filter((child) => child.path.length === 2 && child.path[0] === entry2.path[0]).map((child) => child.path[1]).filter((value) => Boolean(value));
41612
+ const options = commandOptions(program, entry2);
41613
+ return ` ${entry2.path[0]})
39377
41614
  COMPREPLY=($(compgen -W '${shellWords([...children, ...options])}' -- "$cur"))
39378
41615
  return
39379
41616
  ;;`;
@@ -39400,14 +41637,14 @@ function generateCompletionScript(program, shell) {
39400
41637
  `);
39401
41638
  }
39402
41639
  if (shell === "zsh") {
39403
- const commandEntries = entries.filter((entry) => entry.path.length === 1).map((entry) => zshEntry(entry.path[0] || "", entry.description));
41640
+ const commandEntries = entries.filter((entry2) => entry2.path.length === 1).map((entry2) => zshEntry(entry2.path[0] || "", entry2.description));
39404
41641
  const optionEntries = optionFlags(program.options.map(optionEntry)).map((flag) => `'${flag}'`);
39405
- const childCases = entries.filter((entry) => entry.path.length === 1).map((entry) => {
39406
- const children = entries.filter((child) => child.path.length === 2 && child.path[0] === entry.path[0]);
41642
+ const childCases = entries.filter((entry2) => entry2.path.length === 1).map((entry2) => {
41643
+ const children = entries.filter((child) => child.path.length === 2 && child.path[0] === entry2.path[0]);
39407
41644
  if (children.length === 0)
39408
41645
  return "";
39409
41646
  const childEntries = children.map((child) => zshEntry(child.path[1] || "", child.description)).join(" ");
39410
- return ` ${entry.path[0]}) local -a subcommands; subcommands=(${childEntries}); _describe 'subcommand' subcommands ;;`;
41647
+ return ` ${entry2.path[0]}) local -a subcommands; subcommands=(${childEntries}); _describe 'subcommand' subcommands ;;`;
39411
41648
  }).filter(Boolean).join(`
39412
41649
  `);
39413
41650
  return [
@@ -39443,11 +41680,11 @@ function generateCompletionScript(program, shell) {
39443
41680
  parts.push(`-d "${fishEscape(option.description)}"`);
39444
41681
  fishLines.push(parts.join(" "));
39445
41682
  }
39446
- for (const entry of entries) {
39447
- if (entry.path.length === 1) {
39448
- fishLines.push(`complete -c todos -n "__fish_use_subcommand" -a "${entry.path[0]}" -d "${fishEscape(entry.description)}"`);
39449
- } else if (entry.path.length === 2) {
39450
- fishLines.push(`complete -c todos -n "__fish_seen_subcommand_from ${entry.path[0]}" -a "${entry.path[1]}" -d "${fishEscape(entry.description)}"`);
41683
+ for (const entry2 of entries) {
41684
+ if (entry2.path.length === 1) {
41685
+ fishLines.push(`complete -c todos -n "__fish_use_subcommand" -a "${entry2.path[0]}" -d "${fishEscape(entry2.description)}"`);
41686
+ } else if (entry2.path.length === 2) {
41687
+ fishLines.push(`complete -c todos -n "__fish_seen_subcommand_from ${entry2.path[0]}" -a "${entry2.path[1]}" -d "${fishEscape(entry2.description)}"`);
39451
41688
  }
39452
41689
  }
39453
41690
  fishLines.push("");
@@ -39499,6 +41736,7 @@ export {
39499
41736
  upsertEncryptionProfile,
39500
41737
  upsertCapacityProfile,
39501
41738
  upsertAgentRunAdapter,
41739
+ uploadRunArtifactsToS3,
39502
41740
  updateUserScaffold,
39503
41741
  updateTemplate,
39504
41742
  updateTaskList,
@@ -39554,6 +41792,7 @@ export {
39554
41792
  slugify,
39555
41793
  simulateAgentReplayFile,
39556
41794
  simulateAgentReplay,
41795
+ signAwsV4Request,
39557
41796
  shouldRegisterToolForProfile,
39558
41797
  shouldRegisterToolForProfile2 as shouldRegisterToolForAccessProfile,
39559
41798
  setupEphemeralDemoDb,
@@ -39691,6 +41930,7 @@ export {
39691
41930
  redactHandoffPayload,
39692
41931
  redactExportRecord,
39693
41932
  redactEvidenceText,
41933
+ redactDatabaseUrl,
39694
41934
  redactCommentContent,
39695
41935
  redactActivityRecord,
39696
41936
  recoverStaleLeases,
@@ -39717,11 +41957,15 @@ export {
39717
41957
  previewInstalledTemplate,
39718
41958
  previewInboxIntake,
39719
41959
  previewBuiltinTemplate,
41960
+ postgresTodosSyncSchemaSql,
39720
41961
  pollLocalSnapshots,
41962
+ planRunArtifactsS3Sync,
39721
41963
  pauseFocusSession,
39722
41964
  patrolTasks,
39723
41965
  parseTmuxTarget,
41966
+ parseStorageMode,
39724
41967
  parseRecurrenceRule,
41968
+ parseQuietHours,
39725
41969
  parseNaturalLanguageTask,
39726
41970
  parseIssueExport,
39727
41971
  parseGoalCommand,
@@ -39747,6 +41991,7 @@ export {
39747
41991
  mergeDuplicateTask,
39748
41992
  materializePlanSteps,
39749
41993
  matchCapabilities,
41994
+ looksSensitiveKey,
39750
41995
  logTrace,
39751
41996
  logTime,
39752
41997
  logTaskChange,
@@ -39756,6 +42001,8 @@ export {
39756
42001
  logActivity,
39757
42002
  lockTask,
39758
42003
  loadUserScaffoldStore,
42004
+ loadTodosStorageConfig,
42005
+ loadStorageConfig,
39759
42006
  loadSandboxProfiles,
39760
42007
  loadIssueExportFromFile,
39761
42008
  loadConfig,
@@ -39856,6 +42103,7 @@ export {
39856
42103
  linkRunArtifact,
39857
42104
  issueToTask,
39858
42105
  isValidRecurrenceRule,
42106
+ isTodosRemoteStorageEnabled,
39859
42107
  isSnapshotStale,
39860
42108
  isEncryptedValue,
39861
42109
  isEncryptedBridgeBundle,
@@ -39873,6 +42121,7 @@ export {
39873
42121
  importTemplate,
39874
42122
  importTaskBoardBundle,
39875
42123
  importStoredArtifactContent,
42124
+ importSqliteTodosStorageSnapshot,
39876
42125
  importRoadmapBundle,
39877
42126
  importOnboardingFixture,
39878
42127
  importLocalBridgeBundle,
@@ -39897,6 +42146,10 @@ export {
39897
42146
  getUnlockImpact,
39898
42147
  getTraceStats,
39899
42148
  getTopologyDocs,
42149
+ getTodosStorageMode,
42150
+ getTodosStorageEnvName,
42151
+ getTodosStorageDatabaseUrl,
42152
+ getTodosStorageDatabaseEnv,
39900
42153
  getTimeReport,
39901
42154
  getTimeLogs,
39902
42155
  getTerminalNotificationRule,
@@ -39933,6 +42186,9 @@ export {
39933
42186
  getTaskBoard,
39934
42187
  getTask,
39935
42188
  getStoredHandoffAsPacket,
42189
+ getStorageMode,
42190
+ getStorageDatabaseUrl,
42191
+ getStorageDatabaseEnv,
39936
42192
  getStatus,
39937
42193
  getStaleTasks,
39938
42194
  getStaleTaskReport,
@@ -39979,6 +42235,8 @@ export {
39979
42235
  getOnboardingFixture,
39980
42236
  getNextTask,
39981
42237
  getNextCycle,
42238
+ getNativeStorageSyncPlan,
42239
+ getNativeStorageStatus,
39982
42240
  getMcpToolNames,
39983
42241
  getMachineTopologyDiagnostics,
39984
42242
  getMachineLocalPath,
@@ -40128,6 +42386,7 @@ export {
40128
42386
  exportTaskFields,
40129
42387
  exportTaskBoardBundle,
40130
42388
  exportStoredArtifactContent,
42389
+ exportSqliteTodosStorageSnapshot,
40131
42390
  exportSchemasToDirectory,
40132
42391
  exportRunReplay,
40133
42392
  exportRoadmapBundle,
@@ -40154,6 +42413,7 @@ export {
40154
42413
  evaluateCondition,
40155
42414
  ensureTaskList,
40156
42415
  ensureProject,
42416
+ ensureEncryptionProfile,
40157
42417
  enqueueAgentRun,
40158
42418
  encryptionProfileStatus,
40159
42419
  encryptValue,
@@ -40161,6 +42421,7 @@ export {
40161
42421
  encryptSensitiveFields,
40162
42422
  emitLocalEventHooksQuiet,
40163
42423
  emitLocalEventHooks,
42424
+ downloadRunArtifactsFromS3,
40164
42425
  dispatchWebhook,
40165
42426
  dispatchToMultiple,
40166
42427
  dismissReminder,
@@ -40203,6 +42464,8 @@ export {
40203
42464
  createVerificationEvidence,
40204
42465
  createUserScaffold,
40205
42466
  createTuiDashboardSnapshot,
42467
+ createTodosStorageAdapter,
42468
+ createTodosS3ArtifactStore,
40206
42469
  createTodosRegistry,
40207
42470
  createTemplate,
40208
42471
  createTaskList,
@@ -40222,6 +42485,8 @@ export {
40222
42485
  createReminder,
40223
42486
  createReleaseCompatibilityReport,
40224
42487
  createProject,
42488
+ createPostgresTodosSyncStore,
42489
+ createPostgresTodosStorageAdapter,
40225
42490
  createPlanWithSteps,
40226
42491
  createPlan,
40227
42492
  createOrg,
@@ -40240,6 +42505,7 @@ export {
40240
42505
  createJsonContractsManifest,
40241
42506
  createInboxItem,
40242
42507
  createInboxIntake,
42508
+ createHybridTodosStorageAdapter,
40243
42509
  createHandoffPacket,
40244
42510
  createHandoff,
40245
42511
  createGoalWorkflow,
@@ -40306,6 +42572,8 @@ export {
40306
42572
  bulkCreateTasks,
40307
42573
  bulkAddTaskFiles,
40308
42574
  buildTaskBoardSnapshot,
42575
+ buildS3ObjectUrl,
42576
+ buildS3ObjectKey,
40309
42577
  buildRunReplayBundle,
40310
42578
  buildResourceSnapshot,
40311
42579
  buildReportExportData,
@@ -40325,6 +42593,7 @@ export {
40325
42593
  attachPlanToProject,
40326
42594
  assignLabelToTask,
40327
42595
  assertToolAllowed,
42596
+ assertTodosRemoteStorageConfig,
40328
42597
  assertNoSecrets,
40329
42598
  assertHeadlessOutboundUrl,
40330
42599
  assertExportProfileAllowed,
@@ -40366,6 +42635,9 @@ export {
40366
42635
  TaskNotFoundError,
40367
42636
  TaskListNotFoundError,
40368
42637
  TUI_DASHBOARD_VIEWS,
42638
+ TODOS_STORAGE_TABLES,
42639
+ TODOS_STORAGE_FALLBACK_ENV,
42640
+ TODOS_STORAGE_ENV,
40369
42641
  TODOS_SDK_INTEGRATION_FIXTURE_SCHEMA_VERSION,
40370
42642
  TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT,
40371
42643
  TODOS_REGISTRY,
@@ -40396,6 +42668,7 @@ export {
40396
42668
  TASK_STATUSES,
40397
42669
  TASK_SCHEDULING_SCHEMA,
40398
42670
  TASK_PRIORITIES,
42671
+ STORAGE_TABLES,
40399
42672
  SECRET_REDACTION_SCHEMA,
40400
42673
  SCHEMA_SEMVER,
40401
42674
  SCHEMA_ENTITIES,