@hasna/todos 0.13.2 → 0.13.4

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 (74) hide show
  1. package/README.md +78 -14
  2. package/dist/cli/cloud-router.d.ts +100 -7
  3. package/dist/cli/cloud-router.d.ts.map +1 -1
  4. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  6. package/dist/cli/doctor-integrity.d.ts +106 -0
  7. package/dist/cli/doctor-integrity.d.ts.map +1 -0
  8. package/dist/cli/helpers.d.ts +2 -2
  9. package/dist/cli/index.js +1509 -786
  10. package/dist/cli/stage-a.d.ts.map +1 -1
  11. package/dist/contracts.js +118 -30
  12. package/dist/db/agent-names.d.ts +2 -1
  13. package/dist/db/agent-names.d.ts.map +1 -1
  14. package/dist/db/database.d.ts.map +1 -1
  15. package/dist/db/findings.d.ts +1 -0
  16. package/dist/db/findings.d.ts.map +1 -1
  17. package/dist/db/integrity.d.ts +20 -0
  18. package/dist/db/integrity.d.ts.map +1 -0
  19. package/dist/index.d.ts +5 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +584 -140
  22. package/dist/lib/agent-name-normalize.d.ts +17 -0
  23. package/dist/lib/agent-name-normalize.d.ts.map +1 -0
  24. package/dist/lib/context-packs.d.ts +1 -0
  25. package/dist/lib/context-packs.d.ts.map +1 -1
  26. package/dist/lib/dependency-graph.d.ts +1 -1
  27. package/dist/lib/doctor.d.ts +20 -0
  28. package/dist/lib/doctor.d.ts.map +1 -1
  29. package/dist/lib/integrity.d.ts +215 -0
  30. package/dist/lib/integrity.d.ts.map +1 -0
  31. package/dist/lib/native-storage-status.d.ts +2 -2
  32. package/dist/lib/native-storage-status.d.ts.map +1 -1
  33. package/dist/lib/release-compatibility.d.ts.map +1 -1
  34. package/dist/lib/task-dedupe.d.ts +15 -0
  35. package/dist/lib/task-dedupe.d.ts.map +1 -1
  36. package/dist/mcp/index.d.ts.map +1 -1
  37. package/dist/mcp/index.js +554 -92
  38. package/dist/mcp.js +7 -3
  39. package/dist/registry.d.ts +1 -1
  40. package/dist/registry.d.ts.map +1 -1
  41. package/dist/registry.js +125 -30
  42. package/dist/release-provenance.json +5 -5
  43. package/dist/sdk/types.d.ts +42 -0
  44. package/dist/sdk/types.d.ts.map +1 -1
  45. package/dist/sdk/v1.generated.d.ts +1 -0
  46. package/dist/sdk/v1.generated.d.ts.map +1 -1
  47. package/dist/server/cloud.d.ts +8 -1
  48. package/dist/server/cloud.d.ts.map +1 -1
  49. package/dist/server/index.js +560 -98
  50. package/dist/server/openapi.d.ts +12 -0
  51. package/dist/server/openapi.d.ts.map +1 -1
  52. package/dist/server/serve.d.ts.map +1 -1
  53. package/dist/server/v1.d.ts.map +1 -1
  54. package/dist/storage/config.d.ts +23 -5
  55. package/dist/storage/config.d.ts.map +1 -1
  56. package/dist/storage/factory.d.ts +9 -3
  57. package/dist/storage/factory.d.ts.map +1 -1
  58. package/dist/storage/hybrid.d.ts +7 -0
  59. package/dist/storage/hybrid.d.ts.map +1 -1
  60. package/dist/storage/index.d.ts +3 -3
  61. package/dist/storage/index.d.ts.map +1 -1
  62. package/dist/storage/interfaces.d.ts +29 -8
  63. package/dist/storage/interfaces.d.ts.map +1 -1
  64. package/dist/storage/local-sqlite.d.ts.map +1 -1
  65. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  66. package/dist/storage.d.ts +3 -3
  67. package/dist/storage.d.ts.map +1 -1
  68. package/dist/storage.js +623 -258
  69. package/dist/testing.d.ts +79 -0
  70. package/dist/testing.d.ts.map +1 -0
  71. package/dist/testing.js +107 -0
  72. package/dist/types/index.d.ts +12 -1
  73. package/dist/types/index.d.ts.map +1 -1
  74. 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) {
@@ -11999,6 +12069,11 @@ init_database();
11999
12069
  init_identity_mapping();
12000
12070
  init_storage_tombstones();
12001
12071
 
12072
+ // src/lib/agent-name-normalize.ts
12073
+ function normalizeAgentNameInput(name) {
12074
+ return name.trim().toLowerCase();
12075
+ }
12076
+
12002
12077
  // src/db/agent-names.ts
12003
12078
  class InvalidAgentNameError extends Error {
12004
12079
  suggestions;
@@ -12143,9 +12218,6 @@ var RESERVED_GENERIC_NAMES = new Set([
12143
12218
  ]);
12144
12219
  var NUMERIC_SUFFIX_RE = /[-_]\d+$/;
12145
12220
  var ONE_WORD_NAME_RE = /^[a-z]+$/;
12146
- function normalizeAgentNameInput(name) {
12147
- return name.trim().toLowerCase();
12148
- }
12149
12221
  function hasGeneratedNumericSuffix(name) {
12150
12222
  return NUMERIC_SUFFIX_RE.test(normalizeAgentNameInput(name));
12151
12223
  }
@@ -12650,6 +12722,267 @@ init_audit();
12650
12722
  init_comments();
12651
12723
  init_database();
12652
12724
 
12725
+ // src/db/integrity.ts
12726
+ init_database();
12727
+
12728
+ // src/lib/integrity.ts
12729
+ init_types();
12730
+ var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1";
12731
+ var TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
12732
+ var OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
12733
+ var INTEGRITY_CONDITIONS = [
12734
+ {
12735
+ id: "tasks_without_project",
12736
+ entity: "task",
12737
+ field: "project_id",
12738
+ target: "project",
12739
+ kind: "missing",
12740
+ base_severity: "warn",
12741
+ escalate_when_open: true,
12742
+ impact: "invisible to every project-scoped read (list, status, next, claim)"
12743
+ },
12744
+ {
12745
+ id: "tasks_without_task_list",
12746
+ entity: "task",
12747
+ field: "task_list_id",
12748
+ target: "task_list",
12749
+ kind: "missing",
12750
+ base_severity: "warn",
12751
+ escalate_when_open: true,
12752
+ impact: "invisible to every task-list read \u2014 the list reports zero open work"
12753
+ },
12754
+ {
12755
+ id: "tasks_with_unregistered_project",
12756
+ entity: "task",
12757
+ field: "project_id",
12758
+ target: "project",
12759
+ kind: "dangling",
12760
+ base_severity: "error",
12761
+ escalate_when_open: false,
12762
+ impact: "points at a project id that does not exist; the reference can never resolve"
12763
+ },
12764
+ {
12765
+ id: "tasks_with_unregistered_task_list",
12766
+ entity: "task",
12767
+ field: "task_list_id",
12768
+ target: "task_list",
12769
+ kind: "dangling",
12770
+ base_severity: "error",
12771
+ escalate_when_open: false,
12772
+ impact: "points at a task-list id that does not exist; the reference can never resolve"
12773
+ },
12774
+ {
12775
+ id: "task_lists_without_project",
12776
+ entity: "task_list",
12777
+ field: "project_id",
12778
+ target: "project",
12779
+ kind: "missing",
12780
+ base_severity: "warn",
12781
+ escalate_when_open: false,
12782
+ impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
12783
+ },
12784
+ {
12785
+ id: "task_lists_with_unregistered_project",
12786
+ entity: "task_list",
12787
+ field: "project_id",
12788
+ target: "project",
12789
+ kind: "dangling",
12790
+ base_severity: "error",
12791
+ escalate_when_open: false,
12792
+ impact: "points at a project id that does not exist; the list can never be reached"
12793
+ }
12794
+ ];
12795
+ function integrityCondition(id) {
12796
+ const spec = INTEGRITY_CONDITIONS.find((condition) => condition.id === id);
12797
+ if (!spec)
12798
+ throw new Error(`unknown integrity condition: ${id}`);
12799
+ return spec;
12800
+ }
12801
+ function resolveIntegritySeverity(spec, measurement) {
12802
+ if (spec.kind === "dangling")
12803
+ return "error";
12804
+ if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
12805
+ return "error";
12806
+ return spec.base_severity;
12807
+ }
12808
+ var ENTITY_LABEL = {
12809
+ task: ["task", "tasks"],
12810
+ task_list: ["task list", "task lists"]
12811
+ };
12812
+ function plural(entity, count) {
12813
+ const [one, many] = ENTITY_LABEL[entity];
12814
+ return count === 1 ? one : many;
12815
+ }
12816
+ function formatIntegrityMessage(spec, measurement) {
12817
+ const noun = plural(spec.entity, measurement.count);
12818
+ const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
12819
+ const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
12820
+ 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}`;
12821
+ }
12822
+ function measuredCondition(spec, measurement, source) {
12823
+ return {
12824
+ id: spec.id,
12825
+ entity: spec.entity,
12826
+ field: spec.field,
12827
+ kind: spec.kind,
12828
+ count: measurement.count,
12829
+ open_count: measurement.open_count,
12830
+ severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
12831
+ verified: true,
12832
+ source,
12833
+ message: formatIntegrityMessage(spec, measurement),
12834
+ impact: spec.impact
12835
+ };
12836
+ }
12837
+ function unverifiedCondition(spec, reason) {
12838
+ return {
12839
+ id: spec.id,
12840
+ entity: spec.entity,
12841
+ field: spec.field,
12842
+ kind: spec.kind,
12843
+ count: null,
12844
+ open_count: null,
12845
+ severity: null,
12846
+ verified: false,
12847
+ source: "unverified",
12848
+ unverified_reason: reason,
12849
+ message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
12850
+ impact: spec.impact
12851
+ };
12852
+ }
12853
+ function summarizeIntegrity(conditions) {
12854
+ const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
12855
+ const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
12856
+ const unverified = conditions.length - measured.length;
12857
+ return {
12858
+ ok: unverified === 0 && findings.length === 0,
12859
+ findings: findings.length,
12860
+ rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
12861
+ errors: findings.filter((condition) => condition.severity === "error").length,
12862
+ warnings: findings.filter((condition) => condition.severity === "warn").length,
12863
+ unverified,
12864
+ complete: unverified === 0
12865
+ };
12866
+ }
12867
+ function buildIntegrityReport(conditions, generatedAt) {
12868
+ const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source) => source !== "unverified");
12869
+ return {
12870
+ schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
12871
+ generated_at: generatedAt,
12872
+ source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
12873
+ conditions,
12874
+ summary: summarizeIntegrity(conditions)
12875
+ };
12876
+ }
12877
+ function adoptRemoteIntegrityReport(raw, fallbackGeneratedAt) {
12878
+ const received = new Map;
12879
+ if (Array.isArray(raw.conditions)) {
12880
+ for (const entry of raw.conditions) {
12881
+ if (entry && typeof entry["id"] === "string")
12882
+ received.set(entry["id"], entry);
12883
+ }
12884
+ }
12885
+ const conditions = INTEGRITY_CONDITIONS.map((spec) => {
12886
+ const entry = received.get(spec.id);
12887
+ const count = entry?.["count"];
12888
+ const verified = entry?.["verified"];
12889
+ if (!entry || verified === false || typeof count !== "number" || !Number.isFinite(count)) {
12890
+ 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");
12891
+ }
12892
+ const openRaw = entry["open_count"];
12893
+ return measuredCondition(spec, {
12894
+ count,
12895
+ open_count: spec.entity === "task" ? typeof openRaw === "number" && Number.isFinite(openRaw) ? openRaw : 0 : null
12896
+ }, typeof raw.source === "string" && raw.source === "postgres" ? "postgres" : "remote-authority");
12897
+ });
12898
+ const report = buildIntegrityReport(conditions, typeof raw.generated_at === "string" ? raw.generated_at : fallbackGeneratedAt);
12899
+ return typeof raw.source === "string" && (raw.source === "sqlite" || raw.source === "postgres") ? { ...report, source: raw.source } : report;
12900
+ }
12901
+ var SQLITE_TABLE = {
12902
+ task: "tasks",
12903
+ task_list: "task_lists",
12904
+ project: "projects"
12905
+ };
12906
+ function sqliteOpenStatusList() {
12907
+ return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
12908
+ }
12909
+ function buildSqliteIntegritySql(spec) {
12910
+ const table = SQLITE_TABLE[spec.entity];
12911
+ const target = SQLITE_TABLE[spec.target];
12912
+ const column = `t."${spec.field}"`;
12913
+ 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})`;
12914
+ const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
12915
+ return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
12916
+ }
12917
+ var RECORD_TYPE = {
12918
+ task: "tasks",
12919
+ task_list: "task_lists",
12920
+ project: "projects"
12921
+ };
12922
+ function buildPostgresIntegritySql(spec, options) {
12923
+ const params = [options.service, RECORD_TYPE[spec.entity]];
12924
+ const p = (value) => {
12925
+ params.push(value);
12926
+ return `$${params.length}`;
12927
+ };
12928
+ const column = `t.payload->>'${spec.field}'`;
12929
+ 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})`;
12930
+ const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
12931
+ 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}`;
12932
+ return { sql, params };
12933
+ }
12934
+ function referenceOf(row, field) {
12935
+ const raw = row[field];
12936
+ if (typeof raw !== "string")
12937
+ return null;
12938
+ const trimmed = raw.trim();
12939
+ return trimmed === "" ? null : trimmed;
12940
+ }
12941
+ function measureIntegrityRows(spec, sets) {
12942
+ const rows = spec.entity === "task" ? sets.tasks : sets.taskLists;
12943
+ if (!rows)
12944
+ return null;
12945
+ const registered = spec.kind === "dangling" ? spec.target === "project" ? sets.projectIds : sets.taskListIds : undefined;
12946
+ if (spec.kind === "dangling" && !registered)
12947
+ return null;
12948
+ let count = 0;
12949
+ let open = 0;
12950
+ for (const row of rows) {
12951
+ const reference = referenceOf(row, spec.field);
12952
+ const matches = spec.kind === "missing" ? reference === null : reference !== null && !registered.has(reference);
12953
+ if (!matches)
12954
+ continue;
12955
+ count++;
12956
+ if (spec.entity === "task") {
12957
+ const status = row.status;
12958
+ if (typeof status === "string" && OPEN_TASK_STATUSES.includes(status))
12959
+ open++;
12960
+ }
12961
+ }
12962
+ return { count, open_count: spec.entity === "task" ? open : null };
12963
+ }
12964
+
12965
+ // src/db/integrity.ts
12966
+ var REQUIRED_TABLES = { tasks: true, task_lists: true, projects: true };
12967
+ function tableExists(db, table) {
12968
+ return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
12969
+ }
12970
+ function scanSqliteIntegrity(db = getDatabase()) {
12971
+ const missing = Object.keys(REQUIRED_TABLES).filter((table) => !tableExists(db, table));
12972
+ const conditions = INTEGRITY_CONDITIONS.map((spec) => {
12973
+ if (missing.length > 0) {
12974
+ return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
12975
+ }
12976
+ try {
12977
+ const row = db.query(buildSqliteIntegritySql(spec)).get();
12978
+ return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
12979
+ } catch (error) {
12980
+ return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
12981
+ }
12982
+ });
12983
+ return buildIntegrityReport(conditions, now());
12984
+ }
12985
+
12653
12986
  // src/storage/sqlite-snapshot.ts
