@hasna/todos 0.13.2 → 0.13.3

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 (69) hide show
  1. package/README.md +78 -14
  2. package/dist/cli/cloud-router.d.ts +57 -7
  3. package/dist/cli/cloud-router.d.ts.map +1 -1
  4. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  5. package/dist/cli/doctor-integrity.d.ts +106 -0
  6. package/dist/cli/doctor-integrity.d.ts.map +1 -0
  7. package/dist/cli/helpers.d.ts +2 -2
  8. package/dist/cli/index.js +1461 -778
  9. package/dist/cli/stage-a.d.ts.map +1 -1
  10. package/dist/contracts.js +118 -30
  11. package/dist/db/database.d.ts.map +1 -1
  12. package/dist/db/findings.d.ts +1 -0
  13. package/dist/db/findings.d.ts.map +1 -1
  14. package/dist/db/integrity.d.ts +20 -0
  15. package/dist/db/integrity.d.ts.map +1 -0
  16. package/dist/index.d.ts +5 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +562 -132
  19. package/dist/lib/context-packs.d.ts +1 -0
  20. package/dist/lib/context-packs.d.ts.map +1 -1
  21. package/dist/lib/dependency-graph.d.ts +1 -1
  22. package/dist/lib/doctor.d.ts +20 -0
  23. package/dist/lib/doctor.d.ts.map +1 -1
  24. package/dist/lib/integrity.d.ts +215 -0
  25. package/dist/lib/integrity.d.ts.map +1 -0
  26. package/dist/lib/native-storage-status.d.ts +2 -2
  27. package/dist/lib/native-storage-status.d.ts.map +1 -1
  28. package/dist/lib/release-compatibility.d.ts.map +1 -1
  29. package/dist/lib/task-dedupe.d.ts +15 -0
  30. package/dist/lib/task-dedupe.d.ts.map +1 -1
  31. package/dist/mcp/index.d.ts.map +1 -1
  32. package/dist/mcp/index.js +517 -85
  33. package/dist/mcp.js +7 -3
  34. package/dist/registry.d.ts +1 -1
  35. package/dist/registry.d.ts.map +1 -1
  36. package/dist/registry.js +125 -30
  37. package/dist/release-provenance.json +5 -5
  38. package/dist/sdk/types.d.ts +42 -0
  39. package/dist/sdk/types.d.ts.map +1 -1
  40. package/dist/sdk/v1.generated.d.ts +1 -0
  41. package/dist/sdk/v1.generated.d.ts.map +1 -1
  42. package/dist/server/cloud.d.ts +8 -1
  43. package/dist/server/cloud.d.ts.map +1 -1
  44. package/dist/server/index.js +521 -89
  45. package/dist/server/openapi.d.ts +12 -0
  46. package/dist/server/openapi.d.ts.map +1 -1
  47. package/dist/server/serve.d.ts.map +1 -1
  48. package/dist/server/v1.d.ts.map +1 -1
  49. package/dist/storage/config.d.ts +23 -5
  50. package/dist/storage/config.d.ts.map +1 -1
  51. package/dist/storage/factory.d.ts +9 -3
  52. package/dist/storage/factory.d.ts.map +1 -1
  53. package/dist/storage/hybrid.d.ts +7 -0
  54. package/dist/storage/hybrid.d.ts.map +1 -1
  55. package/dist/storage/index.d.ts +3 -3
  56. package/dist/storage/index.d.ts.map +1 -1
  57. package/dist/storage/interfaces.d.ts +29 -8
  58. package/dist/storage/interfaces.d.ts.map +1 -1
  59. package/dist/storage/local-sqlite.d.ts.map +1 -1
  60. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  61. package/dist/storage.d.ts +3 -3
  62. package/dist/storage.d.ts.map +1 -1
  63. package/dist/storage.js +601 -250
  64. package/dist/testing.d.ts +79 -0
  65. package/dist/testing.d.ts.map +1 -0
  66. package/dist/testing.js +107 -0
  67. package/dist/types/index.d.ts +12 -1
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/package.json +7 -3
package/dist/storage.js CHANGED
@@ -43,15 +43,18 @@ var __require = import.meta.require;
43
43
  var exports_config = {};
