@hasna/todos 0.15.35 → 0.15.46

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 (92) hide show
  1. package/dist/cli/cloud-router.d.ts +52 -18
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  7. package/dist/cli/commands/task-subtree-transfer-commands.d.ts +3 -0
  8. package/dist/cli/commands/task-subtree-transfer-commands.d.ts.map +1 -0
  9. package/dist/cli/index.js +3430 -827
  10. package/dist/cli/stage-a.d.ts.map +1 -1
  11. package/dist/contracts.js +66 -58
  12. package/dist/db/plan-comments.d.ts +14 -0
  13. package/dist/db/plan-comments.d.ts.map +1 -0
  14. package/dist/db/schema.d.ts.map +1 -1
  15. package/dist/db/task-crud.d.ts.map +1 -1
  16. package/dist/db/task-graph.d.ts.map +1 -1
  17. package/dist/db/task-lifecycle.d.ts.map +1 -1
  18. package/dist/db/task-lists.d.ts.map +1 -1
  19. package/dist/db/task-relations.d.ts.map +1 -1
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1897 -334
  23. package/dist/lib/comment-cursor.d.ts +19 -0
  24. package/dist/lib/comment-cursor.d.ts.map +1 -1
  25. package/dist/lib/import-export-bridge.d.ts.map +1 -1
  26. package/dist/lib/instant-compare.d.ts +35 -0
  27. package/dist/lib/instant-compare.d.ts.map +1 -0
  28. package/dist/mcp/index.js +2403 -501
  29. package/dist/mcp.js +8 -4
  30. package/dist/pr-groups/postgres.d.ts.map +1 -1
  31. package/dist/project-registration/postgres.d.ts.map +1 -1
  32. package/dist/project-registration.js +290 -81
  33. package/dist/registry.d.ts +1 -1
  34. package/dist/registry.d.ts.map +1 -1
  35. package/dist/registry.js +73 -58
  36. package/dist/release-provenance.json +5 -5
  37. package/dist/sdk/index.d.ts +1 -1
  38. package/dist/sdk/index.d.ts.map +1 -1
  39. package/dist/sdk/index.js +35 -0
  40. package/dist/sdk/v1.generated.d.ts +141 -0
  41. package/dist/sdk/v1.generated.d.ts.map +1 -1
  42. package/dist/server/cloud.d.ts +12 -2
  43. package/dist/server/cloud.d.ts.map +1 -1
  44. package/dist/server/index.js +5161 -8425
  45. package/dist/server/openapi.d.ts +2714 -1937
  46. package/dist/server/openapi.d.ts.map +1 -1
  47. package/dist/server/v1.d.ts +2 -1
  48. package/dist/server/v1.d.ts.map +1 -1
  49. package/dist/storage/config.d.ts +12 -30
  50. package/dist/storage/config.d.ts.map +1 -1
  51. package/dist/storage/index.d.ts +2 -2
  52. package/dist/storage/index.d.ts.map +1 -1
  53. package/dist/storage/interfaces.d.ts +13 -2
  54. package/dist/storage/interfaces.d.ts.map +1 -1
  55. package/dist/storage/local-sqlite.d.ts.map +1 -1
  56. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  57. package/dist/storage/timestamp-backfill.d.ts +62 -0
  58. package/dist/storage/timestamp-backfill.d.ts.map +1 -0
  59. package/dist/storage.d.ts +2 -2
  60. package/dist/storage.d.ts.map +1 -1
  61. package/dist/storage.js +268 -74
  62. package/dist/task-manifest/postgres.d.ts.map +1 -1
  63. package/dist/task-manifest.js +15 -6
  64. package/dist/task-subtree-transfer/authority.d.ts +27 -0
  65. package/dist/task-subtree-transfer/authority.d.ts.map +1 -0
  66. package/dist/task-subtree-transfer/backend.d.ts +9 -0
  67. package/dist/task-subtree-transfer/backend.d.ts.map +1 -0
  68. package/dist/task-subtree-transfer/canonical.d.ts +14 -0
  69. package/dist/task-subtree-transfer/canonical.d.ts.map +1 -0
  70. package/dist/task-subtree-transfer/http.d.ts +16 -0
  71. package/dist/task-subtree-transfer/http.d.ts.map +1 -0
  72. package/dist/task-subtree-transfer/index.d.ts +6 -0
  73. package/dist/task-subtree-transfer/index.d.ts.map +1 -0
  74. package/dist/task-subtree-transfer/planner.d.ts +24 -0
  75. package/dist/task-subtree-transfer/planner.d.ts.map +1 -0
  76. package/dist/task-subtree-transfer/postgres.d.ts +19 -0
  77. package/dist/task-subtree-transfer/postgres.d.ts.map +1 -0
  78. package/dist/task-subtree-transfer/schema-sql.d.ts +5 -0
  79. package/dist/task-subtree-transfer/schema-sql.d.ts.map +1 -0
  80. package/dist/task-subtree-transfer/sqlite.d.ts +16 -0
  81. package/dist/task-subtree-transfer/sqlite.d.ts.map +1 -0
  82. package/dist/task-subtree-transfer/types.d.ts +148 -0
  83. package/dist/task-subtree-transfer/types.d.ts.map +1 -0
  84. package/dist/task-subtree-transfer.d.ts +2 -0
  85. package/dist/task-subtree-transfer.d.ts.map +1 -0
  86. package/dist/task-subtree-transfer.js +1878 -0
  87. package/dist/testing.d.ts +41 -7
  88. package/dist/testing.d.ts.map +1 -1
  89. package/dist/testing.js +44 -1
  90. package/dist/types/index.d.ts +24 -0
  91. package/dist/types/index.d.ts.map +1 -1
  92. package/package.json +8 -4