12654
12987
  init_database();
12655
12988
  init_audit();
@@ -13041,15 +13374,15 @@ function resolveTaskRefLocal(db, ref) {
13041
13374
  return null;
13042
13375
  if (TASK_UUID_RE.test(raw))
13043
13376
  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}`)}%`);
13377
+ 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
13378
  if (prefixRows.length > 1) {
13046
- throw new Error(`Task reference is ambiguous: "${ref}"`);
13379
+ throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
13047
13380
  }
13048
13381
  if (prefixRows.length === 1)
13049
13382
  return getTask(prefixRows[0].id, db);
13050
- const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
13383
+ const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
13051
13384
  if (shortIdRows.length > 1) {
13052
- throw new Error(`Task reference is ambiguous: "${ref}"`);
13385
+ throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
13053
13386
  }
13054
13387
  if (shortIdRows.length === 1)
13055
13388
  return getTask(shortIdRows[0].id, db);
@@ -13198,6 +13531,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
13198
13531
  exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
13199
13532
  importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
13200
13533
  },
13534
+ integrity: {
13535
+ report: () => scanSqliteIntegrity(database())
13536
+ },
13201
13537
  transaction: (fn) => {
13202
13538
  const tx = database().transaction(() => fn(adapter));
13203
13539
  return tx();
@@ -13206,6 +13542,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
13206
13542
  return adapter;
13207
13543
  }
