@hasna/todos 0.13.1 → 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 (76) hide show
  1. package/README.md +80 -15
  2. package/dist/cli/cloud-router.d.ts +78 -12
  3. package/dist/cli/cloud-router.d.ts.map +1 -1
  4. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  7. package/dist/cli/doctor-integrity.d.ts +106 -0
  8. package/dist/cli/doctor-integrity.d.ts.map +1 -0
  9. package/dist/cli/helpers.d.ts +2 -2
  10. package/dist/cli/index.js +1748 -1012
  11. package/dist/cli/stage-a.d.ts.map +1 -1
  12. package/dist/contracts.js +137 -39
  13. package/dist/db/database.d.ts.map +1 -1
  14. package/dist/db/findings.d.ts +1 -0
  15. package/dist/db/findings.d.ts.map +1 -1
  16. package/dist/db/integrity.d.ts +20 -0
  17. package/dist/db/integrity.d.ts.map +1 -0
  18. package/dist/db/task-crud.d.ts.map +1 -1
  19. package/dist/index.d.ts +5 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +592 -147
  22. package/dist/lib/context-packs.d.ts +1 -0
  23. package/dist/lib/context-packs.d.ts.map +1 -1
  24. package/dist/lib/dependency-graph.d.ts +19 -11
  25. package/dist/lib/dependency-graph.d.ts.map +1 -1
  26. package/dist/lib/doctor.d.ts +20 -0
  27. package/dist/lib/doctor.d.ts.map +1 -1
  28. package/dist/lib/integrity.d.ts +215 -0
  29. package/dist/lib/integrity.d.ts.map +1 -0
  30. package/dist/lib/native-storage-status.d.ts +2 -2
  31. package/dist/lib/native-storage-status.d.ts.map +1 -1
  32. package/dist/lib/redaction.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 +544 -97
  38. package/dist/mcp.js +8 -4
  39. package/dist/registry.d.ts +1 -1
  40. package/dist/registry.d.ts.map +1 -1
  41. package/dist/registry.js +144 -39
  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 +585 -118
  50. package/dist/server/openapi.d.ts +12 -0
  51. package/dist/server/openapi.d.ts.map +1 -1
  52. package/dist/server/port.d.ts +52 -0
  53. package/dist/server/port.d.ts.map +1 -0
  54. package/dist/server/serve.d.ts.map +1 -1
  55. package/dist/server/v1.d.ts.map +1 -1
  56. package/dist/storage/config.d.ts +23 -5
  57. package/dist/storage/config.d.ts.map +1 -1
  58. package/dist/storage/factory.d.ts +9 -3
  59. package/dist/storage/factory.d.ts.map +1 -1
  60. package/dist/storage/hybrid.d.ts +7 -0
  61. package/dist/storage/hybrid.d.ts.map +1 -1
  62. package/dist/storage/index.d.ts +3 -3
  63. package/dist/storage/index.d.ts.map +1 -1
  64. package/dist/storage/interfaces.d.ts +43 -8
  65. package/dist/storage/interfaces.d.ts.map +1 -1
  66. package/dist/storage/local-sqlite.d.ts.map +1 -1
  67. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  68. package/dist/storage.d.ts +3 -3
  69. package/dist/storage.d.ts.map +1 -1
  70. package/dist/storage.js +619 -256
  71. package/dist/testing.d.ts +79 -0
  72. package/dist/testing.d.ts.map +1 -0
  73. package/dist/testing.js +107 -0
  74. package/dist/types/index.d.ts +27 -1
  75. package/dist/types/index.d.ts.map +1 -1
  76. package/package.json +8 -4
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,160 +3826,6 @@ var init_machines = __esm(() => {
3630
3826
  ];
3631
3827
  });
3632
3828
 