44
44
  __export(exports_config, {
45
45
  parseStorageMode: () => parseStorageMode,
46
+ parseStorageBackend: () => parseStorageBackend,
46
47
  loadTodosStorageConfig: () => loadTodosStorageConfig,
47
48
  loadStorageConfig: () => loadStorageConfig,
48
49
  isTodosShadowEnabled: () => isTodosShadowEnabled,
49
50
  isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
51
+ isTodosPostgresBackend: () => isTodosPostgresBackend,
50
52
  getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
51
53
  getTodosStorageMode: () => getTodosStorageMode,
52
54
  getTodosStorageEnvName: () => getTodosStorageEnvName,
53
55
  getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
54
56
  getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
57
+ getTodosStorageBackend: () => getTodosStorageBackend,
55
58
  getStorageMode: () => getStorageMode,
56
59
  getStorageDatabaseUrl: () => getStorageDatabaseUrl,
57
60
  getStorageDatabaseEnv: () => getStorageDatabaseEnv,
@@ -76,7 +79,7 @@ function getCanonicalTodosRdsConfig(env = process.env) {
76
79
  };
77
80
  }
78
81
  function loadTodosStorageConfig(env = process.env) {
79
- const mode = getTodosStorageMode(env);
82
+ const mode = getTodosStorageBackend(env);
80
83
  const databaseUrl = getTodosStorageDatabaseUrl(env);
81
84
  const bucket = readStorageEnv(env, "s3Bucket").value;
82
85
  const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
@@ -113,29 +116,41 @@ function loadTodosStorageConfig(env = process.env) {
113
116
  function loadStorageConfig(env = process.env) {
114
117
  return loadTodosStorageConfig(env);
115
118
  }
119
+ function isTodosPostgresBackend(config) {
120
+ return config.mode === "postgres";
121
+ }
116
122
  function isTodosRemoteStorageEnabled(config) {
117
- return config.mode === "remote" || config.mode === "hybrid";
123
+ return isTodosPostgresBackend(config);
118
124
  }
119
125
  function assertTodosRemoteStorageConfig(config) {
120
- if (!isTodosRemoteStorageEnabled(config))
126
+ if (!isTodosPostgresBackend(config))
121
127
  return;
122
128
  if (!config.database?.url) {
123
129
  throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
124
130
  }
125
131
  }
126
- function parseStorageMode(value) {
132
+ function parseStorageBackend(value) {
127
133
  const normalized = clean(value)?.toLowerCase();
128
134
  if (!normalized)
129
- return "local";
130
- if (normalized === "local" || normalized === "remote" || normalized === "hybrid")
135
+ return "sqlite";
136
+ if (normalized === "sqlite" || normalized === "postgres")
131
137
  return normalized;
132
- throw new Error(`${TODOS_STORAGE_ENV.mode} must be local, remote, or hybrid`);
138
+ const legacy = LEGACY_BACKEND_TOKENS[normalized];
139
+ if (legacy)
140
+ return legacy;
141
+ throw new Error(`${TODOS_STORAGE_ENV.mode} must be sqlite or postgres (legacy values local and remote are accepted)`);
142
+ }
143
+ function parseStorageMode(value) {
144
+ return parseStorageBackend(value);
145
+ }
146
+ function getTodosStorageBackend(env = process.env) {
147
+ return parseStorageBackend(readStorageEnv(env, "mode").value);
133
148
  }
134
149
  function getTodosStorageMode(env = process.env) {
135
- return parseStorageMode(readStorageEnv(env, "mode").value);
150
+ return getTodosStorageBackend(env);
136
151
  }
137
152
  function getStorageMode(env = process.env) {
138
- return getTodosStorageMode(env);
153
+ return getTodosStorageBackend(env);
139
154
  }
140
155
  function isTodosShadowEnabled(env = process.env) {
141
156
  return parseBoolean(readStorageEnv(env, "shadow").value, false);
@@ -146,8 +161,8 @@ function getTodosStorageShadowEnvName(env = process.env) {
146
161
  function assertTodosShadowConfig(config, env = process.env) {
147
162
  if (!isTodosShadowEnabled(env))
148
163
  return;
149
- if (config.mode !== "local") {
150
- throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires ${TODOS_STORAGE_ENV.mode}=local (got ${config.mode})`);
164
+ if (config.mode !== "sqlite") {
165
+ throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
151
166
  }
152
167
  if (!config.database?.url) {
153
168
  throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
@@ -203,7 +218,7 @@ function parsePositiveInteger(value, fallback) {
203
218
  }
204
219
  return parsed;
205
220
  }
206
- 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";
221
+ 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", LEGACY_BACKEND_TOKENS;
207
222
  var init_config = __esm(() => {
208
223
  TODOS_STORAGE_TABLES = [
209
224
  "todos_sync_records",
@@ -244,6 +259,187 @@ var init_config = __esm(() => {
244
259
  syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
245
260
  syncDryRun: "TODOS_SYNC_DRY_RUN"
246
261
  };
262
+ LEGACY_BACKEND_TOKENS = {
263
+ local: "sqlite",
264
+ remote: "postgres",
265
+ postgresql: "postgres",
266
+ hybrid: "postgres",
267
+ self_hosted: "postgres",
268
+ cloud: "postgres"
269
+ };
270
+ });
271
+
272
+ // src/types/index.ts
273
+ function isBlockingDependencyStatus(status) {
274
+ return status !== "completed" && status !== "cancelled";
275
+ }
276
+ var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
277
+ var init_types = __esm(() => {
278
+ TASK_STATUSES = [
279
+ "pending",
280
+ "in_progress",
281
+ "completed",
282
+ "failed",
283
+ "cancelled"
284
+ ];
285
+ TASK_PRIORITIES = [
286
+ "low",
287
+ "medium",
288
+ "high",
289
+ "critical"
290
+ ];
291
+ PLAN_STATUSES = ["active", "completed", "archived"];
292
+ VersionConflictError = class VersionConflictError extends Error {
293
+ taskId;
294
+ expectedVersion;
295
+ actualVersion;
296
+ static code = "VERSION_CONFLICT";
297
+ static suggestion = "Fetch the task with get_task to get the current version before updating.";
298
+ constructor(taskId, expectedVersion, actualVersion) {
299
+ super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
300
+ this.taskId = taskId;
301
+ this.expectedVersion = expectedVersion;
302
+ this.actualVersion = actualVersion;
303
+ this.name = "VersionConflictError";
304
+ }
305
+ };
306
+ TaskNotFoundError = class TaskNotFoundError extends Error {
307
+ taskId;
308
+ static code = "TASK_NOT_FOUND";
309
+ static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
310
+ constructor(taskId) {
311
+ super(`Task not found: ${taskId}`);
312
+ this.taskId = taskId;
313
+ this.name = "TaskNotFoundError";
314
+ }
315
+ };
316
+ TaskReferenceAmbiguousError = class TaskReferenceAmbiguousError extends Error {
317
+ reference;
318
+ static code = "TASK_REFERENCE_AMBIGUOUS";
319
+ candidateTaskIds;
320
+ candidateProjectIds;
321
+ constructor(reference, candidates) {
322
+ const candidateTaskIds = [...new Set(candidates.map((candidate) => candidate.task_id))].sort();
323
+ const candidateProjectIds = [...new Set(candidates.map((candidate) => candidate.project_id).filter((projectId) => typeof projectId === "string" && projectId.length > 0))].sort();
324
+ const projectList = candidateProjectIds.length > 0 ? candidateProjectIds.join(", ") : "(unscoped)";
325
+ super(`Task reference is ambiguous: "${reference}". Candidate project IDs: ${projectList}. ` + "Use a full task UUID.");
326
+ this.reference = reference;
327
+ this.name = "TaskReferenceAmbiguousError";
328
+ this.candidateTaskIds = candidateTaskIds;
329
+ this.candidateProjectIds = candidateProjectIds;
330
+ }
331
+ };
332
+ ProjectNotFoundError = class ProjectNotFoundError extends Error {
333
+ projectId;
334
+ static code = "PROJECT_NOT_FOUND";
335
+ static suggestion = "Use list_projects to see available projects.";
336
+ constructor(projectId) {
337
+ super(`Project not found: ${projectId}`);
338
+ this.projectId = projectId;
339
+ this.name = "ProjectNotFoundError";
340
+ }
341
+ };
342
+ ResourceConflictError = class ResourceConflictError extends Error {
343
+ code;
344
+ constructor(code, message) {
345
+ super(message);
346
+ this.code = code;
347
+ this.name = "ResourceConflictError";
348
+ }
349
+ };
350
+ PlanNotFoundError = class PlanNotFoundError extends Error {
351
+ planId;
352
+ static code = "PLAN_NOT_FOUND";
353
+ static suggestion = "Use list_plans to see available plans.";
354
+ constructor(planId) {
355
+ super(`Plan not found: ${planId}`);
356
+ this.planId = planId;
357
+ this.name = "PlanNotFoundError";
358
+ }
359
+ };
360
+ LockError = class LockError extends Error {
361
+ taskId;
362
+ lockedBy;
363
+ static code = "LOCK_ERROR";
364
+ static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
365
+ constructor(taskId, lockedBy) {
366
+ super(`Task ${taskId} is locked by ${lockedBy}`);
367
+ this.taskId = taskId;
368
+ this.lockedBy = lockedBy;
369
+ this.name = "LockError";
370
+ }
371
+ };
372
+ AgentNotFoundError = class AgentNotFoundError extends Error {
373
+ agentId;
374
+ static code = "AGENT_NOT_FOUND";
375
+ static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
376
+ constructor(agentId) {
377
+ super(`Agent not found: ${agentId}`);
378
+ this.agentId = agentId;
379
+ this.name = "AgentNotFoundError";
380
+ }
381
+ };
382
+ IdentityAliasAmbiguousError = class IdentityAliasAmbiguousError extends Error {
383
+ code = "IDENTITY_ALIAS_AMBIGUOUS";
384
+ candidates;
385
+ constructor(subject, candidates = []) {
386
+ super(`Identity alias or source is ambiguous: ${subject}`);
387
+ this.name = "IdentityAliasAmbiguousError";
388
+ this.candidates = [...new Set(candidates)].sort();
389
+ }
390
+ };
391
+ IdentityIdImmutableError = class IdentityIdImmutableError extends Error {
392
+ code = "IDENTITY_ID_IMMUTABLE";
393
+ constructor(agentId) {
394
+ super(`IDENTITY_ID_IMMUTABLE: canonical identity for agent ${agentId} cannot be replaced`);
395
+ this.name = "IdentityIdImmutableError";
396
+ }
397
+ };
398
+ TaskListNotFoundError = class TaskListNotFoundError extends Error {
399
+ taskListId;
400
+ static code = "TASK_LIST_NOT_FOUND";
401
+ static suggestion = "Use list_task_lists to see available lists.";
402
+ constructor(taskListId) {
403
+ super(`Task list not found: ${taskListId}`);
404
+ this.taskListId = taskListId;
405
+ this.name = "TaskListNotFoundError";
406
+ }
407
+ };
408
+ DependencyCycleError = class DependencyCycleError extends Error {
409
+ taskId;
410
+ dependsOn;
411
+ static code = "DEPENDENCY_CYCLE";
412
+ static suggestion = "Check the dependency chain with get_task to avoid circular references.";
413
+ constructor(taskId, dependsOn) {
414
+ super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
415
+ this.taskId = taskId;
416
+ this.dependsOn = dependsOn;
417
+ this.name = "DependencyCycleError";
418
+ }
419
+ };
420
+ CompletionGuardError = class CompletionGuardError extends Error {
421
+ reason;
422
+ retryAfterSeconds;
423
+ static code = "COMPLETION_BLOCKED";
424
+ static suggestion = "Wait for the cooldown period, then retry.";
425
+ constructor(reason, retryAfterSeconds) {
426
+ super(reason);
427
+ this.reason = reason;
428
+ this.retryAfterSeconds = retryAfterSeconds;
429
+ this.name = "CompletionGuardError";
430
+ }
431
+ };
432
+ DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
433
+ DispatchNotFoundError = class DispatchNotFoundError extends Error {
434
+ dispatchId;
435
+ static code = "DISPATCH_NOT_FOUND";
436
+ static suggestion = "Check the dispatch ID with list_dispatches.";
437
+ constructor(dispatchId) {
438
+ super(`Dispatch not found: ${dispatchId}`);
439
+ this.dispatchId = dispatchId;
440
+ this.name = "DispatchNotFoundError";
441
+ }
442
+ };
247
443
  });
248
444
 
249
445
  // src/db/migrations.ts
@@ -3630,163 +3826,6 @@ var init_machines = __esm(() => {
3630
3826
  ];
3631
3827
  });
3632
3828
 
3633
- // src/types/index.ts
3634
- function isBlockingDependencyStatus(status) {
3635
- return status !== "completed" && status !== "cancelled";
3636
- }
3637
- var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3638
- var init_types = __esm(() => {
3639
- TASK_STATUSES = [
3640
- "pending",
3641
- "in_progress",
3642
- "completed",
3643
- "failed",
3644
- "cancelled"
3645
- ];
3646
- TASK_PRIORITIES = [
3647
- "low",
3648
- "medium",
3649
- "high",
3650
- "critical"
3651
- ];
3652
- PLAN_STATUSES = ["active", "completed", "archived"];
3653
- VersionConflictError = class VersionConflictError extends Error {
3654
- taskId;
3655
- expectedVersion;
3656
- actualVersion;
3657
- static code = "VERSION_CONFLICT";
3658
- static suggestion = "Fetch the task with get_task to get the current version before updating.";
3659
- constructor(taskId, expectedVersion, actualVersion) {
3660
- super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
3661
- this.taskId = taskId;
3662
- this.expectedVersion = expectedVersion;
3663
- this.actualVersion = actualVersion;
3664
- this.name = "VersionConflictError";
3665
- }
3666
- };
3667
- TaskNotFoundError = class TaskNotFoundError extends Error {
3668
- taskId;
3669
- static code = "TASK_NOT_FOUND";
3670
- static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
3671
- constructor(taskId) {
3672
- super(`Task not found: ${taskId}`);
3673
- this.taskId = taskId;
3674
- this.name = "TaskNotFoundError";
3675
- }
3676
- };
3677
- ProjectNotFoundError = class ProjectNotFoundError extends Error {
3678
- projectId;
3679
- static code = "PROJECT_NOT_FOUND";
3680
- static suggestion = "Use list_projects to see available projects.";
3681
- constructor(projectId) {
3682
- super(`Project not found: ${projectId}`);
3683
- this.projectId = projectId;
3684
- this.name = "ProjectNotFoundError";
3685
- }
3686
- };
3687
- ResourceConflictError = class ResourceConflictError extends Error {
3688
- code;
3689
- constructor(code, message) {
3690
- super(message);
3691
- this.code = code;
3692
- this.name = "ResourceConflictError";
3693
- }
3694
- };
3695
- PlanNotFoundError = class PlanNotFoundError extends Error {
3696
- planId;
3697
- static code = "PLAN_NOT_FOUND";
3698
- static suggestion = "Use list_plans to see available plans.";
3699
- constructor(planId) {
3700
- super(`Plan not found: ${planId}`);
3701
- this.planId = planId;
3702
- this.name = "PlanNotFoundError";
3703
- }
3704
- };
3705
- LockError = class LockError extends Error {
3706
- taskId;
3707
- lockedBy;
3708
- static code = "LOCK_ERROR";
3709
- static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
3710
- constructor(taskId, lockedBy) {
3711
- super(`Task ${taskId} is locked by ${lockedBy}`);
3712
- this.taskId = taskId;
3713
- this.lockedBy = lockedBy;
3714
- this.name = "LockError";
3715
- }
3716
- };
3717
- AgentNotFoundError = class AgentNotFoundError extends Error {
3718
- agentId;
3719
- static code = "AGENT_NOT_FOUND";
3720
- static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
3721
- constructor(agentId) {
3722
- super(`Agent not found: ${agentId}`);
3723
- this.agentId = agentId;
3724
- this.name = "AgentNotFoundError";
3725
- }
3726
- };
3727
- IdentityAliasAmbiguousError = class IdentityAliasAmbiguousError extends Error {
3728
- code = "IDENTITY_ALIAS_AMBIGUOUS";
3729
- candidates;
3730
- constructor(subject, candidates = []) {
3731
- super(`Identity alias or source is ambiguous: ${subject}`);
3732
- this.name = "IdentityAliasAmbiguousError";
3733
- this.candidates = [...new Set(candidates)].sort();
3734
- }
3735
- };
3736
- IdentityIdImmutableError = class IdentityIdImmutableError extends Error {
3737
- code = "IDENTITY_ID_IMMUTABLE";
3738
- constructor(agentId) {
3739
- super(`IDENTITY_ID_IMMUTABLE: canonical identity for agent ${agentId} cannot be replaced`);
3740
- this.name = "IdentityIdImmutableError";
3741
- }
3742
- };
3743
- TaskListNotFoundError = class TaskListNotFoundError extends Error {
3744
- taskListId;
3745
- static code = "TASK_LIST_NOT_FOUND";
3746
- static suggestion = "Use list_task_lists to see available lists.";
3747
- constructor(taskListId) {
3748
- super(`Task list not found: ${taskListId}`);
3749
- this.taskListId = taskListId;
3750
- this.name = "TaskListNotFoundError";
3751
- }
3752
- };
3753
- DependencyCycleError = class DependencyCycleError extends Error {
3754
- taskId;
3755
- dependsOn;
3756
- static code = "DEPENDENCY_CYCLE";
3757
- static suggestion = "Check the dependency chain with get_task to avoid circular references.";
3758
- constructor(taskId, dependsOn) {
3759
- super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
3760
- this.taskId = taskId;
3761
- this.dependsOn = dependsOn;
3762
- this.name = "DependencyCycleError";
3763
- }
3764
- };
3765
- CompletionGuardError = class CompletionGuardError extends Error {
3766
- reason;
3767
- retryAfterSeconds;
3768
- static code = "COMPLETION_BLOCKED";
3769
- static suggestion = "Wait for the cooldown period, then retry.";
3770
- constructor(reason, retryAfterSeconds) {
3771
- super(reason);
3772
- this.reason = reason;
3773
- this.retryAfterSeconds = retryAfterSeconds;
3774
- this.name = "CompletionGuardError";
3775
- }
3776
- };
3777
- DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
3778
- DispatchNotFoundError = class DispatchNotFoundError extends Error {
3779
- dispatchId;
3780
- static code = "DISPATCH_NOT_FOUND";
3781
- static suggestion = "Check the dispatch ID with list_dispatches.";
3782
- constructor(dispatchId) {
3783
- super(`Dispatch not found: ${dispatchId}`);
3784
- this.dispatchId = dispatchId;
3785
- this.name = "DispatchNotFoundError";
3786
- }
3787
- };
3788
- });
3789
-
3790
3829
  // src/db/identity-mapping.ts
3791
3830
  function timestamp() {
3792
3831
  return new Date().toISOString();
@@ -4092,6 +4131,30 @@ function findGitRoot(startDir) {
4092
4131
  }
4093
4132
  return null;
4094
4133
  }
4134
+ function getGlobalDbPath() {
4135
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4136
+ return join(home, ".hasna", "todos", "todos.db");
4137
+ }
4138
+ function hasExplicitProjectArg(args = process.argv.slice(2)) {
4139
+ return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
4140
+ }
4141
+ function getCliCommand(args = process.argv.slice(2)) {
4142
+ const globalOptionsWithValues = new Set(["--project", "--agent", "--session"]);
4143
+ for (let index = 0;index < args.length; index += 1) {
4144
+ const arg = args[index];
4145
+ if (globalOptionsWithValues.has(arg)) {
4146
+ index += 1;
4147
+ continue;
4148
+ }
4149
+ if (arg.startsWith("-"))
4150
+ continue;
4151
+ return arg;
4152
+ }
4153
+ return null;
4154
+ }
4155
+ function canCreateScopedProjectDb(args = process.argv.slice(2)) {
4156
+ return getCliCommand(args) === "project-bootstrap" && !args.some((arg) => arg === "--dry-run" || arg.startsWith("--dry-run="));
4157
+ }
4095
4158
  function getDbPath() {
4096
4159
  if (process.env["HASNA_TODOS_DB_PATH"]) {
4097
4160
  return process.env["HASNA_TODOS_DB_PATH"];
@@ -4099,18 +4162,19 @@ function getDbPath() {
4099
4162
  if (process.env["TODOS_DB_PATH"]) {
4100
4163
  return process.env["TODOS_DB_PATH"];
4101
4164
  }
4165
+ if (hasExplicitProjectArg())
4166
+ return getGlobalDbPath();
4102
4167
  const cwd = process.cwd();
4103
4168
  const nearest = findNearestProjectDb(cwd);
4104
4169
  if (nearest)
4105
4170
  return nearest;
4106
4171
  if (process.env["TODOS_DB_SCOPE"] === "project") {
4107
4172
  const gitRoot = findGitRoot(cwd);
4108
- if (gitRoot) {
4173
+ if (gitRoot && canCreateScopedProjectDb()) {
4109
4174
  return join(gitRoot, ".hasna", "todos", "todos.db");
4110
4175
  }
4111
4176
  }
4112
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4113
- return join(home, ".hasna", "todos", "todos.db");
4177
+ return getGlobalDbPath();
4114
4178
  }
4115
4179
  function getDatabasePath() {
4116
4180
  return getDbPath();
@@ -4213,10 +4277,13 @@ function resolvePartialId(db, table, partialId) {
4213
4277
  return null;
4214
4278
  }
4215
4279
  if (table === "tasks") {
4216
- const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
4280
+ const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = LOWER(?) ORDER BY project_id, id").all(partialId);
4217
4281
  if (shortIdRows.length === 1) {
4218
4282
  return shortIdRows[0].id;
4219
4283
  }
4284
+ if (shortIdRows.length > 1) {
4285
+ throw new TaskReferenceAmbiguousError(partialId, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
4286
+ }
4220
4287
  }
4221
4288
  if (table === "task_lists") {
4222
4289
  const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
@@ -11834,6 +11901,9 @@ var init_tasks = __esm(() => {
11834
11901
  // src/storage/index.ts
11835
11902
  init_config();
11836
11903
 
11904
+ // src/storage/local-sqlite.ts
11905
+ init_types();
11906
+
11837
11907
  // src/lib/search.ts
11838
11908
  init_database();
11839
11909
  function rowToTask(row) {
@@ -12650,6 +12720,267 @@ init_audit();
12650
12720
  init_comments();
12651
12721
  init_database();
12652
12722
 
12723
+ // src/db/integrity.ts
12724
+ init_database();
12725
+
12726
+ // src/lib/integrity.ts
12727
+ init_types();
12728
+ var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1";
12729
+ var TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
12730
+ var OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
12731
+ var INTEGRITY_CONDITIONS = [
12732
+ {
12733
+ id: "tasks_without_project",
12734
+ entity: "task",
12735
+ field: "project_id",
12736
+ target: "project",
12737
+ kind: "missing",
12738
+ base_severity: "warn",
12739
+ escalate_when_open: true,
12740
+ impact: "invisible to every project-scoped read (list, status, next, claim)"
12741
+ },
12742
+ {
12743
+ id: "tasks_without_task_list",
12744
+ entity: "task",
12745
+ field: "task_list_id",
12746
+ target: "task_list",
12747
+ kind: "missing",
12748
+ base_severity: "warn",
12749
+ escalate_when_open: true,
12750
+ impact: "invisible to every task-list read \u2014 the list reports zero open work"
12751
+ },
12752
+ {
12753
+ id: "tasks_with_unregistered_project",
12754
+ entity: "task",
12755
+ field: "project_id",
12756
+ target: "project",
12757
+ kind: "dangling",
12758
+ base_severity: "error",
12759
+ escalate_when_open: false,
12760
+ impact: "points at a project id that does not exist; the reference can never resolve"
12761
+ },
12762
+ {
12763
+ id: "tasks_with_unregistered_task_list",
12764
+ entity: "task",
12765
+ field: "task_list_id",
12766
+ target: "task_list",
12767
+ kind: "dangling",
12768
+ base_severity: "error",
12769
+ escalate_when_open: false,
12770
+ impact: "points at a task-list id that does not exist; the reference can never resolve"
12771
+ },
12772
+ {
12773
+ id: "task_lists_without_project",
12774
+ entity: "task_list",
12775
+ field: "project_id",
12776
+ target: "project",
12777
+ kind: "missing",
12778
+ base_severity: "warn",
12779
+ escalate_when_open: false,
12780
+ impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
12781
+ },
12782
+ {
12783
+ id: "task_lists_with_unregistered_project",
12784
+ entity: "task_list",
12785
+ field: "project_id",
12786
+ target: "project",
12787
+ kind: "dangling",
12788
+ base_severity: "error",
12789
+ escalate_when_open: false,
12790
+ impact: "points at a project id that does not exist; the list can never be reached"
12791
+ }
12792
+ ];
12793
+ function integrityCondition(id) {
12794
+ const spec = INTEGRITY_CONDITIONS.find((condition) => condition.id === id);
12795
+ if (!spec)
12796
+ throw new Error(`unknown integrity condition: ${id}`);
12797
+ return spec;
12798
+ }
12799
+ function resolveIntegritySeverity(spec, measurement) {
12800
+ if (spec.kind === "dangling")
12801
+ return "error";
12802
+ if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
12803
+ return "error";
12804
+ return spec.base_severity;
12805
+ }
12806
+ var ENTITY_LABEL = {
12807
+ task: ["task", "tasks"],
12808
+ task_list: ["task list", "task lists"]
12809
+ };
12810
+ function plural(entity, count) {
12811
+ const [one, many] = ENTITY_LABEL[entity];
12812
+ return count === 1 ? one : many;
12813
+ }
12814
+ function formatIntegrityMessage(spec, measurement) {
12815
+ const noun = plural(spec.entity, measurement.count);
12816
+ const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
12817
+ const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
12818
+ return spec.kind === "missing" ? `${measurement.count} ${noun} ${verb[0]} no ${spec.field}${open}` : `${measurement.count} ${noun} ${verb[1]} a ${spec.target.replace("_", " ")} that is not registered${open}`;
12819
+ }
12820
+ function measuredCondition(spec, measurement, source) {
12821
+ return {
12822
+ id: spec.id,
12823
+ entity: spec.entity,
12824
+ field: spec.field,
12825
+ kind: spec.kind,
12826
+ count: measurement.count,
12827
+ open_count: measurement.open_count,
12828
+ severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
12829
+ verified: true,
12830
+ source,
12831
+ message: formatIntegrityMessage(spec, measurement),
12832
+ impact: spec.impact
12833
+ };
12834
+ }
12835
+ function unverifiedCondition(spec, reason) {
12836
+ return {
12837
+ id: spec.id,
12838
+ entity: spec.entity,
12839
+ field: spec.field,
12840
+ kind: spec.kind,
12841
+ count: null,
12842
+ open_count: null,
12843
+ severity: null,
12844
+ verified: false,
12845
+ source: "unverified",
12846
+ unverified_reason: reason,
12847
+ message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
12848
+ impact: spec.impact
12849
+ };
12850
+ }
12851
+ function summarizeIntegrity(conditions) {
12852
+ const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
12853
+ const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
12854
+ const unverified = conditions.length - measured.length;
12855
+ return {
12856
+ ok: unverified === 0 && findings.length === 0,
12857
+ findings: findings.length,
12858
+ rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
12859
+ errors: findings.filter((condition) => condition.severity === "error").length,
12860
+ warnings: findings.filter((condition) => condition.severity === "warn").length,
12861
+ unverified,
12862
+ complete: unverified === 0
12863
+ };
12864
+ }
12865
+ function buildIntegrityReport(conditions, generatedAt) {
12866
+ const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source) => source !== "unverified");
12867
+ return {
12868
+ schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
12869
+ generated_at: generatedAt,
12870
+ source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
12871
+ conditions,
12872
+ summary: summarizeIntegrity(conditions)
12873
+ };
12874
+ }
12875
+ function adoptRemoteIntegrityReport(raw, fallbackGeneratedAt) {
12876
+ const received = new Map;
12877
+ if (Array.isArray(raw.conditions)) {
12878
+ for (const entry of raw.conditions) {
12879
+ if (entry && typeof entry["id"] === "string")
12880
+ received.set(entry["id"], entry);
12881
+ }
12882
+ }
12883
+ const conditions = INTEGRITY_CONDITIONS.map((spec) => {
12884
+ const entry = received.get(spec.id);
12885
+ const count = entry?.["count"];
12886
+ const verified = entry?.["verified"];
12887
+ if (!entry || verified === false || typeof count !== "number" || !Number.isFinite(count)) {
12888
+ return unverifiedCondition(spec, entry ? typeof entry["unverified_reason"] === "string" ? String(entry["unverified_reason"]) : "authority reported this condition without a usable count" : "authority did not report this condition");
12889
+ }
12890
+ const openRaw = entry["open_count"];
12891
+ return measuredCondition(spec, {
12892
+ count,
12893
+ open_count: spec.entity === "task" ? typeof openRaw === "number" && Number.isFinite(openRaw) ? openRaw : 0 : null
12894
+ }, typeof raw.source === "string" && raw.source === "postgres" ? "postgres" : "remote-authority");
12895
+ });
12896
+ const report = buildIntegrityReport(conditions, typeof raw.generated_at === "string" ? raw.generated_at : fallbackGeneratedAt);
12897
+ return typeof raw.source === "string" && (raw.source === "sqlite" || raw.source === "postgres") ? { ...report, source: raw.source } : report;
12898
+ }
12899
+ var SQLITE_TABLE = {
12900
+ task: "tasks",
12901
+ task_list: "task_lists",
12902
+ project: "projects"
12903
+ };
12904
+ function sqliteOpenStatusList() {
12905
+ return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
12906
+ }
12907
+ function buildSqliteIntegritySql(spec) {
12908
+ const table = SQLITE_TABLE[spec.entity];
12909
+ const target = SQLITE_TABLE[spec.target];
12910
+ const column = `t."${spec.field}"`;
12911
+ const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' ` + `AND NOT EXISTS (SELECT 1 FROM "${target}" r WHERE r."id" = ${column})`;
12912
+ const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
12913
+ return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
12914
+ }
12915
+ var RECORD_TYPE = {
12916
+ task: "tasks",
12917
+ task_list: "task_lists",
12918
+ project: "projects"
12919
+ };
12920
+ function buildPostgresIntegritySql(spec, options) {
12921
+ const params = [options.service, RECORD_TYPE[spec.entity]];
12922
+ const p = (value) => {
12923
+ params.push(value);
12924
+ return `$${params.length}`;
12925
+ };
12926
+ const column = `t.payload->>'${spec.field}'`;
12927
+ const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' AND NOT EXISTS (` + `SELECT 1 FROM ${options.table} r WHERE r.service = $1 AND r.object_type = ${p(RECORD_TYPE[spec.target])} ` + `AND r.deleted_at IS NULL AND r.object_id = ${column})`;
12928
+ const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
12929
+ const sql = `/* todos:integrity-${spec.id} */ SELECT COUNT(*)::int AS count, ${openExpr} AS open_count ` + `FROM ${options.table} t WHERE t.service = $1 AND t.object_type = $2 AND t.deleted_at IS NULL AND ${predicate}`;
12930
+ return { sql, params };
12931
+ }
12932
+ function referenceOf(row, field) {
12933
+ const raw = row[field];
12934
+ if (typeof raw !== "string")
12935
+ return null;
12936
+ const trimmed = raw.trim();
12937
+ return trimmed === "" ? null : trimmed;
12938
+ }
12939
+ function measureIntegrityRows(spec, sets) {
12940
+ const rows = spec.entity === "task" ? sets.tasks : sets.taskLists;
12941
+ if (!rows)
12942
+ return null;
12943
+ const registered = spec.kind === "dangling" ? spec.target === "project" ? sets.projectIds : sets.taskListIds : undefined;
12944
+ if (spec.kind === "dangling" && !registered)
12945
+ return null;
12946
+ let count = 0;
12947
+ let open = 0;
12948
+ for (const row of rows) {
12949
+ const reference = referenceOf(row, spec.field);
12950
+ const matches = spec.kind === "missing" ? reference === null : reference !== null && !registered.has(reference);
12951
+ if (!matches)
12952
+ continue;
12953
+ count++;
12954
+ if (spec.entity === "task") {
12955
+ const status = row.status;
12956
+ if (typeof status === "string" && OPEN_TASK_STATUSES.includes(status))
12957
+ open++;
12958
+ }
12959
+ }
12960
+ return { count, open_count: spec.entity === "task" ? open : null };
12961
+ }
12962
+
12963
+ // src/db/integrity.ts
12964
+ var REQUIRED_TABLES = { tasks: true, task_lists: true, projects: true };
12965
+ function tableExists(db, table) {
12966
+ return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
12967
+ }
12968
+ function scanSqliteIntegrity(db = getDatabase()) {
12969
+ const missing = Object.keys(REQUIRED_TABLES).filter((table) => !tableExists(db, table));
12970
+ const conditions = INTEGRITY_CONDITIONS.map((spec) => {
12971
+ if (missing.length > 0) {
12972
+ return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
12973
+ }
12974
+ try {
12975
+ const row = db.query(buildSqliteIntegritySql(spec)).get();
12976
+ return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
12977
+ } catch (error) {
12978
+ return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
12979
+ }
12980
+ });
12981
+ return buildIntegrityReport(conditions, now());
12982
+ }
12983
+
12653
12984
  // src/storage/sqlite-snapshot.ts
12654
12985
  init_database();
12655
12986
  init_audit();
@@ -13041,15 +13372,15 @@ function resolveTaskRefLocal(db, ref) {
13041
13372
  return null;
13042
13373
  if (TASK_UUID_RE.test(raw))
13043
13374
  return getTask(raw, db);
13044
- const prefixRows = db.query("SELECT id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
13375
+ const prefixRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' ORDER BY project_id, id LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
13045
13376
  if (prefixRows.length > 1) {
13046
- throw new Error(`Task reference is ambiguous: "${ref}"`);
13377
+ throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
13047
13378
  }
13048
13379
  if (prefixRows.length === 1)
13049
13380
  return getTask(prefixRows[0].id, db);
13050
- const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
13381
+ const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
13051
13382
  if (shortIdRows.length > 1) {
13052
- throw new Error(`Task reference is ambiguous: "${ref}"`);
13383
+ throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
13053
13384
  }
13054
13385
  if (shortIdRows.length === 1)
13055
13386
  return getTask(shortIdRows[0].id, db);
@@ -13198,6 +13529,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
13198
13529
  exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
13199
13530
  importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
13200
13531
  },
13532
+ integrity: {
13533
+ report: () => scanSqliteIntegrity(database())
13534
+ },
13201
13535
  transaction: (fn) => {
13202
13536
  const tx = database().transaction(() => fn(adapter));
13203
13537
  return tx();
@@ -13206,6 +13540,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
13206
13540
  return adapter;
13207
13541
  }
13208
13542
 
13543
+ // src/storage/postgres-adapter.ts
13544
+ init_types();
13545
+ import { randomUUID as randomUUID3 } from "crypto";
13546
+
13209
13547
  // src/storage/postgres-sync.ts
13210
13548
  var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
13211
13549
  var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
@@ -13595,54 +13933,7 @@ function assertSafeIdentifier(value) {
13595
13933
  throw new Error(`Unsafe Postgres identifier: ${value}`);
13596
13934
  }
13597
13935
 
13598
- // src/storage/hybrid.ts
13599
- function createHybridTodosStorageAdapter(options) {
13600
- const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
13601
- const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
13602
- if (!syncStore)
13603
- throw new Error("hybrid storage requires a Postgres sync store or query client");
13604
- if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
13605
- throw new Error("hybrid storage requires local snapshot export/import support");
13606
- }
13607
- const exportSnapshot = async (context) => await local.sync.exportSnapshot(context);
13608
- const importSnapshot = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
13609
- const remote = {
13610
- ensureSchema: () => syncStore.ensureSchema(),
13611
- async pushSnapshot(context) {
13612
- const snapshot = await exportSnapshot(context);
13613
- return syncStore.pushSnapshot(snapshot, context);
13614
- },
13615
- async pullSnapshot(options2, context) {
13616
- const snapshot = await syncStore.pullSnapshot(options2);
13617
- return importSnapshot(snapshot, context);
13618
- },
13619
- async syncOnce(options2, context) {
13620
- const pulled = await remote.pullSnapshot(options2, context);
13621
- const pushed = await remote.pushSnapshot(context);
13622
- return { pulled, pushed };
13623
- }
13624
- };
13625
- return {
13626
- ...local,
13627
- kind: "hybrid",
13628
- capabilities: {
13629
- ...local.capabilities,
13630
- localPersistence: true,
13631
- remotePersistence: true,
13632
- sync: true
13633
- },
13634
- sync: {
13635
- ...local.sync,
13636
- exportSnapshot,
13637
- importSnapshot
13638
- },
13639
- remote
13640
- };
13641
- }
13642
-
13643
13936
  // src/storage/postgres-adapter.ts
13644
- init_types();
13645
- import { randomUUID as randomUUID3 } from "crypto";
13646
13937
  init_redaction();
13647
13938
  function createPostgresTodosStorageAdapter(options) {
13648
13939
  const store = new PostgresJsonRecordStore(options);
@@ -13770,6 +14061,9 @@ function createPostgresTodosStorageAdapter(options) {
13770
14061
  exportSnapshot: () => exportSnapshot(store),
13771
14062
  importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
13772
14063
  },
14064
+ integrity: {
14065
+ report: () => store.integrityReport()
14066
+ },
13773
14067
  transaction: (fn) => fn(adapter)
13774
14068
  };
13775
14069
  return adapter;
@@ -13876,8 +14170,9 @@ class PostgresJsonRecordStore {
13876
14170
  conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
13877
14171
  if (filter.session_id !== undefined)
13878
14172
  conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
13879
- if (filter.tags?.length)
13880
- conds.push(`payload->'tags' @> ${p(filter.tags)}::jsonb`);
14173
+ if (filter.tags?.length) {
14174
+ conds.push(`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(payload->'tags', '[]'::jsonb)) AS task_tags(tag) ` + `WHERE ${inClause("task_tags.tag", filter.tags)})`);
14175
+ }
13881
14176
  if (filter.has_recurrence !== undefined) {
13882
14177
  conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
13883
14178
  }
@@ -13938,7 +14233,8 @@ class PostgresJsonRecordStore {
13938
14233
  AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
13939
14234
  LIMIT 2`, [this.service, raw, upper]);
13940
14235
  if (prefixResult.rows.length > 1) {
13941
- throw new Error(`Task reference is ambiguous: "${ref}"`);
14236
+ const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
14237
+ throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
13942
14238
  }
13943
14239
  if (prefixResult.rows.length === 1) {
13944
14240
  return payloadRecord2(prefixResult.rows[0].payload);
@@ -13949,7 +14245,8 @@ class PostgresJsonRecordStore {
13949
14245
  AND LOWER(payload->>'short_id') = $2
13950
14246
  LIMIT 2`, [this.service, raw]);
13951
14247
  if (shortIdResult.rows.length > 1) {
13952
- throw new Error(`Task reference is ambiguous: "${ref}"`);
14248
+ const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
14249
+ throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
13953
14250
  }
13954
14251
  if (shortIdResult.rows.length === 1) {
13955
14252
  return payloadRecord2(shortIdResult.rows[0].payload);
@@ -13963,6 +14260,24 @@ class PostgresJsonRecordStore {
13963
14260
  const result = await this.options.client.query(sql, params);
13964
14261
  return Number(result.rows[0]?.count ?? 0);
13965
14262
  }
14263
+ async integrityReport() {
14264
+ await this.ensureSchema();
14265
+ const conditions = [];
14266
+ for (const spec of INTEGRITY_CONDITIONS) {
14267
+ const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
14268
+ try {
14269
+ const result = await this.options.client.query(sql, params);
14270
+ const row = result.rows[0];
14271
+ conditions.push(measuredCondition(spec, {
14272
+ count: Number(row?.count ?? 0),
14273
+ open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
14274
+ }, "postgres"));
14275
+ } catch (error) {
14276
+ conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
14277
+ }
14278
+ }
14279
+ return buildIntegrityReport(conditions, new Date().toISOString());
14280
+ }
13966
14281
  async listTombstones() {
13967
14282
  await this.ensureSchema();
13968
14283
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
@@ -14438,7 +14753,7 @@ async function startTask2(id, agentId, store) {
14438
14753
  if (task.status !== "pending" && task.status !== "in_progress") {
14439
14754
  throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
14440
14755
  }
14441
- return patchTask(task, {
14756
+ const started = await patchTask(task, {
14442
14757
  status: "in_progress",
14443
14758
  assigned_to: task.assigned_to ?? agentId,
14444
14759
  agent_id: task.agent_id ?? agentId,
@@ -14446,6 +14761,8 @@ async function startTask2(id, agentId, store) {
14446
14761
  locked_at: new Date().toISOString(),
14447
14762
  started_at: task.started_at ?? new Date().toISOString()
14448
14763
  }, store);
14764
+ await logTaskChange2(task.id, "start", "status", task.status, "in_progress", agentId, store);
14765
+ return started;
14449
14766
  }
14450
14767
  async function completeTask2(id, agentId, options, store) {
14451
14768
  const task = await store.completeTask(id, agentId, options);
@@ -15554,17 +15871,17 @@ function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
15554
15871
  init_config();
15555
15872
  function createTodosStorageAdapter(options = {}) {
15556
15873
  const config = options.config ?? loadTodosStorageConfig(options.env);
15557
- if (config.mode === "local") {
15874
+ if (config.mode === "sqlite") {
15558
15875
  if (isTodosShadowEnabled(options.env))
15559
15876
  return createShadowAdapter(options, config);
15560
15877
  return createLocalSqliteTodosStorageAdapter(options.local);
15561
15878
  }
15562
15879
  assertTodosRemoteStorageConfig(config);
15563
- const adapter = config.mode === "hybrid" ? options.hybridAdapter ?? createImplicitHybridAdapter(options) ?? options.remoteAdapter : options.remoteAdapter ?? createImplicitPostgresAdapter(options);
15880
+ const adapter = options.remoteAdapter ?? createImplicitPostgresAdapter(options);
15564
15881
  if (!adapter) {
15565
- throw new Error(`${config.mode} storage requires a repo-native remote adapter. ` + "Pass remoteAdapter/hybridAdapter after wiring Postgres RDS and S3 support.");
15882
+ throw new Error("postgres storage requires a repo-native Postgres adapter. " + "Pass remoteAdapter or postgresClient after wiring Postgres RDS support.");
15566
15883
  }
15567
- assertRemoteAdapterCapabilities(adapter, config.mode);
15884
+ assertPostgresAdapterCapabilities(adapter);
15568
15885
  return adapter;
15569
15886
  }
15570
15887
  function createShadowAdapter(options, config) {
@@ -15578,7 +15895,7 @@ function createShadowAdapter(options, config) {
15578
15895
  ...options.local ? { local: options.local } : {},
15579
15896
  ...options.postgresSyncStore ? { syncStore: options.postgresSyncStore } : {},
15580
15897
  ...postgresClient ? { postgresClient } : {},
15581
- ...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
15898
+ ...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
15582
15899
  });
15583
15900
  }
15584
15901
  function createImplicitPostgresAdapter(options) {
@@ -15586,25 +15903,12 @@ function createImplicitPostgresAdapter(options) {
15586
15903
  return null;
15587
15904
  return createPostgresTodosStorageAdapter({
15588
15905
  client: options.postgresClient,
15589
- ...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
15590
- });
15591
- }
15592
- function createImplicitHybridAdapter(options) {
15593
- if (!options.postgresClient && !options.postgresSyncStore)
15594
- return null;
15595
- return createHybridTodosStorageAdapter({
15596
- ...options.hybrid ?? {},
15597
- local: options.local,
15598
- postgresClient: options.postgresClient,
15599
- syncStore: options.postgresSyncStore
15906
+ ...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
15600
15907
  });
15601
15908
  }
15602
- function assertRemoteAdapterCapabilities(adapter, mode) {
15909
+ function assertPostgresAdapterCapabilities(adapter) {
15603
15910
  if (!adapter.capabilities.remotePersistence) {
15604
- throw new Error(`${mode} storage adapter must set capabilities.remotePersistence=true`);
15605
- }
15606
- if (mode === "hybrid" && !adapter.capabilities.localPersistence) {
15607
- throw new Error("hybrid storage adapter must also set capabilities.localPersistence=true");
15911
+ throw new Error("postgres storage adapter must set capabilities.remotePersistence=true");
15608
15912
  }
15609
15913
  }
15610
15914
  // src/storage/shadow-outbox.ts
@@ -15931,6 +16235,50 @@ async function closeRuntimeShadowCloud() {
15931
16235
  await cloud.close().catch(() => {});
15932
16236
  }
15933
16237
  }
16238
+ // src/storage/hybrid.ts
16239
+ function createHybridTodosStorageAdapter(options) {
16240
+ const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
16241
+ const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
16242
+ if (!syncStore)
16243
+ throw new Error("hybrid storage requires a Postgres sync store or query client");
16244
+ if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
16245
+ throw new Error("hybrid storage requires local snapshot export/import support");
16246
+ }
16247
+ const exportSnapshot2 = async (context) => await local.sync.exportSnapshot(context);
16248
+ const importSnapshot2 = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
16249
+ const remote = {
16250
+ ensureSchema: () => syncStore.ensureSchema(),
16251
+ async pushSnapshot(context) {
16252
+ const snapshot = await exportSnapshot2(context);
16253
+ return syncStore.pushSnapshot(snapshot, context);
16254
+ },
16255
+ async pullSnapshot(options2, context) {
16256
+ const snapshot = await syncStore.pullSnapshot(options2);
16257
+ return importSnapshot2(snapshot, context);
16258
+ },
16259
+ async syncOnce(options2, context) {
16260
+ const pulled = await remote.pullSnapshot(options2, context);
16261
+ const pushed = await remote.pushSnapshot(context);
16262
+ return { pulled, pushed };
16263
+ }
16264
+ };
16265
+ return {
16266
+ ...local,
16267
+ kind: "hybrid",
16268
+ capabilities: {
16269
+ ...local.capabilities,
16270
+ localPersistence: true,
16271
+ remotePersistence: true,
16272
+ sync: true
16273
+ },
16274
+ sync: {
16275
+ ...local.sync,
16276
+ exportSnapshot: exportSnapshot2,
16277
+ importSnapshot: importSnapshot2
16278
+ },
16279
+ remote
16280
+ };
16281
+ }
15934
16282
  // src/storage/comment-redaction-backfill.ts
15935
16283
  init_redaction();
15936
16284
  var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
@@ -16465,11 +16813,13 @@ export {
16465
16813
  postgresTodosCommentCursorIndexSql,
16466
16814
  planRunArtifactsS3Sync,
16467
16815
  parseStorageMode,
16816
+ parseStorageBackend,
16468
16817
  maybeInstallShadowCapture2 as maybeInstallShadowCapture,
16469
16818
  loadTodosStorageConfig,
16470
16819
  loadStorageConfig,
16471
16820
  isTodosShadowEnabled,
16472
16821
  isTodosRemoteStorageEnabled,
16822
+ isTodosPostgresBackend,
16473
16823
  isCommentRedactionBackfillComplete,
16474
16824
  installShadowOutboxSchema,
16475
16825
  importSqliteTodosStorageSnapshot,
@@ -16478,6 +16828,7 @@ export {
16478
16828
  getTodosStorageEnvName,
16479
16829
  getTodosStorageDatabaseUrl,
16480
16830
  getTodosStorageDatabaseEnv,
16831
+ getTodosStorageBackend,
16481
16832
  getStorageMode,
16482
16833
  getStorageDatabaseUrl,
16483
16834
  getStorageDatabaseEnv,