13208
13544
 
13545
+ // src/storage/postgres-adapter.ts
13546
+ init_types();
13547
+ import { randomUUID as randomUUID3 } from "crypto";
13548
+
13209
13549
  // src/storage/postgres-sync.ts
13210
13550
  var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
13211
13551
  var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
@@ -13595,54 +13935,7 @@ function assertSafeIdentifier(value) {
13595
13935
  throw new Error(`Unsafe Postgres identifier: ${value}`);
13596
13936
  }
13597
13937
 
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
13938
  // src/storage/postgres-adapter.ts
13644
- init_types();
13645
- import { randomUUID as randomUUID3 } from "crypto";
13646
13939
  init_redaction();
13647
13940
  function createPostgresTodosStorageAdapter(options) {
13648
13941
  const store = new PostgresJsonRecordStore(options);
@@ -13713,7 +14006,7 @@ function createPostgresTodosStorageAdapter(options) {
13713
14006
  agents: {
13714
14007
  register: (input, context) => registerAgent2(input, store, context),
13715
14008
  get: (id) => store.get("agents", id),
13716
- getByName: async (name) => (await store.list("agents")).find((agent) => agent.name === name) ?? null,
14009
+ getByName: async (name) => matchAgentByName(await store.list("agents"), name),
13717
14010
  list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
13718
14011
  update: (id, input) => updateAgent2(id, input, store),
13719
14012
  heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
@@ -13770,6 +14063,9 @@ function createPostgresTodosStorageAdapter(options) {
13770
14063
  exportSnapshot: () => exportSnapshot(store),
13771
14064
  importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
13772
14065
  },
14066
+ integrity: {
14067
+ report: () => store.integrityReport()
14068
+ },
13773
14069
  transaction: (fn) => fn(adapter)
13774
14070
  };
13775
14071
  return adapter;
@@ -13876,8 +14172,9 @@ class PostgresJsonRecordStore {
13876
14172
  conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
13877
14173
  if (filter.session_id !== undefined)
13878
14174
  conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
13879
- if (filter.tags?.length)
13880
- conds.push(`payload->'tags' @> ${p(filter.tags)}::jsonb`);
14175
+ if (filter.tags?.length) {
14176
+ 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)})`);
14177
+ }
13881
14178
  if (filter.has_recurrence !== undefined) {
13882
14179
  conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
13883
14180
  }
@@ -13938,7 +14235,8 @@ class PostgresJsonRecordStore {
13938
14235
  AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
13939
14236
  LIMIT 2`, [this.service, raw, upper]);