3633
- // src/types/index.ts
3634
- var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3635
- var init_types = __esm(() => {
3636
- TASK_STATUSES = [
3637
- "pending",
3638
- "in_progress",
3639
- "completed",
3640
- "failed",
3641
- "cancelled"
3642
- ];
3643
- TASK_PRIORITIES = [
3644
- "low",
3645
- "medium",
3646
- "high",
3647
- "critical"
3648
- ];
3649
- PLAN_STATUSES = ["active", "completed", "archived"];
3650
- VersionConflictError = class VersionConflictError extends Error {
3651
- taskId;
3652
- expectedVersion;
3653
- actualVersion;
3654
- static code = "VERSION_CONFLICT";
3655
- static suggestion = "Fetch the task with get_task to get the current version before updating.";
3656
- constructor(taskId, expectedVersion, actualVersion) {
3657
- super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
3658
- this.taskId = taskId;
3659
- this.expectedVersion = expectedVersion;
3660
- this.actualVersion = actualVersion;
3661
- this.name = "VersionConflictError";
3662
- }
3663
- };
3664
- TaskNotFoundError = class TaskNotFoundError extends Error {
3665
- taskId;
3666
- static code = "TASK_NOT_FOUND";
3667
- static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
3668
- constructor(taskId) {
3669
- super(`Task not found: ${taskId}`);
3670
- this.taskId = taskId;
3671
- this.name = "TaskNotFoundError";
3672
- }
3673
- };
3674
- ProjectNotFoundError = class ProjectNotFoundError extends Error {
3675
- projectId;
3676
- static code = "PROJECT_NOT_FOUND";
3677
- static suggestion = "Use list_projects to see available projects.";
3678
- constructor(projectId) {
3679
- super(`Project not found: ${projectId}`);
3680
- this.projectId = projectId;
3681
- this.name = "ProjectNotFoundError";
3682
- }
3683
- };
3684
- ResourceConflictError = class ResourceConflictError extends Error {
3685
- code;
3686
- constructor(code, message) {
3687
- super(message);
3688
- this.code = code;
3689
- this.name = "ResourceConflictError";
3690
- }
3691
- };
3692
- PlanNotFoundError = class PlanNotFoundError extends Error {
3693
- planId;
3694
- static code = "PLAN_NOT_FOUND";
3695
- static suggestion = "Use list_plans to see available plans.";
3696
- constructor(planId) {
3697
- super(`Plan not found: ${planId}`);
3698
- this.planId = planId;
3699
- this.name = "PlanNotFoundError";
3700
- }
3701
- };
3702
- LockError = class LockError extends Error {
3703
- taskId;
3704
- lockedBy;
3705
- static code = "LOCK_ERROR";
3706
- static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
3707
- constructor(taskId, lockedBy) {
3708
- super(`Task ${taskId} is locked by ${lockedBy}`);
3709
- this.taskId = taskId;
3710
- this.lockedBy = lockedBy;
3711
- this.name = "LockError";
3712
- }
3713
- };
3714
- AgentNotFoundError = class AgentNotFoundError extends Error {
3715
- agentId;
3716
- static code = "AGENT_NOT_FOUND";
3717
- static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
3718
- constructor(agentId) {
3719
- super(`Agent not found: ${agentId}`);
3720
- this.agentId = agentId;
3721
- this.name = "AgentNotFoundError";
3722
- }
3723
- };
3724
- IdentityAliasAmbiguousError = class IdentityAliasAmbiguousError extends Error {
3725
- code = "IDENTITY_ALIAS_AMBIGUOUS";
3726
- candidates;
3727
- constructor(subject, candidates = []) {
3728
- super(`Identity alias or source is ambiguous: ${subject}`);
3729
- this.name = "IdentityAliasAmbiguousError";
3730
- this.candidates = [...new Set(candidates)].sort();
3731
- }
3732
- };
3733
- IdentityIdImmutableError = class IdentityIdImmutableError extends Error {
3734
- code = "IDENTITY_ID_IMMUTABLE";
3735
- constructor(agentId) {
3736
- super(`IDENTITY_ID_IMMUTABLE: canonical identity for agent ${agentId} cannot be replaced`);
3737
- this.name = "IdentityIdImmutableError";
3738
- }
3739
- };
3740
- TaskListNotFoundError = class TaskListNotFoundError extends Error {
3741
- taskListId;
3742
- static code = "TASK_LIST_NOT_FOUND";
3743
- static suggestion = "Use list_task_lists to see available lists.";
3744
- constructor(taskListId) {
3745
- super(`Task list not found: ${taskListId}`);
3746
- this.taskListId = taskListId;
3747
- this.name = "TaskListNotFoundError";
3748
- }
3749
- };
3750
- DependencyCycleError = class DependencyCycleError extends Error {
3751
- taskId;
3752
- dependsOn;
3753
- static code = "DEPENDENCY_CYCLE";
3754
- static suggestion = "Check the dependency chain with get_task to avoid circular references.";
3755
- constructor(taskId, dependsOn) {
3756
- super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
3757
- this.taskId = taskId;
3758
- this.dependsOn = dependsOn;
3759
- this.name = "DependencyCycleError";
3760
- }
3761
- };
3762
- CompletionGuardError = class CompletionGuardError extends Error {
3763
- reason;
3764
- retryAfterSeconds;
3765
- static code = "COMPLETION_BLOCKED";
3766
- static suggestion = "Wait for the cooldown period, then retry.";
3767
- constructor(reason, retryAfterSeconds) {
3768
- super(reason);
3769
- this.reason = reason;
3770
- this.retryAfterSeconds = retryAfterSeconds;
3771
- this.name = "CompletionGuardError";
3772
- }
3773
- };
3774
- DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
3775
- DispatchNotFoundError = class DispatchNotFoundError extends Error {
3776
- dispatchId;
3777
- static code = "DISPATCH_NOT_FOUND";
3778
- static suggestion = "Check the dispatch ID with list_dispatches.";
3779
- constructor(dispatchId) {
3780
- super(`Dispatch not found: ${dispatchId}`);
3781
- this.dispatchId = dispatchId;
3782
- this.name = "DispatchNotFoundError";
3783
- }
3784
- };
3785
- });
3786
-
3787
3829
  // src/db/identity-mapping.ts