@@ -3692,6 +3692,20 @@ function ensureSchema(db) {
3692
3692
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
3693
3693
  ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
3694
3694
  ensureColumn("task_comments", "progress_pct", "INTEGER");
3695
+ ensureTable("plan_comments", `
3696
+ CREATE TABLE plan_comments (
3697
+ id TEXT PRIMARY KEY,
3698
+ plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
3699
+ agent_id TEXT,
3700
+ session_id TEXT,
3701
+ content TEXT NOT NULL,
3702
+ type TEXT DEFAULT 'comment' CHECK(type IN ('comment', 'progress', 'note')),
3703
+ progress_pct INTEGER CHECK(progress_pct IS NULL OR (progress_pct >= 0 AND progress_pct <= 100)),
3704
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3705
+ machine_id TEXT,
3706
+ synced_at TEXT
3707
+ )`);
3708
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plan_comments_plan ON plan_comments(plan_id)");
3695
3709
  ensureColumn("projects", "machine_id", "TEXT");
3696
3710
  ensureColumn("projects", "synced_at", "TEXT");
3697
3711
  ensureColumn("tasks", "machine_id", "TEXT");
@@ -4673,6 +4687,7 @@ var init_machines = __esm(() => {
4673
4687
  "task_lists",
4674
4688
  "plans",
4675
4689
  "task_comments",
4690
+ "plan_comments",
4676
4691
  "sessions",
4677
4692
  "task_history",
4678
4693
  "webhooks",
@@ -4866,20 +4881,16 @@ var init_identity_mapping = __esm(() => {
4866
4881
  // src/storage/config.ts
4867
4882
  var exports_config = {};
4868
4883
  __export(exports_config, {
4869
- parseStorageMode: () => parseStorageMode,
4870
4884
  parseStorageBackend: () => parseStorageBackend,
4871
4885
  loadTodosStorageConfig: () => loadTodosStorageConfig,
4872
4886
  loadStorageConfig: () => loadStorageConfig,
4873
4887
  isTodosShadowEnabled: () => isTodosShadowEnabled,
4874
- isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
4875
4888
  isTodosPostgresBackend: () => isTodosPostgresBackend,
4876
4889
  getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
4877
- getTodosStorageMode: () => getTodosStorageMode,
4878
4890
  getTodosStorageEnvName: () => getTodosStorageEnvName,
4879
4891
  getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
4880
4892
  getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
4881
4893
  getTodosStorageBackend: () => getTodosStorageBackend,
4882
- getStorageMode: () => getStorageMode,
4883
4894
  getStorageDatabaseUrl: () => getStorageDatabaseUrl,
4884
4895
  getStorageDatabaseEnv: () => getStorageDatabaseEnv,
4885
4896
  getCanonicalTodosRdsConfig: () => getCanonicalTodosRdsConfig,
@@ -4889,7 +4900,6 @@ __export(exports_config, {
4889
4900
  TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
4890
4901
  TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
4891
4902
  STORAGE_TABLES: () => STORAGE_TABLES,
4892
- REMOVED_STORAGE_MODE_ENV_KEYS: () => REMOVED_STORAGE_MODE_ENV_KEYS,
4893
4903
  CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
4894
4904
  CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
4895
4905
  CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
@@ -4944,9 +4954,6 @@ function loadStorageConfig(env = process.env) {
4944
4954
  function isTodosPostgresBackend(config) {
4945
4955
  return config.mode === "postgres";
4946
4956
  }
4947
- function isTodosRemoteStorageEnabled(config) {
4948
- return isTodosPostgresBackend(config);
4949
- }
4950
4957
  function assertTodosRemoteStorageConfig(config) {
4951
4958
  if (!isTodosPostgresBackend(config))
4952
4959
  return;
@@ -4962,32 +4969,15 @@ function parseStorageBackend(value) {
4962
4969
  return "sqlite";
4963
4970
  if (normalized === "postgres" || normalized === "postgresql")
4964
4971
  return "postgres";
4965
- if (["local", "remote", "cloud", "hybrid", "self_hosted"].includes(normalized)) {
4966
- throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} selects the backend. Deployment modes no longer exist: ` + `delete the storage-mode variable and set ${TODOS_STORAGE_ENV.databaseUrl} to select the ` + `postgresql backend, or leave it unset for sqlite.`);
4967
- }
4968
4972
  throw new Error(`Storage backend must be sqlite or postgres`);
4969
4973
  }
4970
- function parseStorageMode(value) {
4971
- return parseStorageBackend(value);
4972
- }
4973
4974
  function getTodosStorageBackend(env = process.env) {
4974
- for (const key of REMOVED_STORAGE_MODE_ENV_KEYS) {
4975
- if (Object.hasOwn(env, key) && env[key] !== undefined) {
4976
- throw new Error(`${key} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `Set ${TODOS_STORAGE_ENV.databaseUrl} to select the postgresql backend, ` + `or leave it unset for sqlite.`);
4977
- }
4978
- }
4979
4975
  if (!getTodosStorageDatabaseUrl(env))
4980
4976
  return "sqlite";
4981
4977
  if (isTodosShadowEnabled(env))
4982
4978
  return "sqlite";
4983
4979
  return "postgres";
4984
4980
  }
4985
- function getTodosStorageMode(env = process.env) {
4986
- return getTodosStorageBackend(env);
4987
- }
4988
- function getStorageMode(env = process.env) {
4989
- return getTodosStorageBackend(env);
4990
- }
4991
4981
  function isTodosShadowEnabled(env = process.env) {
4992
4982
  return parseBoolean(readStorageEnv(env, "shadow").value, false);
4993
4983
  }
@@ -5054,7 +5044,7 @@ function parsePositiveInteger(value, fallback) {
5054
5044
  }
5055
5045
  return parsed;
5056
5046
  }
5057
- var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, REMOVED_STORAGE_MODE_ENV_KEYS, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos";
5047
+ var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos";
5058
5048
  var init_config2 = __esm(() => {
5059
5049
  TODOS_STORAGE_TABLES = [
5060
5050
  "todos_sync_records",
@@ -5093,12 +5083,6 @@ var init_config2 = __esm(() => {
5093
5083
  syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
5094
5084
  syncDryRun: "TODOS_SYNC_DRY_RUN"
5095
5085
  };
5096
- REMOVED_STORAGE_MODE_ENV_KEYS = [
5097
- "HASNA_TODOS_STORAGE_MODE",
5098
- "HASNA_TODOS_MODE",
5099
- "TODOS_STORAGE_MODE",
5100
- "TODOS_MODE"
5101
- ];
5102
5086
  });
5103
5087
 
5104
5088
  // src/storage/shadow-outbox-schema.ts
@@ -7270,22 +7254,31 @@ function updateTaskList2(id, input, db) {
7270
7254
  throw new TaskListNotFoundError(id);
7271
7255
  const sets = ["updated_at = ?"];
7272
7256
  const params = [now()];
7273
- if (input.slug !== undefined) {
7274
- const slug = slugify(input.slug);
7275
- if (!slug)
7276
- throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
7277
- const duplicate = existing.project_id ? d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ? AND id != ?").get(existing.project_id, slug, id) : d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ? AND id != ?").get(slug, id);
7257
+ const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? existing.project_id : input.project_id;
7258
+ const scopeChanged = projectId !== existing.project_id;
7259
+ const effectiveSlug = input.slug !== undefined ? slugify(input.slug) : existing.slug;
7260
+ if (input.slug !== undefined && !effectiveSlug) {
7261
+ throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
7262
+ }
7263
+ if (scopeChanged || input.slug !== undefined && effectiveSlug !== existing.slug) {
7264
+ const duplicate = projectId ? d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ? AND id != ?").get(projectId, effectiveSlug, id) : d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ? AND id != ?").get(effectiveSlug, id);
7278
7265
  if (duplicate) {
7279
- throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
7266
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
7280
7267
  }
7281
- if (slug !== existing.slug) {
7268
+ if (scopeChanged || effectiveSlug !== existing.slug) {
7282
7269
  releaseCanonicalSlugClaims("task_list", id, d);
7283
- if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(existing.project_id), slug, id, d)) {
7284
- throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
7270
+ if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(projectId), effectiveSlug, id, d)) {
7271
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
7285
7272
  }
7286
7273
  }
7274
+ }
7275
+ if (input.slug !== undefined) {
7287
7276
  sets.push("slug = ?");
7288
- params.push(slug);
7277
+ params.push(effectiveSlug);
7278
+ }
7279
+ if (projectId !== existing.project_id) {
7280
+ sets.push("project_id = ?");
7281
+ params.push(projectId);
7289
7282
  }
7290
7283
  if (input.name !== undefined) {
7291
7284
  sets.push("name = ?");
@@ -8794,7 +8787,7 @@ function getTaskGraph(taskId, direction = "both", db) {
8794
8787
  const deps = getTaskDependencies(t.id, d);
8795
8788
  const hasUnfinishedDeps = deps.some((dep) => {
8796
8789
  const depTask = getTask(dep.depends_on, d);
8797
- return depTask && depTask.status !== "completed";
8790
+ return depTask && isBlockingDependencyStatus(depTask.status);
8798
8791
  });
8799
8792
  return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
8800
8793
  }
@@ -8918,7 +8911,7 @@ function getBlockingDeps(id, db) {
8918
8911
  const blocking = [];
8919
8912
  for (const dep of deps) {
8920
8913
  const task = getTask(dep.depends_on, d);
8921
- if (task && task.status !== "completed")
8914
+ if (task && isBlockingDependencyStatus(task.status))
8922
8915
  blocking.push(task);
8923
8916
  }
8924
8917
  return blocking;
@@ -9242,7 +9235,7 @@ function getNextTask2(agentId, filters, db) {
9242
9235
  conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
9243
9236
  params.push(...filters.tags);
9244
9237
  }
9245
- conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')");
9238
+ conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status NOT IN ('completed', 'cancelled'))");
9246
9239
  const where = conditions.join(" AND ");
9247
9240
  let recentProjectIds = [];
9248
9241
  const assignedAliasParams = [];
@@ -9286,7 +9279,7 @@ function getActiveWork2(filters, db) {
9286
9279
  }
9287
9280
  function getTasksChangedSince(since, filters, db) {
9288
9281
  const d = db || getDatabase();
9289
- const conditions = ["updated_at > ?"];
9282
+ const conditions = ["(julianday(updated_at) IS NULL OR julianday(updated_at) > julianday(?))"];
9290
9283
  const params = [since];
9291
9284
  if (filters?.project_id) {
9292
9285
  conditions.push("project_id = ?");
@@ -9321,8 +9314,8 @@ function failTask2(id, agentId, reason, options, db) {
9321
9314
  const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
9322
9315
  const timestamp2 = now();
9323
9316
  const failTx = d.transaction(() => {
9324
- const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
9325
- WHERE id = ? AND version = ?`, [safeReason, JSON.stringify(safeMeta), timestamp2, id, task.version]);
9317
+ const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, completed_at = COALESCE(completed_at, ?), metadata = ?, version = version + 1, updated_at = ?
9318
+ WHERE id = ? AND version = ?`, [safeReason, timestamp2, JSON.stringify(safeMeta), timestamp2, id, task.version]);
9326
9319
  if (res.changes === 0) {
9327
9320
  const current = getTask(id, d);
9328
9321
  throw new VersionConflictError(id, task.version, current?.version ?? -1);
@@ -9334,6 +9327,7 @@ function failTask2(id, agentId, reason, options, db) {
9334
9327
  status: "failed",
9335
9328
  locked_by: null,
9336
9329
  locked_at: null,
9330
+ completed_at: task.completed_at ?? timestamp2,
9337
9331
  reason: safeReason,
9338
9332
  metadata: safeMeta,
9339
9333
  version: task.version + 1,
@@ -9982,13 +9976,20 @@ function updateTaskStored(id, input, db) {
9982
9976
  if (isTerminalStatus(input.status)) {
9983
9977
  sets.push("locked_by = NULL");
9984
9978
  sets.push("locked_at = NULL");
9985
- }
9986
- if (input.status === "completed") {
9987
- sets.push("completed_at = ?");
9988
- params.push(completionTimestamp);
9979
+ if (input.status === "completed") {
9980
+ sets.push("completed_at = ?");
9981
+ params.push(completionTimestamp);
9982
+ } else {
9983
+ sets.push("completed_at = COALESCE(completed_at, ?)");
9984
+ params.push(completionTimestamp);
9985
+ }
9989
9986
  } else if (task.status === "completed" && input.completed_at === undefined) {
9990
9987
  sets.push("completed_at = NULL");
9991
9988
  }
9989
+ if (input.status === "in_progress") {
9990
+ sets.push("started_at = COALESCE(started_at, ?)");
9991
+ params.push(timestamp2);
9992
+ }
9992
9993
  }
9993
9994
  if (input.priority !== undefined) {
9994
9995
  sets.push("priority = ?");
@@ -10126,7 +10127,6 @@ function updateTaskStored(id, input, db) {
10126
10127
  if (input.approved_by !== undefined)
10127
10128
  logTaskChange2(id, "approve", "approved_by", null, input.approved_by, agentId, d);
10128
10129
  const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
10129
- const completedNow = input.status === "completed";
10130
10130
  const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
10131
10131
  const updatedTask = {
10132
10132
  ...task,
@@ -10137,7 +10137,8 @@ function updateTaskStored(id, input, db) {
10137
10137
  updated_at: timestamp2,
10138
10138
  locked_by: terminalNow ? null : task.locked_by,
10139
10139
  locked_at: terminalNow ? null : task.locked_at,
10140
- completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
10140
+ completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : terminalNow ? task.completed_at ?? completionTimestamp : reopened ? null : task.completed_at,
10141
+ started_at: input.status === "in_progress" ? task.started_at ?? timestamp2 : task.started_at,
10141
10142
  sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
10142
10143
  actual_minutes: input.actual_minutes ?? task.actual_minutes,
10143
10144
  confidence: input.confidence !== undefined ? input.confidence : task.confidence,
@@ -10198,6 +10199,9 @@ function deleteTask(id, db) {
10198
10199
  version: row.version
10199
10200
  }, d);
10200
10201
  const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
10202
+ if (result.changes > 0) {
10203
+ d.run("DELETE FROM task_dependencies WHERE task_id = ? OR depends_on = ?", [id, id]);
10204
+ }
10201
10205
  return result.changes > 0;
10202
10206
  }
10203
10207
  var init_task_crud = __esm(() => {
@@ -12808,7 +12812,7 @@ import { createHash as createHash7 } from "crypto";
12808
12812
  // package.json
12809
12813
  var package_default = {
12810
12814
  name: "@hasna/todos",
12811
- version: "0.15.35",
12815
+ version: "0.15.46",
12812
12816
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12813
12817
  type: "module",
12814
12818
  main: "dist/index.js",
@@ -12854,6 +12858,10 @@ var package_default = {
12854
12858
  "./task-manifest": {
12855
12859
  types: "./dist/task-manifest.d.ts",
12856
12860
  import: "./dist/task-manifest.js"
12861
+ },
12862
+ "./task-subtree-transfer": {
12863
+ types: "./dist/task-subtree-transfer.d.ts",
12864
+ import: "./dist/task-subtree-transfer.js"
12857
12865
  }
12858
12866
  },
12859
12867
  workspaces: [
@@ -12867,8 +12875,8 @@ var package_default = {
12867
12875
  "README.md"
12868
12876
  ],
12869
12877
  scripts: {
12870
- build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
12871
- "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
12878
+ build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
12879
+ "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
12872
12880
  migrate: "bun run src/server/index.ts migrate",
12873
12881
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
12874
12882
  "generate:sdk": "bun run scripts/generate-sdk.ts",
@@ -12922,7 +12930,7 @@ var package_default = {
12922
12930
  author: "Andrei Hasna <andrei@hasna.com>",
12923
12931
  license: "Apache-2.0",
12924
12932
  dependencies: {
12925
- "@hasna/contracts": "0.5.2",
12933
+ "@hasna/contracts": "0.13.4",
12926
12934
  "@hasna/events": "^0.1.11",
12927
12935
  "@modelcontextprotocol/sdk": "^1.12.1",
12928
12936
  chalk: "^5.4.1",
@@ -12960,6 +12968,57 @@ function normalizeAgentNameInput(name) {
12960
12968
  // src/storage/postgres-adapter.ts
12961
12969
  init_creator_identity();
12962
12970
 
12971
+ // src/lib/instant-compare.ts
12972
+ var SQLITE_STAMP = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(?:Z|([+-])(\d{2}):(\d{2}))?)?$/;
12973
+ var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
12974
+ function isLeapYear(year) {
12975
+ return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
12976
+ }
12977
+ function sqliteJulianDay(value) {
12978
+ const m = SQLITE_STAMP.exec(value);
12979
+ if (!m)
12980
+ return null;
12981
+ const year = Number(m[1]);
12982
+ const month = Number(m[2]);
12983
+ const day = Number(m[3]);
12984
+ const hour = m[4] === undefined ? 0 : Number(m[4]);
12985
+ const minute = m[5] === undefined ? 0 : Number(m[5]);
12986
+ const second = m[6] === undefined ? 0 : Number(m[6]);
12987
+ const frac = m[7];
12988
+ const sign = m[8];
12989
+ const offsetHour = m[9];
12990
+ const offsetMinute = m[10];
12991
+ if (month < 1 || month > 12)
12992
+ return null;
12993
+ if (hour > 23 || minute > 59 || second > 59)
12994
+ return null;
12995
+ const maxDay = (DAYS_IN_MONTH[month - 1] ?? 0) + (month === 2 && isLeapYear(year) ? 1 : 0);
12996
+ if (day < 1 || day > maxDay)
12997
+ return null;
12998
+ let offsetMinutes = 0;
12999
+ if (sign !== undefined) {
13000
+ const oh = Number(offsetHour ?? "0");
13001
+ const om = Number(offsetMinute ?? "0");
13002
+ if (oh > 23 || om > 59)
13003
+ return null;
13004
+ offsetMinutes = oh * 60 + om;
13005
+ if (sign === "-")
13006
+ offsetMinutes = -offsetMinutes;
13007
+ }
13008
+ const micros = frac === undefined ? 0 : Number((frac + "000000").slice(0, 6));
13009
+ const ms = Date.UTC(year, month - 1, day, hour, minute, second);
13010
+ return (ms + micros / 1000) / 86400000 + 2440587.5 - offsetMinutes / 1440;
13011
+ }
13012
+ function changedSinceStampNewer(stamp, since) {
13013
+ const stampJd = sqliteJulianDay(stamp);
13014
+ if (stampJd === null)
13015
+ return true;
13016
+ const sinceJd = sqliteJulianDay(since);
13017
+ if (sinceJd === null)
13018
+ return false;
13019
+ return stampJd > sinceJd;
13020
+ }
13021
+
12963
13022
  // src/lib/plan-project-link-contract.ts
12964
13023
  init_types();
12965
13024
  import { createHash as createHash2 } from "crypto";
@@ -13783,7 +13842,26 @@ function createPostgresTodosStorageAdapter(options) {
13783
13842
  list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
13784
13843
  update: (id, input) => updatePlan(id, input, store),
13785
13844
  completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
13786
- delete: (id, context) => store.deletePlan(id, context)
13845
+ delete: (id, context) => store.deletePlan(id, context),
13846
+ addComment: (input, context) => addPlanComment(input, store, context),
13847
+ getComments: async (planId) => {
13848
+ const pages = [];
13849
+ let before;
13850
+ while (true) {
13851
+ const page = await store.listPlanComments(planId, { limit: 1000, ...before ? { before } : {} });
13852
+ if (page.length === 0)
13853
+ break;
13854
+ pages.unshift(page);
13855
+ if (page.length < 1000)
13856
+ break;
13857
+ const oldest = page[0];
13858
+ before = { created_at: oldest.created_at, id: oldest.id };
13859
+ }
13860
+ return pages.flat().map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
13861
+ },
13862
+ getCommentsPage: async (planId, options2) => {
13863
+ return (await store.listPlanComments(planId, options2)).map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
13864
+ }
13787
13865
  },
13788
13866
  planProjectLinks: {
13789
13867
  apply: (input, context) => store.applyPlanProjectLink(input, context),
@@ -13877,11 +13955,16 @@ class PostgresJsonRecordStore {
13877
13955
  return context?.requestId ?? this.sourceMachineId ?? null;
13878
13956
  }
13879
13957
  async ensureSchema() {
13880
- this.schemaReady ??= (async () => {
13881
- for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
13882
- await this.options.client.query(sql);
13883
- }
13884
- })();
13958
+ if (!this.schemaReady) {
13959
+ this.schemaReady = (async () => {
13960
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
13961
+ await this.options.client.query(sql);
13962
+ }
13963
+ })().catch((error) => {
13964
+ this.schemaReady = null;
13965
+ throw error;
13966
+ });
13967
+ }
13885
13968
  await this.schemaReady;
13886
13969
  }
13887
13970
  async get(type, id) {
@@ -14001,6 +14084,27 @@ class PostgresJsonRecordStore {
14001
14084
  LIMIT $${params.length}`, params);
14002
14085
  return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
14003
14086
  }
14087
+ async listPlanComments(planId, options = {}) {
14088
+ await this.ensureSchema();
14089
+ const limit = options.limit ?? 100;
14090
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
14091
+ throw new Error("Postgres plan comment limit must be an integer between 1 and 1001");
14092
+ }
14093
+ const params = [this.service, planId];
14094
+ let cursorPredicate = "";
14095
+ if (options.before) {
14096
+ params.push(options.before.created_at, options.before.id);
14097
+ cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
14098
+ }
14099
+ params.push(limit);
14100
+ const result = await this.options.client.query(`/* todos:list-plan-comments */ SELECT payload FROM ${this.tableName}
14101
+ WHERE service = $1 AND object_type = 'plan_comments' AND deleted_at IS NULL
14102
+ AND payload->>'plan_id' = $2
14103
+ ${cursorPredicate}
14104
+ ORDER BY payload->>'created_at' DESC, object_id DESC
14105
+ LIMIT $${params.length}`, params);
14106
+ return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
14107
+ }
14004
14108
  async listRecords(type) {
14005
14109
  await this.ensureSchema();
14006
14110
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
@@ -15162,6 +15266,15 @@ class PostgresJsonRecordStore {
15162
15266
  WHERE r.service = $1 AND r.object_type = 'tasks' AND r.deleted_at IS NULL
15163
15267
  AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
15164
15268
  RETURNING 1
15269
+ ), deleted_plan_comments AS (
15270
+ UPDATE ${this.tableName} r SET
15271
+ deleted_at = $3::timestamptz,
15272
+ updated_at = $3::timestamptz,
15273
+ source_machine_id = COALESCE($4, r.source_machine_id),
15274
+ version = COALESCE(r.version, 0) + 1
15275
+ WHERE r.service = $1 AND r.object_type = 'plan_comments' AND r.deleted_at IS NULL
15276
+ AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
15277
+ RETURNING 1
15165
15278
  ), deleted_plan AS (
15166
15279
  UPDATE ${this.tableName} r SET
15167
15280
  deleted_at = $3::timestamptz,
@@ -15337,7 +15450,8 @@ async function updateTask(id, input, store, context) {
15337
15450
  task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
15338
15451
  parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
15339
15452
  created_by: existing.created_by,
15340
- completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
15453
+ completed_at: input.status === "completed" ? input.completed_at ?? new Date().toISOString() : input.completed_at !== undefined ? input.completed_at : input.status !== undefined && isTerminalStatus(input.status) ? existing.completed_at ?? new Date().toISOString() : reopened ? null : existing.completed_at,
15454
+ started_at: input.status === "in_progress" ? existing.started_at ?? new Date().toISOString() : existing.started_at
15341
15455
  };
15342
15456
  const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
15343
15457
  operation: "update",
@@ -15382,7 +15496,8 @@ async function failTask(id, agentId, reason, options, store) {
15382
15496
  status: "failed",
15383
15497
  assigned_to: task.assigned_to ?? agentId ?? null,
15384
15498
  reason: reason ?? task.reason,
15385
- retry_after: options?.retry_after ?? task.retry_after
15499
+ retry_after: options?.retry_after ?? task.retry_after,
15500
+ completed_at: task.completed_at ?? new Date().toISOString()
15386
15501
  }, store);
15387
15502
  if (!options?.retry)
15388
15503
  return { task: failed };
@@ -15614,7 +15729,7 @@ async function getActiveWork(filters, store) {
15614
15729
  }));
15615
15730
  }
15616
15731
  async function getChangedSince(since, filters, store) {
15617
- return (await listTasks(filters ?? {}, store)).filter((task) => task.updated_at > since);
15732
+ return (await listTasks(filters ?? {}, store)).filter((task) => changedSinceStampNewer(task.updated_at ?? "", since));
15618
15733
  }
15619
15734
  async function createProject(input, store, context) {
15620
15735
  const timestamp = new Date().toISOString();
@@ -15830,16 +15945,22 @@ async function createTaskList(input, store, context) {
15830
15945
  async function updateTaskList(id, input, store) {
15831
15946
  const list = await requireRecord("task_lists", id, store);
15832
15947
  const patch = definedPatch(input);
15948
+ const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? list.project_id : input.project_id;
15833
15949
  if (input.slug !== undefined) {
15834
15950
  const slug = slugifyRaw(input.slug);
15835
15951
  if (!slug)
15836
15952
  throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
15837
- const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === list.project_id && candidate.slug === slug);
15953
+ patch.slug = slug;
15954
+ }
15955
+ const effectiveSlug = patch.slug ?? list.slug;
15956
+ if (projectId !== list.project_id || effectiveSlug !== list.slug) {
15957
+ const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === projectId && candidate.slug === effectiveSlug);
15838
15958
  if (duplicate) {
15839
- throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
15959
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
15840
15960
  }
15841
- patch.slug = slug;
15842
15961
  }
15962
+ if (projectId !== list.project_id)
15963
+ patch.project_id = projectId;
15843
15964
  return store.upsert("task_lists", {
15844
15965
  ...list,
15845
15966
  ...patch,
@@ -15932,6 +16053,22 @@ async function addComment(input, store, context) {
15932
16053
  function redactComment(comment) {
15933
16054
  return { ...comment, content: redactEvidenceText(comment.content) };
15934
16055
  }
16056
+ async function addPlanComment(input, store, context) {
16057
+ const comment = {
16058
+ id: randomUUID(),
16059
+ plan_id: input.plan_id,
16060
+ agent_id: input.agent_id ?? context?.agentId ?? null,
16061
+ session_id: input.session_id ?? context?.sessionId ?? null,
16062
+ content: redactEvidenceText(input.content),
16063
+ type: input.type ?? "comment",
16064
+ progress_pct: input.progress_pct ?? null,
16065
+ created_at: new Date().toISOString()
16066
+ };
16067
+ return store.upsert("plan_comments", comment, context);
16068
+ }
16069
+ function redactPlanComment(comment) {
16070
+ return { ...comment, content: redactEvidenceText(comment.content) };
16071
+ }
15935
16072
  async function exportSnapshot(store) {
15936
16073
  return {
15937
16074
  exportedAt: new Date().toISOString(),
@@ -16523,14 +16660,23 @@ class PostgresTodosProjectRegistrationBackend {
16523
16660
  this.cursorTableName = safeIdentifier(options.cursorTableName ?? "todos_sync_cursors", "cursorTableName");
16524
16661
  }
16525
16662
  async ensureSchema() {
16526
- this.schemaReady ??= (async () => {
16527
- for (const statement of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
16528
- await this.client.query(statement);
16529
- }
16530
- for (const statement of postgresTodosProjectRegistrationSchemaSql()) {
16531
- await this.client.query(statement);
16663
+ if (this.schemaReady === null) {
16664
+ const attempt = (async () => {
16665
+ for (const statement of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
16666
+ await this.client.query(statement);
16667
+ }
16668
+ for (const statement of postgresTodosProjectRegistrationSchemaSql()) {
16669
+ await this.client.query(statement);
16670
+ }
16671
+ })();
16672
+ this.schemaReady = attempt;
16673
+ try {
16674
+ await attempt;
16675
+ } catch (error) {
16676
+ this.schemaReady = null;
16677
+ throw error;
16532
16678
  }
16533
- })();
16679
+ }
16534
16680
  await this.schemaReady;
16535
16681
  }
16536
16682
  async transaction(fn) {
@@ -16817,6 +16963,40 @@ init_tasks();
16817
16963
  init_projects();
16818
16964
  init_plans();
16819
16965
 
16966
+ // src/db/plan-comments.ts
16967
+ init_types();
16968
+ init_database();
16969
+ init_plans();
16970
+ init_prewrite_secrets();
16971
+ function addPlanComment2(input, db) {
16972
+ const d = db || getDatabase();
16973
+ if (!getPlan(input.plan_id, d)) {
16974
+ throw new PlanNotFoundError(input.plan_id);
16975
+ }
16976
+ const id = uuid();
16977
+ const timestamp2 = now();
16978
+ d.run(`INSERT INTO plan_comments (id, plan_id, agent_id, session_id, content, type, progress_pct, created_at)
16979
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
16980
+ id,
16981
+ input.plan_id,
16982
+ input.agent_id || null,
16983
+ input.session_id || null,
16984
+ sanitizePreWriteText(input.content, "comment.content"),
16985
+ input.type || "comment",
16986
+ input.progress_pct ?? null,
16987
+ timestamp2
16988
+ ]);
16989
+ return getPlanComment(id, d);
16990
+ }
16991
+ function getPlanComment(id, db) {
16992
+ const d = db || getDatabase();
16993
+ return d.query("SELECT * FROM plan_comments WHERE id = ?").get(id);
16994
+ }
16995
+ function listPlanComments(planId, db) {
16996
+ const d = db || getDatabase();
16997
+ return d.query("SELECT * FROM plan_comments WHERE plan_id = ? ORDER BY created_at, rowid").all(planId);
16998
+ }
16999
+
16820
17000
  // src/db/agents.ts
16821
17001
  init_types();
16822
17002
  init_database();
@@ -17469,6 +17649,20 @@ init_task_lists();
17469
17649
  init_templates();
17470
17650
  init_audit();
17471
17651
  init_comments();
17652
+
17653
+ // src/lib/comment-cursor.ts
17654
+ function isStrictlyOlder(comment, before) {
17655
+ return comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id;
17656
+ }
17657
+ function compareCommentKeyset(left, right) {
17658
+ if (isStrictlyOlder(left, right))
17659
+ return -1;
17660
+ if (isStrictlyOlder(right, left))
17661
+ return 1;
17662
+ return 0;
17663
+ }
17664
+
17665
+ // src/storage/local-sqlite.ts
17472
17666
  init_database();
17473
17667
 
17474
17668
  // src/db/integrity.ts
@@ -18164,7 +18358,7 @@ function matchesExtraFilters(task, filter) {
18164
18358
  }
18165
18359
  if (filter.tags?.length) {
18166
18360
  const taskTags = new Set(task.tags ?? []);
18167
- if (!filter.tags.every((tag) => taskTags.has(tag)))
18361
+ if (!filter.tags.some((tag) => taskTags.has(tag)))
18168
18362
  return false;
18169
18363
  }
18170
18364
  if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
@@ -18240,7 +18434,22 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
18240
18434
  list: (projectId) => listPlans(projectId, database()),
18241
18435
  update: (id, input) => updatePlan2(id, input, database()),
18242
18436
  completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
18243
- delete: (id) => deletePlan(id, database())
18437
+ delete: (id) => deletePlan(id, database()),
18438
+ addComment: (input) => addPlanComment2(input, database()),
18439
+ getComments: (planId) => listPlanComments(planId, database()),
18440
+ getCommentsPage: (planId, options2) => {
18441
+ if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
18442
+ throw new Error("Plan comment limit must be an integer between 1 and 1001");
18443
+ }
18444
+ let comments = listPlanComments(planId, database());
18445
+ comments = comments.sort(compareCommentKeyset);
18446
+ if (options2?.before) {
18447
+ const before = options2.before;
18448
+ comments = comments.filter((comment) => isStrictlyOlder(comment, before));
18449
+ }
18450
+ const limit = options2?.limit ?? 100;
18451
+ return comments.slice(-limit);
18452
+ }
18244
18453
  },
18245
18454
  planProjectLinks: {
18246
18455
  apply: (input) => applyPlanProjectLinkSqlite(input, database()),
@@ -18281,10 +18490,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
18281
18490
  throw new Error("Comment limit must be an integer between 1 and 1001");
18282
18491
  }
18283
18492
  let comments = listComments(taskId, database());
18284
- comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
18493
+ comments = comments.sort(compareCommentKeyset);
18285
18494
  if (options2?.before) {
18286
18495
  const before = options2.before;
18287
- comments = comments.filter((comment) => comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id);
18496
+ comments = comments.filter((comment) => isStrictlyOlder(comment, before));
18288
18497
  }
18289
18498
  if (options2?.limit !== undefined)
18290
18499
  comments = comments.slice(-options2.limit);