13940
14237
  if (prefixResult.rows.length > 1) {
13941
- throw new Error(`Task reference is ambiguous: "${ref}"`);
14238
+ const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
14239
+ throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
13942
14240
  }
13943
14241
  if (prefixResult.rows.length === 1) {
13944
14242
  return payloadRecord2(prefixResult.rows[0].payload);
@@ -13949,7 +14247,8 @@ class PostgresJsonRecordStore {
13949
14247
  AND LOWER(payload->>'short_id') = $2
13950
14248
  LIMIT 2`, [this.service, raw]);
13951
14249
  if (shortIdResult.rows.length > 1) {
13952
- throw new Error(`Task reference is ambiguous: "${ref}"`);
14250
+ const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
14251
+ throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
13953
14252
  }
13954
14253
  if (shortIdResult.rows.length === 1) {
13955
14254
  return payloadRecord2(shortIdResult.rows[0].payload);
@@ -13963,6 +14262,24 @@ class PostgresJsonRecordStore {
13963
14262
  const result = await this.options.client.query(sql, params);
13964
14263
  return Number(result.rows[0]?.count ?? 0);
13965
14264
  }
14265
+ async integrityReport() {
14266
+ await this.ensureSchema();
14267
+ const conditions = [];
14268
+ for (const spec of INTEGRITY_CONDITIONS) {
14269
+ const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
14270
+ try {
14271
+ const result = await this.options.client.query(sql, params);
14272
+ const row = result.rows[0];
14273
+ conditions.push(measuredCondition(spec, {
14274
+ count: Number(row?.count ?? 0),
14275
+ open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
14276
+ }, "postgres"));
14277
+ } catch (error) {
14278
+ conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
14279
+ }
14280
+ }
14281
+ return buildIntegrityReport(conditions, new Date().toISOString());
14282
+ }
13966
14283
  async listTombstones() {
13967
14284
  await this.ensureSchema();
13968
14285
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
@@ -14438,7 +14755,7 @@ async function startTask2(id, agentId, store) {
14438
14755
  if (task.status !== "pending" && task.status !== "in_progress") {
14439
14756
  throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
14440
14757
  }
14441
- return patchTask(task, {
14758
+ const started = await patchTask(task, {
14442
14759
  status: "in_progress",
14443
14760
  assigned_to: task.assigned_to ?? agentId,
14444
14761
  agent_id: task.agent_id ?? agentId,
@@ -14446,6 +14763,8 @@ async function startTask2(id, agentId, store) {
14446
14763
  locked_at: new Date().toISOString(),
14447
14764
  started_at: task.started_at ?? new Date().toISOString()
14448
14765
  }, store);
14766
+ await logTaskChange2(task.id, "start", "status", task.status, "in_progress", agentId, store);
14767
+ return started;
14449
14768
  }
14450
14769
  async function completeTask2(id, agentId, options, store) {
14451
14770
  const task = await store.completeTask(id, agentId, options);
@@ -14751,15 +15070,27 @@ async function updatePlan2(id, input, store) {
14751
15070
  }
14752
15071
  return store.upsert("plans", { ...plan, ...patch, updated_at: new Date().toISOString() });
14753
15072
  }
15073
+ function matchAgentByName(agents, name, options) {
15074
+ const target = normalizeAgentNameInput(name);
15075
+ if (!target)
15076
+ return null;
15077
+ const matches = agents.filter((agent) => normalizeAgentNameInput(agent.name) === target && (options?.includeArchived !== false || agent.status !== "archived"));
15078
+ if (matches.length === 0)
15079
+ return null;
15080
+ return matches.reduce((freshest, candidate) => new Date(candidate.last_seen_at).getTime() > new Date(freshest.last_seen_at).getTime() ? candidate : freshest);
15081
+ }
14754
15082
  async function registerAgent2(input, store, context) {
14755
- const existing = (await store.list("agents")).find((agent2) => agent2.name === input.name && agent2.status !== "archived");
15083
+ const canonicalName = normalizeAgentNameInput(input.name);
15084
+ const existing = matchAgentByName(await store.list("agents"), canonicalName, {
15085
+ includeArchived: false
15086
+ });
14756
15087
  if (existing && !input.force && existing.session_id && existing.session_id !== input.session_id) {
14757
- return { conflict: true, message: `Agent name '${input.name}' is already active` };
15088
+ return { conflict: true, message: `Agent name '${canonicalName}' is already active` };
14758
15089
  }
14759
15090
  const timestamp2 = new Date().toISOString();
14760
15091
  const agent = {
14761
15092
  id: existing?.id ?? randomUUID3().slice(0, 8),
14762
- name: input.name,
15093
+ name: canonicalName,
14763
15094
  description: input.description ?? existing?.description ?? null,
14764
15095
  role: input.role ?? existing?.role ?? null,
14765
15096
  title: input.title ?? existing?.title ?? null,
@@ -14797,7 +15128,7 @@ async function resolveAgent(idOrName, store) {
14797
15128
  const byId = await store.get("agents", idOrName);
14798
15129
  if (byId)
14799
15130
  return byId;
14800
- return (await store.list("agents")).find((agent) => agent.name === idOrName) ?? null;
15131
+ return matchAgentByName(await store.list("agents"), idOrName);
14801
15132
  }
14802
15133
  async function heartbeatAgent(idOrName, store, context) {
14803
15134
  const agent = await resolveAgent(idOrName, store);
@@ -15554,17 +15885,17 @@ function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
15554
15885
  init_config();
15555
15886
  function createTodosStorageAdapter(options = {}) {
15556
15887
  const config = options.config ?? loadTodosStorageConfig(options.env);
15557
- if (config.mode === "local") {
15888
+ if (config.mode === "sqlite") {
15558
15889
  if (isTodosShadowEnabled(options.env))
15559
15890
  return createShadowAdapter(options, config);
15560
15891
  return createLocalSqliteTodosStorageAdapter(options.local);
15561
15892
  }
15562
15893
  assertTodosRemoteStorageConfig(config);
15563
- const adapter = config.mode === "hybrid" ? options.hybridAdapter ?? createImplicitHybridAdapter(options) ?? options.remoteAdapter : options.remoteAdapter ?? createImplicitPostgresAdapter(options);
15894
+ const adapter = options.remoteAdapter ?? createImplicitPostgresAdapter(options);
15564
15895
  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.");
15896
+ throw new Error("postgres storage requires a repo-native Postgres adapter. " + "Pass remoteAdapter or postgresClient after wiring Postgres RDS support.");
15566
15897
  }
15567
- assertRemoteAdapterCapabilities(adapter, config.mode);
15898
+ assertPostgresAdapterCapabilities(adapter);
15568
15899
  return adapter;
15569
15900
  }
15570
15901
  function createShadowAdapter(options, config) {
@@ -15578,7 +15909,7 @@ function createShadowAdapter(options, config) {
15578
15909
  ...options.local ? { local: options.local } : {},
15579
15910
  ...options.postgresSyncStore ? { syncStore: options.postgresSyncStore } : {},
15580
15911
  ...postgresClient ? { postgresClient } : {},
15581
- ...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
15912
+ ...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
15582
15913
  });
15583
15914
  }
15584
15915
  function createImplicitPostgresAdapter(options) {
@@ -15586,25 +15917,12 @@ function createImplicitPostgresAdapter(options) {
15586
15917
  return null;
15587
15918
  return createPostgresTodosStorageAdapter({
15588
15919
  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
15920
+ ...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
15600
15921
  });
15601
15922
  }
15602
- function assertRemoteAdapterCapabilities(adapter, mode) {
15923
+ function assertPostgresAdapterCapabilities(adapter) {
15603
15924
  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");
15925
+ throw new Error("postgres storage adapter must set capabilities.remotePersistence=true");
15608
15926
  }
15609
15927
  }
15610
15928
  // src/storage/shadow-outbox.ts
@@ -15931,6 +16249,50 @@ async function closeRuntimeShadowCloud() {
15931
16249
  await cloud.close().catch(() => {});
15932
16250
  }
15933
16251
  }
16252
+ // src/storage/hybrid.ts
16253
+ function createHybridTodosStorageAdapter(options) {
16254
+ const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
16255
+ const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
16256
+ if (!syncStore)
16257
+ throw new Error("hybrid storage requires a Postgres sync store or query client");
16258
+ if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
16259
+ throw new Error("hybrid storage requires local snapshot export/import support");
16260
+ }
16261
+ const exportSnapshot2 = async (context) => await local.sync.exportSnapshot(context);
16262
+ const importSnapshot2 = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
16263
+ const remote = {
16264
+ ensureSchema: () => syncStore.ensureSchema(),
16265
+ async pushSnapshot(context) {
16266
+ const snapshot = await exportSnapshot2(context);
16267
+ return syncStore.pushSnapshot(snapshot, context);
16268
+ },
16269
+ async pullSnapshot(options2, context) {
16270
+ const snapshot = await syncStore.pullSnapshot(options2);
16271
+ return importSnapshot2(snapshot, context);
16272
+ },
16273
+ async syncOnce(options2, context) {
16274
+ const pulled = await remote.pullSnapshot(options2, context);
16275
+ const pushed = await remote.pushSnapshot(context);
16276
+ return { pulled, pushed };
16277
+ }
16278
+ };
16279
+ return {
16280
+ ...local,
16281
+ kind: "hybrid",
16282
+ capabilities: {
16283
+ ...local.capabilities,
16284
+ localPersistence: true,
16285
+ remotePersistence: true,
16286
+ sync: true
16287
+ },
16288
+ sync: {
16289
+ ...local.sync,
16290
+ exportSnapshot: exportSnapshot2,
16291
+ importSnapshot: importSnapshot2
16292
+ },
16293
+ remote
16294
+ };
16295
+ }
15934
16296
  // src/storage/comment-redaction-backfill.ts
15935
16297
  init_redaction();
15936
16298
  var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
@@ -16465,11 +16827,13 @@ export {
16465
16827
  postgresTodosCommentCursorIndexSql,
16466
16828
  planRunArtifactsS3Sync,
16467
16829
  parseStorageMode,
16830
+ parseStorageBackend,
16468
16831
  maybeInstallShadowCapture2 as maybeInstallShadowCapture,
16469
16832
  loadTodosStorageConfig,
16470
16833
  loadStorageConfig,
16471
16834
  isTodosShadowEnabled,
16472
16835
  isTodosRemoteStorageEnabled,
16836
+ isTodosPostgresBackend,
16473
16837
  isCommentRedactionBackfillComplete,
16474
16838
  installShadowOutboxSchema,
16475
16839
  importSqliteTodosStorageSnapshot,
@@ -16478,6 +16842,7 @@ export {
16478
16842
  getTodosStorageEnvName,
16479
16843
  getTodosStorageDatabaseUrl,
16480
16844
  getTodosStorageDatabaseEnv,
16845
+ getTodosStorageBackend,
16481
16846
  getStorageMode,
16482
16847
  getStorageDatabaseUrl,
16483
16848
  getStorageDatabaseEnv,