3788
3830
  function timestamp() {
3789
3831
  return new Date().toISOString();
@@ -4089,6 +4131,30 @@ function findGitRoot(startDir) {
4089
4131
  }
4090
4132
  return null;
4091
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
+ }
4092
4158
  function getDbPath() {
4093
4159
  if (process.env["HASNA_TODOS_DB_PATH"]) {
4094
4160
  return process.env["HASNA_TODOS_DB_PATH"];
@@ -4096,18 +4162,19 @@ function getDbPath() {
4096
4162
  if (process.env["TODOS_DB_PATH"]) {
4097
4163
  return process.env["TODOS_DB_PATH"];
4098
4164
  }
4165
+ if (hasExplicitProjectArg())
4166
+ return getGlobalDbPath();
4099
4167
  const cwd = process.cwd();
4100
4168
  const nearest = findNearestProjectDb(cwd);
4101
4169
  if (nearest)
4102
4170
  return nearest;
4103
4171
  if (process.env["TODOS_DB_SCOPE"] === "project") {
4104
4172
  const gitRoot = findGitRoot(cwd);
4105
- if (gitRoot) {
4173
+ if (gitRoot && canCreateScopedProjectDb()) {
4106
4174
  return join(gitRoot, ".hasna", "todos", "todos.db");
4107
4175
  }
4108
4176
  }
4109
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4110
- return join(home, ".hasna", "todos", "todos.db");
4177
+ return getGlobalDbPath();
4111
4178
  }
4112
4179
  function getDatabasePath() {
4113
4180
  return getDbPath();
@@ -4123,8 +4190,8 @@ function ensureDir(filePath) {
4123
4190
  function openDatabase(path) {
4124
4191
  ensureDir(path);
4125
4192
  const db = new Database(path);
4126
- db.run("PRAGMA journal_mode = WAL");
4127
4193
  db.run("PRAGMA busy_timeout = 5000");
4194
+ db.run("PRAGMA journal_mode = WAL");
4128
4195
  db.run("PRAGMA foreign_keys = ON");
4129
4196
  runMigrations(db);
4130
4197
  ensureAgentIdentitySchema(db);
@@ -4210,10 +4277,13 @@ function resolvePartialId(db, table, partialId) {
4210
4277
  return null;
4211
4278
  }
4212
4279
  if (table === "tasks") {
4213
- 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);
4214
4281
  if (shortIdRows.length === 1) {
4215
4282
  return shortIdRows[0].id;
4216
4283
  }
4284
+ if (shortIdRows.length > 1) {
4285
+ throw new TaskReferenceAmbiguousError(partialId, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
4286
+ }
4217
4287
  }
4218
4288
  if (table === "task_lists") {
4219
4289
  const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
@@ -4955,11 +5025,15 @@ function secretPatterns() {
4955
5025
  return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
4956
5026
  }
4957
5027
  function isRedactionPlaceholderMatch(match) {
4958
- const placeholder = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
4959
5028
  const trimmed = match.trim();
4960
- return new RegExp(`^${placeholder}$`).test(trimmed) || new RegExp(`=\\s*['"]?${placeholder}['"]?$`).test(trimmed);
5029
+ return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(trimmed) || new RegExp(`=\\s*['"]?${REDACTION_PLACEHOLDER}['"]?$`).test(trimmed);
5030
+ }
5031
+ function isRedactionPlaceholderKey(key) {
5032
+ return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(key.trim());
4961
5033
  }
4962
5034
  function isSecretKey(key) {
5035
+ if (isRedactionPlaceholderKey(key))
5036
+ return false;
4963
5037
  if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
4964
5038
  return false;
4965
5039
  if (DEFAULT_SECRET_KEY_PATTERN.test(key))
@@ -5020,7 +5094,7 @@ function upsertSecretSafetyConfig(input) {
5020
5094
  saveConfig({ ...config, secret_safety: next });
5021
5095
  return next;
5022
5096
  }
5023
- var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS;
5097
+ var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
5024
5098
  var init_redaction = __esm(() => {
5025
5099
  init_config2();
5026
5100
  DEFAULT_SECRET_PATTERNS = [
@@ -5044,6 +5118,7 @@ var init_redaction = __esm(() => {
5044
5118
  "completion_tokens",
5045
5119
  "cost_tokens"
5046
5120
  ]);
5121
+ REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
5047
5122
  });
5048
5123
 
5049
5124
  // src/lib/workspace-trust.ts
@@ -6683,7 +6758,7 @@ function scanTextForSecrets(text, options = {}) {
6683
6758
  };
6684
6759
  }
6685
6760
  function redactText(text, options = {}) {
6686
- const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER;
6761
+ const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER2;
6687
6762
  let out = text;
6688
6763
  for (const { pattern, allowlist_ok } of DEFAULT_PATTERNS) {
6689
6764
  out = out.replace(new RegExp(pattern.source, pattern.flags), (match) => {
@@ -6744,7 +6819,7 @@ function redactExportRecord(record) {
6744
6819
  function getDefaultSecretPatterns() {
6745
6820
  return DEFAULT_PATTERNS.map((p) => ({ name: p.name, source: p.pattern.source }));
6746
6821
  }
6747
- var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
6822
+ var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER2 = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
6748
6823
  var init_secret_redaction = __esm(() => {
6749
6824
  init_redaction();
6750
6825
  SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
@@ -8801,10 +8876,11 @@ function getTaskWithRelations(id, db) {
8801
8876
  JOIN task_dependencies td ON td.depends_on = t.id
8802
8877
  WHERE td.task_id = ?`).all(id);
8803
8878
  const dependencies = depRows.map(rowToTask2);
8804
- const blockedByRows = d.query(`SELECT t.* FROM tasks t
8879
+ const blocked_by = dependencies.filter((dep) => isBlockingDependencyStatus(dep.status));
8880
+ const blocksRows = d.query(`SELECT t.* FROM tasks t
8805
8881
  JOIN task_dependencies td ON td.task_id = t.id
8806
8882
  WHERE td.depends_on = ?`).all(id);
8807
- const blocked_by = blockedByRows.map(rowToTask2);
8883
+ const blocks = blocksRows.map(rowToTask2);
8808
8884
  const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
8809
8885
  const parent = task.parent_id ? getTask(task.parent_id, d) : null;
8810
8886
  const checklist = getChecklist(id, d);
@@ -8813,6 +8889,7 @@ function getTaskWithRelations(id, db) {
8813
8889
  subtasks,
8814
8890
  dependencies,
8815
8891
  blocked_by,
8892
+ blocks,
8816
8893
  comments,
8817
8894
  parent,
8818
8895
  checklist
@@ -11824,6 +11901,9 @@ var init_tasks = __esm(() => {
11824
11901
  // src/storage/index.ts
11825
11902
  init_config();
11826
11903
 
11904
+ // src/storage/local-sqlite.ts
11905
+ init_types();
11906
+
11827
11907
  // src/lib/search.ts
11828
11908
  init_database();
11829
11909
  function rowToTask(row) {
@@ -12640,6 +12720,267 @@ init_audit();
12640
12720
  init_comments();
12641
12721
  init_database();
12642
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
+
12643
12984
  // src/storage/sqlite-snapshot.ts
12644
12985
  init_database();
12645
12986
  init_audit();
@@ -13031,15 +13372,15 @@ function resolveTaskRefLocal(db, ref) {
13031
13372
  return null;
13032
13373
  if (TASK_UUID_RE.test(raw))
13033
13374
  return getTask(raw, db);
13034
- 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}`)}%`);
13035
13376
  if (prefixRows.length > 1) {
13036
- 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 })));
13037
13378
  }
13038
13379
  if (prefixRows.length === 1)
13039
13380
  return getTask(prefixRows[0].id, db);
13040
- 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);
13041
13382
  if (shortIdRows.length > 1) {
13042
- 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 })));
13043
13384
  }
13044
13385
  if (shortIdRows.length === 1)
13045
13386
  return getTask(shortIdRows[0].id, db);
@@ -13188,6 +13529,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
13188
13529
  exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
13189
13530
  importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
13190
13531
  },
13532
+ integrity: {
13533
+ report: () => scanSqliteIntegrity(database())
13534
+ },
13191
13535
  transaction: (fn) => {
13192
13536
  const tx = database().transaction(() => fn(adapter));
13193
13537
  return tx();
@@ -13196,6 +13540,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
13196
13540
  return adapter;
13197
13541
  }
13198
13542
 
13543
+ // src/storage/postgres-adapter.ts
13544
+ init_types();
13545
+ import { randomUUID as randomUUID3 } from "crypto";
13546
+
13199
13547
  // src/storage/postgres-sync.ts
13200
13548
  var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
13201
13549
  var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
@@ -13585,54 +13933,7 @@ function assertSafeIdentifier(value) {
13585
13933
  throw new Error(`Unsafe Postgres identifier: ${value}`);
13586
13934
  }
13587
13935
 
13588
- // src/storage/hybrid.ts
13589
- function createHybridTodosStorageAdapter(options) {
13590
- const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
13591
- const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
13592
- if (!syncStore)
13593
- throw new Error("hybrid storage requires a Postgres sync store or query client");
13594
- if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
13595
- throw new Error("hybrid storage requires local snapshot export/import support");
13596
- }
13597
- const exportSnapshot = async (context) => await local.sync.exportSnapshot(context);
13598
- const importSnapshot = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
13599
- const remote = {
13600
- ensureSchema: () => syncStore.ensureSchema(),
13601
- async pushSnapshot(context) {
13602
- const snapshot = await exportSnapshot(context);
13603
- return syncStore.pushSnapshot(snapshot, context);
13604
- },
13605
- async pullSnapshot(options2, context) {
13606
- const snapshot = await syncStore.pullSnapshot(options2);
13607
- return importSnapshot(snapshot, context);
13608
- },
13609
- async syncOnce(options2, context) {
13610
- const pulled = await remote.pullSnapshot(options2, context);
13611
- const pushed = await remote.pushSnapshot(context);
13612
- return { pulled, pushed };
13613
- }
13614
- };
13615
- return {
13616
- ...local,
13617
- kind: "hybrid",
13618
- capabilities: {
13619
- ...local.capabilities,
13620
- localPersistence: true,
13621
- remotePersistence: true,
13622
- sync: true
13623
- },
13624
- sync: {
13625
- ...local.sync,
13626
- exportSnapshot,
13627
- importSnapshot
13628
- },
13629
- remote
13630
- };
13631
- }
13632
-
13633
13936
  // src/storage/postgres-adapter.ts
13634
- init_types();
13635
- import { randomUUID as randomUUID3 } from "crypto";
13636
13937
  init_redaction();
13637
13938
  function createPostgresTodosStorageAdapter(options) {
13638
13939
  const store = new PostgresJsonRecordStore(options);
@@ -13760,6 +14061,9 @@ function createPostgresTodosStorageAdapter(options) {
13760
14061
  exportSnapshot: () => exportSnapshot(store),
13761
14062
  importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
13762
14063
  },
14064
+ integrity: {
14065
+ report: () => store.integrityReport()
14066
+ },
13763
14067
  transaction: (fn) => fn(adapter)
13764
14068
  };
13765
14069
  return adapter;
@@ -13866,8 +14170,9 @@ class PostgresJsonRecordStore {
13866
14170
  conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
13867
14171
  if (filter.session_id !== undefined)
13868
14172
  conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
13869
- if (filter.tags?.length)
13870
- 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
+ }
13871
14176
  if (filter.has_recurrence !== undefined) {
13872
14177
  conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
13873
14178
  }
@@ -13928,7 +14233,8 @@ class PostgresJsonRecordStore {
13928
14233
  AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
13929
14234
  LIMIT 2`, [this.service, raw, upper]);
13930
14235
  if (prefixResult.rows.length > 1) {
13931
- 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 })));
13932
14238
  }
13933
14239
  if (prefixResult.rows.length === 1) {
13934
14240
  return payloadRecord2(prefixResult.rows[0].payload);
@@ -13939,7 +14245,8 @@ class PostgresJsonRecordStore {
13939
14245
  AND LOWER(payload->>'short_id') = $2
13940
14246
  LIMIT 2`, [this.service, raw]);
13941
14247
  if (shortIdResult.rows.length > 1) {
13942
- 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 })));
13943
14250
  }
13944
14251
  if (shortIdResult.rows.length === 1) {
13945
14252
  return payloadRecord2(shortIdResult.rows[0].payload);
@@ -13953,6 +14260,24 @@ class PostgresJsonRecordStore {
13953
14260
  const result = await this.options.client.query(sql, params);
13954
14261
  return Number(result.rows[0]?.count ?? 0);
13955
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
+ }
13956
14281
  async listTombstones() {
13957
14282
  await this.ensureSchema();
13958
14283
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
@@ -14428,7 +14753,7 @@ async function startTask2(id, agentId, store) {
14428
14753
  if (task.status !== "pending" && task.status !== "in_progress") {
14429
14754
  throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
14430
14755
  }
14431
- return patchTask(task, {
14756
+ const started = await patchTask(task, {
14432
14757
  status: "in_progress",
14433
14758
  assigned_to: task.assigned_to ?? agentId,
14434
14759
  agent_id: task.agent_id ?? agentId,
@@ -14436,6 +14761,8 @@ async function startTask2(id, agentId, store) {
14436
14761
  locked_at: new Date().toISOString(),
14437
14762
  started_at: task.started_at ?? new Date().toISOString()
14438
14763
  }, store);
14764
+ await logTaskChange2(task.id, "start", "status", task.status, "in_progress", agentId, store);
14765
+ return started;
14439
14766
  }
14440
14767
  async function completeTask2(id, agentId, options, store) {
14441
14768
  const task = await store.completeTask(id, agentId, options);
@@ -14553,9 +14880,11 @@ async function removeDependency2(taskId, dependsOn, store) {
14553
14880
  }
14554
14881
  async function listDependencies(taskId, store) {
14555
14882
  const edges = await store.list("dependencies");
14883
+ const incoming = edges.filter((edge) => edge.depends_on === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on }));
14556
14884
  return {
14557
14885
  dependencies: edges.filter((edge) => edge.task_id === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on })),
14558
- blocked_by: edges.filter((edge) => edge.depends_on === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on }))
14886
+ blocks: incoming,
14887
+ blocked_by: incoming
14559
14888
  };
14560
14889
  }
14561
14890
  async function addVerification(input, store, context) {
@@ -15542,17 +15871,17 @@ function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
15542
15871
  init_config();
15543
15872
  function createTodosStorageAdapter(options = {}) {
15544
15873
  const config = options.config ?? loadTodosStorageConfig(options.env);
15545
- if (config.mode === "local") {
15874
+ if (config.mode === "sqlite") {
15546
15875
  if (isTodosShadowEnabled(options.env))
15547
15876
  return createShadowAdapter(options, config);
15548
15877
  return createLocalSqliteTodosStorageAdapter(options.local);
15549
15878
  }
15550
15879
  assertTodosRemoteStorageConfig(config);
15551
- const adapter = config.mode === "hybrid" ? options.hybridAdapter ?? createImplicitHybridAdapter(options) ?? options.remoteAdapter : options.remoteAdapter ?? createImplicitPostgresAdapter(options);
15880
+ const adapter = options.remoteAdapter ?? createImplicitPostgresAdapter(options);
15552
15881
  if (!adapter) {
15553
- 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.");
15554
15883
  }
15555
- assertRemoteAdapterCapabilities(adapter, config.mode);
15884
+ assertPostgresAdapterCapabilities(adapter);
15556
15885
  return adapter;
15557
15886
  }
15558
15887
  function createShadowAdapter(options, config) {
@@ -15566,7 +15895,7 @@ function createShadowAdapter(options, config) {
15566
15895
  ...options.local ? { local: options.local } : {},
15567
15896
  ...options.postgresSyncStore ? { syncStore: options.postgresSyncStore } : {},
15568
15897
  ...postgresClient ? { postgresClient } : {},
15569
- ...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
15898
+ ...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
15570
15899
  });
15571
15900
  }
15572
15901
  function createImplicitPostgresAdapter(options) {
@@ -15574,25 +15903,12 @@ function createImplicitPostgresAdapter(options) {
15574
15903
  return null;
15575
15904
  return createPostgresTodosStorageAdapter({
15576
15905
  client: options.postgresClient,
15577
- ...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
15578
- });
15579
- }
15580
- function createImplicitHybridAdapter(options) {
15581
- if (!options.postgresClient && !options.postgresSyncStore)
15582
- return null;
15583
- return createHybridTodosStorageAdapter({
15584
- ...options.hybrid ?? {},
15585
- local: options.local,
15586
- postgresClient: options.postgresClient,
15587
- syncStore: options.postgresSyncStore
15906
+ ...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
15588
15907
  });
15589
15908
  }
15590
- function assertRemoteAdapterCapabilities(adapter, mode) {
15909
+ function assertPostgresAdapterCapabilities(adapter) {
15591
15910
  if (!adapter.capabilities.remotePersistence) {
15592
- throw new Error(`${mode} storage adapter must set capabilities.remotePersistence=true`);
15593
- }
15594
- if (mode === "hybrid" && !adapter.capabilities.localPersistence) {
15595
- throw new Error("hybrid storage adapter must also set capabilities.localPersistence=true");
15911
+ throw new Error("postgres storage adapter must set capabilities.remotePersistence=true");
15596
15912
  }
15597
15913
  }
15598
15914
  // src/storage/shadow-outbox.ts
@@ -15919,6 +16235,50 @@ async function closeRuntimeShadowCloud() {
15919
16235
  await cloud.close().catch(() => {});
15920
16236
  }
15921
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
+ }
15922
16282
  // src/storage/comment-redaction-backfill.ts
15923
16283
  init_redaction();
15924
16284
  var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
@@ -16453,11 +16813,13 @@ export {
16453
16813
  postgresTodosCommentCursorIndexSql,
16454
16814
  planRunArtifactsS3Sync,
16455
16815
  parseStorageMode,
16816
+ parseStorageBackend,
16456
16817
  maybeInstallShadowCapture2 as maybeInstallShadowCapture,
16457
16818
  loadTodosStorageConfig,
16458
16819
  loadStorageConfig,
16459
16820
  isTodosShadowEnabled,
16460
16821
  isTodosRemoteStorageEnabled,
16822
+ isTodosPostgresBackend,
16461
16823
  isCommentRedactionBackfillComplete,
16462
16824
  installShadowOutboxSchema,
16463
16825
  importSqliteTodosStorageSnapshot,
@@ -16466,6 +16828,7 @@ export {
16466
16828
  getTodosStorageEnvName,
16467
16829
  getTodosStorageDatabaseUrl,
16468
16830
  getTodosStorageDatabaseEnv,
16831
+ getTodosStorageBackend,
16469
16832
  getStorageMode,
16470
16833
  getStorageDatabaseUrl,
16471
16834
  getStorageDatabaseEnv,