@hasna/todos 0.13.2 → 0.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +78 -14
  2. package/dist/cli/cloud-router.d.ts +57 -7
  3. package/dist/cli/cloud-router.d.ts.map +1 -1
  4. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  5. package/dist/cli/doctor-integrity.d.ts +106 -0
  6. package/dist/cli/doctor-integrity.d.ts.map +1 -0
  7. package/dist/cli/helpers.d.ts +2 -2
  8. package/dist/cli/index.js +1461 -778
  9. package/dist/cli/stage-a.d.ts.map +1 -1
  10. package/dist/contracts.js +118 -30
  11. package/dist/db/database.d.ts.map +1 -1
  12. package/dist/db/findings.d.ts +1 -0
  13. package/dist/db/findings.d.ts.map +1 -1
  14. package/dist/db/integrity.d.ts +20 -0
  15. package/dist/db/integrity.d.ts.map +1 -0
  16. package/dist/index.d.ts +5 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +562 -132
  19. package/dist/lib/context-packs.d.ts +1 -0
  20. package/dist/lib/context-packs.d.ts.map +1 -1
  21. package/dist/lib/dependency-graph.d.ts +1 -1
  22. package/dist/lib/doctor.d.ts +20 -0
  23. package/dist/lib/doctor.d.ts.map +1 -1
  24. package/dist/lib/integrity.d.ts +215 -0
  25. package/dist/lib/integrity.d.ts.map +1 -0
  26. package/dist/lib/native-storage-status.d.ts +2 -2
  27. package/dist/lib/native-storage-status.d.ts.map +1 -1
  28. package/dist/lib/release-compatibility.d.ts.map +1 -1
  29. package/dist/lib/task-dedupe.d.ts +15 -0
  30. package/dist/lib/task-dedupe.d.ts.map +1 -1
  31. package/dist/mcp/index.d.ts.map +1 -1
  32. package/dist/mcp/index.js +517 -85
  33. package/dist/mcp.js +7 -3
  34. package/dist/registry.d.ts +1 -1
  35. package/dist/registry.d.ts.map +1 -1
  36. package/dist/registry.js +125 -30
  37. package/dist/release-provenance.json +5 -5
  38. package/dist/sdk/types.d.ts +42 -0
  39. package/dist/sdk/types.d.ts.map +1 -1
  40. package/dist/sdk/v1.generated.d.ts +1 -0
  41. package/dist/sdk/v1.generated.d.ts.map +1 -1
  42. package/dist/server/cloud.d.ts +8 -1
  43. package/dist/server/cloud.d.ts.map +1 -1
  44. package/dist/server/index.js +521 -89
  45. package/dist/server/openapi.d.ts +12 -0
  46. package/dist/server/openapi.d.ts.map +1 -1
  47. package/dist/server/serve.d.ts.map +1 -1
  48. package/dist/server/v1.d.ts.map +1 -1
  49. package/dist/storage/config.d.ts +23 -5
  50. package/dist/storage/config.d.ts.map +1 -1
  51. package/dist/storage/factory.d.ts +9 -3
  52. package/dist/storage/factory.d.ts.map +1 -1
  53. package/dist/storage/hybrid.d.ts +7 -0
  54. package/dist/storage/hybrid.d.ts.map +1 -1
  55. package/dist/storage/index.d.ts +3 -3
  56. package/dist/storage/index.d.ts.map +1 -1
  57. package/dist/storage/interfaces.d.ts +29 -8
  58. package/dist/storage/interfaces.d.ts.map +1 -1
  59. package/dist/storage/local-sqlite.d.ts.map +1 -1
  60. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  61. package/dist/storage.d.ts +3 -3
  62. package/dist/storage.d.ts.map +1 -1
  63. package/dist/storage.js +601 -250
  64. package/dist/testing.d.ts +79 -0
  65. package/dist/testing.d.ts.map +1 -0
  66. package/dist/testing.js +107 -0
  67. package/dist/types/index.d.ts +12 -1
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/package.json +7 -3
package/dist/index.js CHANGED
@@ -3427,7 +3427,7 @@ var init_machines = __esm(() => {
3427
3427
  function isBlockingDependencyStatus(status) {
3428
3428
  return status !== "completed" && status !== "cancelled";
3429
3429
  }
3430
- var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3430
+ var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3431
3431
  var init_types = __esm(() => {
3432
3432
  TASK_STATUSES = [
3433
3433
  "pending",
@@ -3467,6 +3467,22 @@ var init_types = __esm(() => {
3467
3467
  this.name = "TaskNotFoundError";
3468
3468
  }
3469
3469
  };
3470
+ TaskReferenceAmbiguousError = class TaskReferenceAmbiguousError extends Error {
3471
+ reference;
3472
+ static code = "TASK_REFERENCE_AMBIGUOUS";
3473
+ candidateTaskIds;
3474
+ candidateProjectIds;
3475
+ constructor(reference, candidates) {
3476
+ const candidateTaskIds = [...new Set(candidates.map((candidate) => candidate.task_id))].sort();
3477
+ const candidateProjectIds = [...new Set(candidates.map((candidate) => candidate.project_id).filter((projectId) => typeof projectId === "string" && projectId.length > 0))].sort();
3478
+ const projectList = candidateProjectIds.length > 0 ? candidateProjectIds.join(", ") : "(unscoped)";
3479
+ super(`Task reference is ambiguous: "${reference}". Candidate project IDs: ${projectList}. ` + "Use a full task UUID.");
3480
+ this.reference = reference;
3481
+ this.name = "TaskReferenceAmbiguousError";
3482
+ this.candidateTaskIds = candidateTaskIds;
3483
+ this.candidateProjectIds = candidateProjectIds;
3484
+ }
3485
+ };
3470
3486
  ProjectNotFoundError = class ProjectNotFoundError extends Error {
3471
3487
  projectId;
3472
3488
  static code = "PROJECT_NOT_FOUND";
@@ -3758,15 +3774,18 @@ var init_identity_mapping = __esm(() => {
3758
3774
  var exports_config = {};
3759
3775
  __export(exports_config, {
3760
3776
  parseStorageMode: () => parseStorageMode,
3777
+ parseStorageBackend: () => parseStorageBackend,
3761
3778
  loadTodosStorageConfig: () => loadTodosStorageConfig,
3762
3779
  loadStorageConfig: () => loadStorageConfig,
3763
3780
  isTodosShadowEnabled: () => isTodosShadowEnabled,
3764
3781
  isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
3782
+ isTodosPostgresBackend: () => isTodosPostgresBackend,
3765
3783
  getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
3766
3784
  getTodosStorageMode: () => getTodosStorageMode,
3767
3785
  getTodosStorageEnvName: () => getTodosStorageEnvName,
3768
3786
  getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
3769
3787
  getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
3788
+ getTodosStorageBackend: () => getTodosStorageBackend,
3770
3789
  getStorageMode: () => getStorageMode,
3771
3790
  getStorageDatabaseUrl: () => getStorageDatabaseUrl,
3772
3791
  getStorageDatabaseEnv: () => getStorageDatabaseEnv,
@@ -3791,7 +3810,7 @@ function getCanonicalTodosRdsConfig(env = process.env) {
3791
3810
  };
3792
3811
  }
3793
3812
  function loadTodosStorageConfig(env = process.env) {
3794
- const mode = getTodosStorageMode(env);
3813
+ const mode = getTodosStorageBackend(env);
3795
3814
  const databaseUrl = getTodosStorageDatabaseUrl(env);
3796
3815
  const bucket = readStorageEnv(env, "s3Bucket").value;
3797
3816
  const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
@@ -3828,29 +3847,41 @@ function loadTodosStorageConfig(env = process.env) {
3828
3847
  function loadStorageConfig(env = process.env) {
3829
3848
  return loadTodosStorageConfig(env);
3830
3849
  }
3850
+ function isTodosPostgresBackend(config) {
3851
+ return config.mode === "postgres";
3852
+ }
3831
3853
  function isTodosRemoteStorageEnabled(config) {
3832
- return config.mode === "remote" || config.mode === "hybrid";
3854
+ return isTodosPostgresBackend(config);
3833
3855
  }
3834
3856
  function assertTodosRemoteStorageConfig(config) {
3835
- if (!isTodosRemoteStorageEnabled(config))
3857
+ if (!isTodosPostgresBackend(config))
3836
3858
  return;
3837
3859
  if (!config.database?.url) {
3838
3860
  throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
3839
3861
  }
3840
3862
  }
3841
- function parseStorageMode(value) {
3863
+ function parseStorageBackend(value) {
3842
3864
  const normalized = clean(value)?.toLowerCase();
3843
3865
  if (!normalized)
3844
- return "local";
3845
- if (normalized === "local" || normalized === "remote" || normalized === "hybrid")
3866
+ return "sqlite";
3867
+ if (normalized === "sqlite" || normalized === "postgres")
3846
3868
  return normalized;
3847
- throw new Error(`${TODOS_STORAGE_ENV.mode} must be local, remote, or hybrid`);
3869
+ const legacy = LEGACY_BACKEND_TOKENS[normalized];
3870
+ if (legacy)
3871
+ return legacy;
3872
+ throw new Error(`${TODOS_STORAGE_ENV.mode} must be sqlite or postgres (legacy values local and remote are accepted)`);
3873
+ }
3874
+ function parseStorageMode(value) {
3875
+ return parseStorageBackend(value);
3876
+ }
3877
+ function getTodosStorageBackend(env = process.env) {
3878
+ return parseStorageBackend(readStorageEnv(env, "mode").value);
3848
3879
  }
3849
3880
  function getTodosStorageMode(env = process.env) {
3850
- return parseStorageMode(readStorageEnv(env, "mode").value);
3881
+ return getTodosStorageBackend(env);
3851
3882
  }
3852
3883
  function getStorageMode(env = process.env) {
3853
- return getTodosStorageMode(env);
3884
+ return getTodosStorageBackend(env);
3854
3885
  }
3855
3886
  function isTodosShadowEnabled(env = process.env) {
3856
3887
  return parseBoolean(readStorageEnv(env, "shadow").value, false);
@@ -3861,8 +3892,8 @@ function getTodosStorageShadowEnvName(env = process.env) {
3861
3892
  function assertTodosShadowConfig(config, env = process.env) {
3862
3893
  if (!isTodosShadowEnabled(env))
3863
3894
  return;
3864
- if (config.mode !== "local") {
3865
- throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires ${TODOS_STORAGE_ENV.mode}=local (got ${config.mode})`);
3895
+ if (config.mode !== "sqlite") {
3896
+ throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
3866
3897
  }
3867
3898
  if (!config.database?.url) {
3868
3899
  throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
@@ -3918,7 +3949,7 @@ function parsePositiveInteger(value, fallback) {
3918
3949
  }
3919
3950
  return parsed;
3920
3951
  }
3921
- 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";
3952
+ 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;
3922
3953
  var init_config = __esm(() => {
3923
3954
  TODOS_STORAGE_TABLES = [
3924
3955
  "todos_sync_records",
@@ -3959,6 +3990,14 @@ var init_config = __esm(() => {
3959
3990
  syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
3960
3991
  syncDryRun: "TODOS_SYNC_DRY_RUN"
3961
3992
  };
3993
+ LEGACY_BACKEND_TOKENS = {
3994
+ local: "sqlite",
3995
+ remote: "postgres",
3996
+ postgresql: "postgres",
3997
+ hybrid: "postgres",
3998
+ self_hosted: "postgres",
3999
+ cloud: "postgres"
4000
+ };
3962
4001
  });
3963
4002
 
3964
4003
  // src/storage/shadow-outbox-schema.ts
@@ -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);
@@ -11946,7 +12013,7 @@ var init_dispatches = __esm(() => {
11946
12013
  // package.json
11947
12014
  var package_default = {
11948
12015
  name: "@hasna/todos",
11949
- version: "0.13.2",
12016
+ version: "0.13.3",
11950
12017
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
11951
12018
  type: "module",
11952
12019
  main: "dist/index.js",
@@ -11980,6 +12047,10 @@ var package_default = {
11980
12047
  "./storage": {
11981
12048
  types: "./dist/storage.d.ts",
11982
12049
  import: "./dist/storage.js"
12050
+ },
12051
+ "./testing": {
12052
+ types: "./dist/testing.d.ts",
12053
+ import: "./dist/testing.js"
11983
12054
  }
11984
12055
  },
11985
12056
  workspaces: [
@@ -11992,8 +12063,8 @@ var package_default = {
11992
12063
  "README.md"
11993
12064
  ],
11994
12065
  scripts: {
11995
- build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
11996
- "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
12066
+ build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
12067
+ "build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
11997
12068
  migrate: "bun run src/server/index.ts migrate",
11998
12069
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
11999
12070
  "generate:sdk": "bun run scripts/generate-sdk.ts",
@@ -17740,11 +17811,12 @@ var DEFAULT_LIMITS = {
17740
17811
  verification_limit: 10,
17741
17812
  run_limit: 3,
17742
17813
  dependency_limit: 12,
17743
- plan_task_limit: 20,
17814
+ plan_task_limit: 24,
17744
17815
  max_text_chars: 6000,
17745
17816
  summary_char_limit: 480,
17746
17817
  stale_after_hours: 72
17747
17818
  };
17819
+ var MAX_CONTEXT_TASKS = 24;
17748
17820
  var ALL_CONTEXT_SECTIONS = [
17749
17821
  "project",
17750
17822
  "plan",
@@ -17777,7 +17849,7 @@ function limits(input) {
17777
17849
  verification_limit: clamp(input.verification_limit, DEFAULT_LIMITS.verification_limit, 100),
17778
17850
  run_limit: clamp(input.run_limit, DEFAULT_LIMITS.run_limit, 20),
17779
17851
  dependency_limit: clamp(input.dependency_limit, DEFAULT_LIMITS.dependency_limit, 100),
17780
- plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit, 100),
17852
+ plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit, MAX_CONTEXT_TASKS),
17781
17853
  max_text_chars: clamp(input.max_text_chars, DEFAULT_LIMITS.max_text_chars, 50000),
17782
17854
  summary_char_limit: clamp(input.summary_char_limit, DEFAULT_LIMITS.summary_char_limit, 4000),
17783
17855
  stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
@@ -17800,6 +17872,13 @@ function taskSummary(task2) {
17800
17872
  priority: task2.priority
17801
17873
  };
17802
17874
  }
17875
+ function latestPlanTasks(planId, limit, db) {
17876
+ const rows = db.query(`SELECT * FROM tasks
17877
+ WHERE plan_id = ? AND archived_at IS NULL
17878
+ ORDER BY updated_at DESC, created_at DESC, id ASC
17879
+ LIMIT ?`).all(planId, limit);
17880
+ return rows.map(rowToTask);
17881
+ }
17803
17882
  function acceptanceCriteria(task2, maxText) {
17804
17883
  const metadata = task2.metadata || {};
17805
17884
  const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
@@ -17897,7 +17976,7 @@ function summarizeSection(pack, section, maxChars) {
17897
17976
  return "No plan was attached.";
17898
17977
  return summarizeStrings([
17899
17978
  `Plan ${pack.plan.name} is ${pack.plan.status}`,
17900
- `${pack.plan.tasks.length} listed plan tasks`,
17979
+ `${pack.plan.tasks.length} of ${pack.plan.total_tasks} plan tasks listed; ${pack.plan.omitted_tasks} omitted`,
17901
17980
  ...pack.plan.tasks.slice(0, 4).map((task2) => `${task2.status} ${task2.short_id || task2.id.slice(0, 8)} ${task2.title}`)
17902
17981
  ], maxChars);
17903
17982
  }
@@ -18047,7 +18126,8 @@ function createAgentContextPack(input, db) {
18047
18126
  const limit = limits(input);
18048
18127
  const project = task2.project_id ? getProject(task2.project_id, d) : null;
18049
18128
  const plan = task2.plan_id ? getPlan(task2.plan_id, d) : null;
18050
- const planTasks = task2.plan_id ? listTasks({ plan_id: task2.plan_id, limit: limit.plan_task_limit + 1 }, d) : [];
18129
+ const planTasks = task2.plan_id ? latestPlanTasks(task2.plan_id, limit.plan_task_limit, d) : [];
18130
+ const totalPlanTasks = task2.plan_id ? countTasks({ plan_id: task2.plan_id }, d) : 0;
18051
18131
  const upstream = getTaskDependencies(task2.id, d).map((dep) => taskSummary(getTask(dep.depends_on, d))).filter((item) => Boolean(item));
18052
18132
  const downstream = getTaskDependents(task2.id, d).map((dep) => taskSummary(getTask(dep.task_id, d))).filter((item) => Boolean(item));
18053
18133
  const comments = listComments(task2.id, d);
@@ -18090,6 +18170,9 @@ function createAgentContextPack(input, db) {
18090
18170
  warnings.push(`${runs.length - selectedRuns.length} older runs omitted`);
18091
18171
  if (fileMap.size > relevantFiles.length)
18092
18172
  warnings.push(`${fileMap.size - relevantFiles.length} relevant files omitted`);
18173
+ if (totalPlanTasks > planTasks.length && plan) {
18174
+ warnings.push(`${totalPlanTasks - planTasks.length} plan tasks omitted (${totalPlanTasks} total); load more on demand with "todos plans --show ${plan.id}" and inspect task details with "todos show <task-id>" or "todos context-pack <task-id>"`);
18175
+ }
18093
18176
  const contextTask = {
18094
18177
  id: task2.id,
18095
18178
  short_id: task2.short_id,
@@ -18121,8 +18204,9 @@ function createAgentContextPack(input, db) {
18121
18204
  description: truncate2(plan.description, limit.max_text_chars),
18122
18205
  status: plan.status,
18123
18206
  agent_id: plan.agent_id,
18124
- tasks: planTasks.slice(0, limit.plan_task_limit).map(taskSummary).filter((item) => Boolean(item)),
18125
- omitted_tasks: Math.max(0, planTasks.length - limit.plan_task_limit)
18207
+ tasks: planTasks.map(taskSummary).filter((item) => Boolean(item)),
18208
+ total_tasks: totalPlanTasks,
18209
+ omitted_tasks: Math.max(0, totalPlanTasks - planTasks.length)
18126
18210
  } : null,
18127
18211
  acceptance_criteria: acceptanceCriteria(task2, limit.max_text_chars),
18128
18212
  dependencies: {
@@ -18232,6 +18316,8 @@ ${pack.task.description}` : null,
18232
18316
  "## Project And Plan",
18233
18317
  pack.project ? `- Project: ${pack.project.name} (${pack.project.path})` : "- Project: none",
18234
18318
  pack.plan ? `- Plan: ${pack.plan.name} (${pack.plan.status})` : "- Plan: none",
18319
+ pack.plan ? `- Plan tasks: ${pack.plan.tasks.length} shown of ${pack.plan.total_tasks} total (${pack.plan.omitted_tasks} omitted)` : null,
18320
+ pack.plan && pack.plan.omitted_tasks > 0 ? `- Load more on demand: \`todos plans --show ${pack.plan.id}\`; inspect one task with \`todos show <task-id>\` or \`todos context-pack <task-id>\`.` : null,
18235
18321
  pack.plan && pack.plan.tasks.length > 0 ? bullet(pack.plan.tasks.map((task2) => `${task2.status} ${task2.short_id || task2.id.slice(0, 8)} ${task2.title}`)) : null,
18236
18322
  "",
18237
18323
  "## Acceptance Criteria",
@@ -19900,7 +19986,7 @@ function createReleaseCompatibilityReport(options = {}) {
19900
19986
  "todos --help",
19901
19987
  "todos-mcp --help",
19902
19988
  "todos-serve --help",
19903
- "todos doctor"
19989
+ "todos doctor --no-fail-on-findings"
19904
19990
  ],
19905
19991
  smoke_tests: [
19906
19992
  "command -v todos",
@@ -19908,7 +19994,7 @@ function createReleaseCompatibilityReport(options = {}) {
19908
19994
  "command -v todos-serve",
19909
19995
  "todos --version",
19910
19996
  "todos --help",
19911
- "todos doctor"
19997
+ "todos doctor --no-fail-on-findings"
19912
19998
  ],
19913
19999
  rollback: [
19914
20000
  "npm view @hasna/todos versions --json",
@@ -20402,12 +20488,11 @@ ${task2.description || ""}`.split(/\r?\n/);
20402
20488
  function normalizeStackLocation(value) {
20403
20489
  return value.replace(/:\d+(?::\d+)?/g, "").replace(/^file:\/\//, "").toLowerCase().trim();
20404
20490
  }
20405
- function fingerprint(task2) {
20491
+ function fingerprintContent(task2) {
20406
20492
  const body = task2.description || "";
20407
20493
  const text = `${task2.title}
20408
20494
  ${body}`;
20409
20495
  return {
20410
- task: task2,
20411
20496
  title: normalizeText(task2.title),
20412
20497
  body: normalizeText(body),
20413
20498
  text: normalizeText(text),
@@ -20416,6 +20501,9 @@ ${body}`;
20416
20501
  stackKeys: stackKeysFor(task2)
20417
20502
  };
20418
20503
  }
20504
+ function fingerprint(task2) {
20505
+ return { task: task2, ...fingerprintContent(task2) };
20506
+ }
20419
20507
  function intersects(left, right) {
20420
20508
  for (const value of left)
20421
20509
  if (right.has(value))
@@ -23602,6 +23690,13 @@ var TODOS_PACKAGE_EXPORTS = [
23602
23690
  types: "./dist/storage.d.ts",
23603
23691
  description: "Storage and service adapter contracts for local SQLite implementations.",
23604
23692
  stability: "stable"
23693
+ },
23694
+ {
23695
+ subpath: "./testing",
23696
+ import: "./dist/testing.js",
23697
+ types: "./dist/testing.d.ts",
23698
+ description: "Test-store isolation helpers that keep a consumer's test suite off a shared todos store.",
23699
+ stability: "stable"
23605
23700
  }
23606
23701
  ];
23607
23702
  function source8(version) {
@@ -23633,6 +23728,9 @@ var TODOS_REGISTRY = createTodosRegistry({
23633
23728
  // src/storage/index.ts
23634
23729
  init_config();
23635
23730
 
23731
+ // src/storage/local-sqlite.ts
23732
+ init_types();
23733
+
23636
23734
  // src/lib/search.ts
23637
23735
  init_database();
23638
23736
  function rowToTask3(row) {
@@ -24449,6 +24547,267 @@ init_audit();
24449
24547
  init_comments();
24450
24548
  init_database();
24451
24549
 
24550
+ // src/db/integrity.ts
24551
+ init_database();
24552
+
24553
+ // src/lib/integrity.ts
24554
+ init_types();
24555
+ var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1";
24556
+ var TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
24557
+ var OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
24558
+ var INTEGRITY_CONDITIONS = [
24559
+ {
24560
+ id: "tasks_without_project",
24561
+ entity: "task",
24562
+ field: "project_id",
24563
+ target: "project",
24564
+ kind: "missing",
24565
+ base_severity: "warn",
24566
+ escalate_when_open: true,
24567
+ impact: "invisible to every project-scoped read (list, status, next, claim)"
24568
+ },
24569
+ {
24570
+ id: "tasks_without_task_list",
24571
+ entity: "task",
24572
+ field: "task_list_id",
24573
+ target: "task_list",
24574
+ kind: "missing",
24575
+ base_severity: "warn",
24576
+ escalate_when_open: true,
24577
+ impact: "invisible to every task-list read \u2014 the list reports zero open work"
24578
+ },
24579
+ {
24580
+ id: "tasks_with_unregistered_project",
24581
+ entity: "task",
24582
+ field: "project_id",
24583
+ target: "project",
24584
+ kind: "dangling",
24585
+ base_severity: "error",
24586
+ escalate_when_open: false,
24587
+ impact: "points at a project id that does not exist; the reference can never resolve"
24588
+ },
24589
+ {
24590
+ id: "tasks_with_unregistered_task_list",
24591
+ entity: "task",
24592
+ field: "task_list_id",
24593
+ target: "task_list",
24594
+ kind: "dangling",
24595
+ base_severity: "error",
24596
+ escalate_when_open: false,
24597
+ impact: "points at a task-list id that does not exist; the reference can never resolve"
24598
+ },
24599
+ {
24600
+ id: "task_lists_without_project",
24601
+ entity: "task_list",
24602
+ field: "project_id",
24603
+ target: "project",
24604
+ kind: "missing",
24605
+ base_severity: "warn",
24606
+ escalate_when_open: false,
24607
+ impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
24608
+ },
24609
+ {
24610
+ id: "task_lists_with_unregistered_project",
24611
+ entity: "task_list",
24612
+ field: "project_id",
24613
+ target: "project",
24614
+ kind: "dangling",
24615
+ base_severity: "error",
24616
+ escalate_when_open: false,
24617
+ impact: "points at a project id that does not exist; the list can never be reached"
24618
+ }
24619
+ ];
24620
+ function integrityCondition(id) {
24621
+ const spec = INTEGRITY_CONDITIONS.find((condition) => condition.id === id);
24622
+ if (!spec)
24623
+ throw new Error(`unknown integrity condition: ${id}`);
24624
+ return spec;
24625
+ }
24626
+ function resolveIntegritySeverity(spec, measurement) {
24627
+ if (spec.kind === "dangling")
24628
+ return "error";
24629
+ if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
24630
+ return "error";
24631
+ return spec.base_severity;
24632
+ }
24633
+ var ENTITY_LABEL = {
24634
+ task: ["task", "tasks"],
24635
+ task_list: ["task list", "task lists"]
24636
+ };
24637
+ function plural(entity, count2) {
24638
+ const [one, many] = ENTITY_LABEL[entity];
24639
+ return count2 === 1 ? one : many;
24640
+ }
24641
+ function formatIntegrityMessage(spec, measurement) {
24642
+ const noun = plural(spec.entity, measurement.count);
24643
+ const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
24644
+ const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
24645
+ 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}`;
24646
+ }
24647
+ function measuredCondition(spec, measurement, source9) {
24648
+ return {
24649
+ id: spec.id,
24650
+ entity: spec.entity,
24651
+ field: spec.field,
24652
+ kind: spec.kind,
24653
+ count: measurement.count,
24654
+ open_count: measurement.open_count,
24655
+ severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
24656
+ verified: true,
24657
+ source: source9,
24658
+ message: formatIntegrityMessage(spec, measurement),
24659
+ impact: spec.impact
24660
+ };
24661
+ }
24662
+ function unverifiedCondition(spec, reason) {
24663
+ return {
24664
+ id: spec.id,
24665
+ entity: spec.entity,
24666
+ field: spec.field,
24667
+ kind: spec.kind,
24668
+ count: null,
24669
+ open_count: null,
24670
+ severity: null,
24671
+ verified: false,
24672
+ source: "unverified",
24673
+ unverified_reason: reason,
24674
+ message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
24675
+ impact: spec.impact
24676
+ };
24677
+ }
24678
+ function summarizeIntegrity(conditions) {
24679
+ const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
24680
+ const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
24681
+ const unverified = conditions.length - measured.length;
24682
+ return {
24683
+ ok: unverified === 0 && findings.length === 0,
24684
+ findings: findings.length,
24685
+ rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
24686
+ errors: findings.filter((condition) => condition.severity === "error").length,
24687
+ warnings: findings.filter((condition) => condition.severity === "warn").length,
24688
+ unverified,
24689
+ complete: unverified === 0
24690
+ };
24691
+ }
24692
+ function buildIntegrityReport(conditions, generatedAt) {
24693
+ const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source9) => source9 !== "unverified");
24694
+ return {
24695
+ schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
24696
+ generated_at: generatedAt,
24697
+ source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
24698
+ conditions,
24699
+ summary: summarizeIntegrity(conditions)
24700
+ };
24701
+ }
24702
+ function adoptRemoteIntegrityReport(raw, fallbackGeneratedAt) {
24703
+ const received = new Map;
24704
+ if (Array.isArray(raw.conditions)) {
24705
+ for (const entry of raw.conditions) {
24706
+ if (entry && typeof entry["id"] === "string")
24707
+ received.set(entry["id"], entry);
24708
+ }
24709
+ }
24710
+ const conditions = INTEGRITY_CONDITIONS.map((spec) => {
24711
+ const entry = received.get(spec.id);
24712
+ const count2 = entry?.["count"];
24713
+ const verified = entry?.["verified"];
24714
+ if (!entry || verified === false || typeof count2 !== "number" || !Number.isFinite(count2)) {
24715
+ 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");
24716
+ }
24717
+ const openRaw = entry["open_count"];
24718
+ return measuredCondition(spec, {
24719
+ count: count2,
24720
+ open_count: spec.entity === "task" ? typeof openRaw === "number" && Number.isFinite(openRaw) ? openRaw : 0 : null
24721
+ }, typeof raw.source === "string" && raw.source === "postgres" ? "postgres" : "remote-authority");
24722
+ });
24723
+ const report = buildIntegrityReport(conditions, typeof raw.generated_at === "string" ? raw.generated_at : fallbackGeneratedAt);
24724
+ return typeof raw.source === "string" && (raw.source === "sqlite" || raw.source === "postgres") ? { ...report, source: raw.source } : report;
24725
+ }
24726
+ var SQLITE_TABLE = {
24727
+ task: "tasks",
24728
+ task_list: "task_lists",
24729
+ project: "projects"
24730
+ };
24731
+ function sqliteOpenStatusList() {
24732
+ return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
24733
+ }
24734
+ function buildSqliteIntegritySql(spec) {
24735
+ const table = SQLITE_TABLE[spec.entity];
24736
+ const target = SQLITE_TABLE[spec.target];
24737
+ const column = `t."${spec.field}"`;
24738
+ 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})`;
24739
+ const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
24740
+ return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
24741
+ }
24742
+ var RECORD_TYPE = {
24743
+ task: "tasks",
24744
+ task_list: "task_lists",
24745
+ project: "projects"
24746
+ };
24747
+ function buildPostgresIntegritySql(spec, options) {
24748
+ const params = [options.service, RECORD_TYPE[spec.entity]];
24749
+ const p = (value) => {
24750
+ params.push(value);
24751
+ return `$${params.length}`;
24752
+ };
24753
+ const column = `t.payload->>'${spec.field}'`;
24754
+ 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})`;
24755
+ const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
24756
+ 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}`;
24757
+ return { sql, params };
24758
+ }
24759
+ function referenceOf(row, field2) {
24760
+ const raw = row[field2];
24761
+ if (typeof raw !== "string")
24762
+ return null;
24763
+ const trimmed = raw.trim();
24764
+ return trimmed === "" ? null : trimmed;
24765
+ }
24766
+ function measureIntegrityRows(spec, sets) {
24767
+ const rows = spec.entity === "task" ? sets.tasks : sets.taskLists;
24768
+ if (!rows)
24769
+ return null;
24770
+ const registered = spec.kind === "dangling" ? spec.target === "project" ? sets.projectIds : sets.taskListIds : undefined;
24771
+ if (spec.kind === "dangling" && !registered)
24772
+ return null;
24773
+ let count2 = 0;
24774
+ let open = 0;
24775
+ for (const row of rows) {
24776
+ const reference = referenceOf(row, spec.field);
24777
+ const matches = spec.kind === "missing" ? reference === null : reference !== null && !registered.has(reference);
24778
+ if (!matches)
24779
+ continue;
24780
+ count2++;
24781
+ if (spec.entity === "task") {
24782
+ const status = row.status;
24783
+ if (typeof status === "string" && OPEN_TASK_STATUSES.includes(status))
24784
+ open++;
24785
+ }
24786
+ }
24787
+ return { count: count2, open_count: spec.entity === "task" ? open : null };
24788
+ }
24789
+
24790
+ // src/db/integrity.ts
24791
+ var REQUIRED_TABLES2 = { tasks: true, task_lists: true, projects: true };
24792
+ function tableExists(db, table) {
24793
+ return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
24794
+ }
24795
+ function scanSqliteIntegrity(db = getDatabase()) {
24796
+ const missing = Object.keys(REQUIRED_TABLES2).filter((table) => !tableExists(db, table));
24797
+ const conditions = INTEGRITY_CONDITIONS.map((spec) => {
24798
+ if (missing.length > 0) {
24799
+ return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
24800
+ }
24801
+ try {
24802
+ const row = db.query(buildSqliteIntegritySql(spec)).get();
24803
+ return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
24804
+ } catch (error) {
24805
+ return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
24806
+ }
24807
+ });
24808
+ return buildIntegrityReport(conditions, now());
24809
+ }
24810
+
24452
24811
  // src/storage/sqlite-snapshot.ts
24453
24812
  init_database();
24454
24813
  init_audit();
@@ -24840,15 +25199,15 @@ function resolveTaskRefLocal(db, ref) {
24840
25199
  return null;
24841
25200
  if (TASK_UUID_RE.test(raw))
24842
25201
  return getTask(raw, db);
24843
- const prefixRows = db.query("SELECT id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
25202
+ 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}`)}%`);
24844
25203
  if (prefixRows.length > 1) {
24845
- throw new Error(`Task reference is ambiguous: "${ref}"`);
25204
+ throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
24846
25205
  }
24847
25206
  if (prefixRows.length === 1)
24848
25207
  return getTask(prefixRows[0].id, db);
24849
- const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
25208
+ const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
24850
25209
  if (shortIdRows.length > 1) {
24851
- throw new Error(`Task reference is ambiguous: "${ref}"`);
25210
+ throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
24852
25211
  }
24853
25212
  if (shortIdRows.length === 1)
24854
25213
  return getTask(shortIdRows[0].id, db);
@@ -24997,6 +25356,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
24997
25356
  exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
24998
25357
  importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
24999
25358
  },
25359
+ integrity: {
25360
+ report: () => scanSqliteIntegrity(database())
25361
+ },
25000
25362
  transaction: (fn) => {
25001
25363
  const tx = database().transaction(() => fn(adapter));
25002
25364
  return tx();
@@ -25005,6 +25367,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
25005
25367
  return adapter;
25006
25368
  }
25007
25369
 
25370
+ // src/storage/postgres-adapter.ts
25371
+ init_types();
25372
+ import { randomUUID as randomUUID3 } from "crypto";
25373
+
25008
25374
  // src/storage/postgres-sync.ts
25009
25375
  var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
25010
25376
  var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
@@ -25394,54 +25760,7 @@ function assertSafeIdentifier(value) {
25394
25760
  throw new Error(`Unsafe Postgres identifier: ${value}`);
25395
25761
  }
25396
25762
 
25397
- // src/storage/hybrid.ts
25398
- function createHybridTodosStorageAdapter(options) {
25399
- const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
25400
- const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
25401
- if (!syncStore)
25402
- throw new Error("hybrid storage requires a Postgres sync store or query client");
25403
- if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
25404
- throw new Error("hybrid storage requires local snapshot export/import support");
25405
- }
25406
- const exportSnapshot = async (context) => await local.sync.exportSnapshot(context);
25407
- const importSnapshot = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
25408
- const remote = {
25409
- ensureSchema: () => syncStore.ensureSchema(),
25410
- async pushSnapshot(context) {
25411
- const snapshot = await exportSnapshot(context);
25412
- return syncStore.pushSnapshot(snapshot, context);
25413
- },
25414
- async pullSnapshot(options2, context) {
25415
- const snapshot = await syncStore.pullSnapshot(options2);
25416
- return importSnapshot(snapshot, context);
25417
- },
25418
- async syncOnce(options2, context) {
25419
- const pulled = await remote.pullSnapshot(options2, context);
25420
- const pushed = await remote.pushSnapshot(context);
25421
- return { pulled, pushed };
25422
- }
25423
- };
25424
- return {
25425
- ...local,
25426
- kind: "hybrid",
25427
- capabilities: {
25428
- ...local.capabilities,
25429
- localPersistence: true,
25430
- remotePersistence: true,
25431
- sync: true
25432
- },
25433
- sync: {
25434
- ...local.sync,
25435
- exportSnapshot,
25436
- importSnapshot
25437
- },
25438
- remote
25439
- };
25440
- }
25441
-
25442
25763
  // src/storage/postgres-adapter.ts
25443
- init_types();
25444
- import { randomUUID as randomUUID3 } from "crypto";
25445
25764
  init_redaction();
25446
25765
  function createPostgresTodosStorageAdapter(options) {
25447
25766
  const store = new PostgresJsonRecordStore(options);
@@ -25569,6 +25888,9 @@ function createPostgresTodosStorageAdapter(options) {
25569
25888
  exportSnapshot: () => exportSnapshot(store),
25570
25889
  importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
25571
25890
  },
25891
+ integrity: {
25892
+ report: () => store.integrityReport()
25893
+ },
25572
25894
  transaction: (fn) => fn(adapter)
25573
25895
  };
25574
25896
  return adapter;
@@ -25675,8 +25997,9 @@ class PostgresJsonRecordStore {
25675
25997
  conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
25676
25998
  if (filter.session_id !== undefined)
25677
25999
  conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
25678
- if (filter.tags?.length)
25679
- conds.push(`payload->'tags' @> ${p(filter.tags)}::jsonb`);
26000
+ if (filter.tags?.length) {
26001
+ 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)})`);
26002
+ }
25680
26003
  if (filter.has_recurrence !== undefined) {
25681
26004
  conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
25682
26005
  }
@@ -25737,7 +26060,8 @@ class PostgresJsonRecordStore {
25737
26060
  AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
25738
26061
  LIMIT 2`, [this.service, raw, upper]);
25739
26062
  if (prefixResult.rows.length > 1) {
25740
- throw new Error(`Task reference is ambiguous: "${ref}"`);
26063
+ const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
26064
+ throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
25741
26065
  }
25742
26066
  if (prefixResult.rows.length === 1) {
25743
26067
  return payloadRecord2(prefixResult.rows[0].payload);
@@ -25748,7 +26072,8 @@ class PostgresJsonRecordStore {
25748
26072
  AND LOWER(payload->>'short_id') = $2
25749
26073
  LIMIT 2`, [this.service, raw]);
25750
26074
  if (shortIdResult.rows.length > 1) {
25751
- throw new Error(`Task reference is ambiguous: "${ref}"`);
26075
+ const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
26076
+ throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
25752
26077
  }
25753
26078
  if (shortIdResult.rows.length === 1) {
25754
26079
  return payloadRecord2(shortIdResult.rows[0].payload);
@@ -25762,6 +26087,24 @@ class PostgresJsonRecordStore {
25762
26087
  const result = await this.options.client.query(sql, params);
25763
26088
  return Number(result.rows[0]?.count ?? 0);
25764
26089
  }
26090
+ async integrityReport() {
26091
+ await this.ensureSchema();
26092
+ const conditions = [];
26093
+ for (const spec of INTEGRITY_CONDITIONS) {
26094
+ const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
26095
+ try {
26096
+ const result = await this.options.client.query(sql, params);
26097
+ const row = result.rows[0];
26098
+ conditions.push(measuredCondition(spec, {
26099
+ count: Number(row?.count ?? 0),
26100
+ open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
26101
+ }, "postgres"));
26102
+ } catch (error) {
26103
+ conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
26104
+ }
26105
+ }
26106
+ return buildIntegrityReport(conditions, new Date().toISOString());
26107
+ }
25765
26108
  async listTombstones() {
25766
26109
  await this.ensureSchema();
25767
26110
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
@@ -26237,7 +26580,7 @@ async function startTask2(id, agentId, store) {
26237
26580
  if (task2.status !== "pending" && task2.status !== "in_progress") {
26238
26581
  throw new Error(`Task is ${task2.status} and cannot be started by ${agentId}`);
26239
26582
  }
26240
- return patchTask(task2, {
26583
+ const started = await patchTask(task2, {
26241
26584
  status: "in_progress",
26242
26585
  assigned_to: task2.assigned_to ?? agentId,
26243
26586
  agent_id: task2.agent_id ?? agentId,
@@ -26245,6 +26588,8 @@ async function startTask2(id, agentId, store) {
26245
26588
  locked_at: new Date().toISOString(),
26246
26589
  started_at: task2.started_at ?? new Date().toISOString()
26247
26590
  }, store);
26591
+ await logTaskChange2(task2.id, "start", "status", task2.status, "in_progress", agentId, store);
26592
+ return started;
26248
26593
  }
26249
26594
  async function completeTask2(id, agentId, options, store) {
26250
26595
  const task2 = await store.completeTask(id, agentId, options);
@@ -27353,17 +27698,17 @@ function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
27353
27698
  init_config();
27354
27699
  function createTodosStorageAdapter(options = {}) {
27355
27700
  const config = options.config ?? loadTodosStorageConfig(options.env);
27356
- if (config.mode === "local") {
27701
+ if (config.mode === "sqlite") {
27357
27702
  if (isTodosShadowEnabled(options.env))
27358
27703
  return createShadowAdapter(options, config);
27359
27704
  return createLocalSqliteTodosStorageAdapter(options.local);
27360
27705
  }
27361
27706
  assertTodosRemoteStorageConfig(config);
27362
- const adapter = config.mode === "hybrid" ? options.hybridAdapter ?? createImplicitHybridAdapter(options) ?? options.remoteAdapter : options.remoteAdapter ?? createImplicitPostgresAdapter(options);
27707
+ const adapter = options.remoteAdapter ?? createImplicitPostgresAdapter(options);
27363
27708
  if (!adapter) {
27364
- throw new Error(`${config.mode} storage requires a repo-native remote adapter. ` + "Pass remoteAdapter/hybridAdapter after wiring Postgres RDS and S3 support.");
27709
+ throw new Error("postgres storage requires a repo-native Postgres adapter. " + "Pass remoteAdapter or postgresClient after wiring Postgres RDS support.");
27365
27710
  }
27366
- assertRemoteAdapterCapabilities(adapter, config.mode);
27711
+ assertPostgresAdapterCapabilities(adapter);
27367
27712
  return adapter;
27368
27713
  }
27369
27714
  function createShadowAdapter(options, config) {
@@ -27377,7 +27722,7 @@ function createShadowAdapter(options, config) {
27377
27722
  ...options.local ? { local: options.local } : {},
27378
27723
  ...options.postgresSyncStore ? { syncStore: options.postgresSyncStore } : {},
27379
27724
  ...postgresClient ? { postgresClient } : {},
27380
- ...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
27725
+ ...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
27381
27726
  });
27382
27727
  }
27383
27728
  function createImplicitPostgresAdapter(options) {
@@ -27385,25 +27730,12 @@ function createImplicitPostgresAdapter(options) {
27385
27730
  return null;
27386
27731
  return createPostgresTodosStorageAdapter({
27387
27732
  client: options.postgresClient,
27388
- ...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
27389
- });
27390
- }
27391
- function createImplicitHybridAdapter(options) {
27392
- if (!options.postgresClient && !options.postgresSyncStore)
27393
- return null;
27394
- return createHybridTodosStorageAdapter({
27395
- ...options.hybrid ?? {},
27396
- local: options.local,
27397
- postgresClient: options.postgresClient,
27398
- syncStore: options.postgresSyncStore
27733
+ ...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
27399
27734
  });
27400
27735
  }
27401
- function assertRemoteAdapterCapabilities(adapter, mode) {
27736
+ function assertPostgresAdapterCapabilities(adapter) {
27402
27737
  if (!adapter.capabilities.remotePersistence) {
27403
- throw new Error(`${mode} storage adapter must set capabilities.remotePersistence=true`);
27404
- }
27405
- if (mode === "hybrid" && !adapter.capabilities.localPersistence) {
27406
- throw new Error("hybrid storage adapter must also set capabilities.localPersistence=true");
27738
+ throw new Error("postgres storage adapter must set capabilities.remotePersistence=true");
27407
27739
  }
27408
27740
  }
27409
27741
  // src/storage/shadow-outbox.ts
@@ -27730,6 +28062,50 @@ async function closeRuntimeShadowCloud() {
27730
28062
  await cloud.close().catch(() => {});
27731
28063
  }
27732
28064
  }
28065
+ // src/storage/hybrid.ts
28066
+ function createHybridTodosStorageAdapter(options) {
28067
+ const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
28068
+ const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
28069
+ if (!syncStore)
28070
+ throw new Error("hybrid storage requires a Postgres sync store or query client");
28071
+ if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
28072
+ throw new Error("hybrid storage requires local snapshot export/import support");
28073
+ }
28074
+ const exportSnapshot2 = async (context) => await local.sync.exportSnapshot(context);
28075
+ const importSnapshot2 = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
28076
+ const remote = {
28077
+ ensureSchema: () => syncStore.ensureSchema(),
28078
+ async pushSnapshot(context) {
28079
+ const snapshot = await exportSnapshot2(context);
28080
+ return syncStore.pushSnapshot(snapshot, context);
28081
+ },
28082
+ async pullSnapshot(options2, context) {
28083
+ const snapshot = await syncStore.pullSnapshot(options2);
28084
+ return importSnapshot2(snapshot, context);
28085
+ },
28086
+ async syncOnce(options2, context) {
28087
+ const pulled = await remote.pullSnapshot(options2, context);
28088
+ const pushed = await remote.pushSnapshot(context);
28089
+ return { pulled, pushed };
28090
+ }
28091
+ };
28092
+ return {
28093
+ ...local,
28094
+ kind: "hybrid",
28095
+ capabilities: {
28096
+ ...local.capabilities,
28097
+ localPersistence: true,
28098
+ remotePersistence: true,
28099
+ sync: true
28100
+ },
28101
+ sync: {
28102
+ ...local.sync,
28103
+ exportSnapshot: exportSnapshot2,
28104
+ importSnapshot: importSnapshot2
28105
+ },
28106
+ remote
28107
+ };
28108
+ }
27733
28109
  // src/storage/comment-redaction-backfill.ts
27734
28110
  init_redaction();
27735
28111
  var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
@@ -31308,10 +31684,10 @@ function getNativeStorageStatus(env = process.env) {
31308
31684
  } catch (error) {
31309
31685
  issues.push(error instanceof Error ? error.message : String(error));
31310
31686
  }
31311
- const remoteEnabled = isTodosRemoteStorageEnabled(config);
31687
+ const remoteEnabled = isTodosPostgresBackend(config);
31312
31688
  const remoteFieldsConfigured = Boolean(config.database || config.objectStorage);
31313
31689
  if (!remoteEnabled && remoteFieldsConfigured) {
31314
- warnings.push(`${TODOS_STORAGE_ENV.mode}=local ignores configured remote storage fields`);
31690
+ warnings.push(`the sqlite backend (${TODOS_STORAGE_ENV.mode}) ignores configured remote storage fields`);
31315
31691
  }
31316
31692
  if (remoteEnabled && !config.objectStorage) {
31317
31693
  warnings.push(`${TODOS_STORAGE_ENV.s3Bucket} is not configured, so artifact sync will stay local`);
@@ -31320,7 +31696,7 @@ function getNativeStorageStatus(env = process.env) {
31320
31696
  ok: issues.length === 0,
31321
31697
  service: "todos",
31322
31698
  mode: config.mode,
31323
- local_default: config.mode === "local",
31699
+ local_default: config.mode === "sqlite",
31324
31700
  remote_enabled: remoteEnabled,
31325
31701
  database: {
31326
31702
  configured: Boolean(config.database),
@@ -31402,8 +31778,12 @@ function storageEnvStatus(env) {
31402
31778
  ]));
31403
31779
  }
31404
31780
  function fallbackConfig(env) {
31405
- const modeValue = clean2(env[TODOS_STORAGE_ENV.mode]);
31406
- const mode = modeValue === "remote" || modeValue === "hybrid" ? modeValue : "local";
31781
+ let mode;
31782
+ try {
31783
+ mode = parseStorageBackend(clean2(env[TODOS_STORAGE_ENV.mode]));
31784
+ } catch {
31785
+ mode = "sqlite";
31786
+ }
31407
31787
  return {
31408
31788
  service: "todos",
31409
31789
  mode,
@@ -50582,7 +50962,7 @@ function parseObject4(value) {
50582
50962
  function normalizeKey(value) {
50583
50963
  return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
50584
50964
  }
50585
- function normalizeFingerprint(value) {
50965
+ function normalizeTaskFindingFingerprint(value) {
50586
50966
  const normalized = normalizeKey(value);
50587
50967
  if (!normalized)
50588
50968
  throw new Error("finding fingerprint is required");
@@ -50691,7 +51071,7 @@ function assertTask(taskId, db) {
50691
51071
  throw new TaskNotFoundError(taskId);
50692
51072
  }
50693
51073
  function nextFinding(input, db) {
50694
- const fingerprint4 = normalizeFingerprint(input.fingerprint);
51074
+ const fingerprint4 = normalizeTaskFindingFingerprint(input.fingerprint);
50695
51075
  const title = redactOptional(input.title, 300);
50696
51076
  if (!title)
50697
51077
  throw new Error("finding title is required");
@@ -50839,7 +51219,7 @@ function resolveMissingTaskFindings(input, db) {
50839
51219
  const timestamp3 = input.resolved_at || now();
50840
51220
  const status = normalizeResolutionStatus(input.status);
50841
51221
  const runId = resolveRunForTask(input.run_id, input.task_id, d);
50842
- const present = new Set(input.fingerprints.map(normalizeFingerprint));
51222
+ const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
50843
51223
  const warnings = [];
50844
51224
  const conditions = ["task_id = ?", "status = 'open'"];
50845
51225
  const params = [input.task_id];
@@ -51366,12 +51746,12 @@ init_artifact_store();
51366
51746
 
51367
51747
  // src/lib/doctor.ts
51368
51748
  init_database();
51749
+ import { chmodSync, copyFileSync as copyFileSync2, existsSync as existsSync30, mkdirSync as mkdirSync22, statSync as statSync12 } from "fs";
51750
+ import { basename as basename7, dirname as dirname18, join as join27 } from "path";
51369
51751
  init_migrations();
51370
51752
  init_schema();
51371
51753
  init_recurrence();
51372
- import { chmodSync, copyFileSync as copyFileSync2, existsSync as existsSync30, mkdirSync as mkdirSync22, statSync as statSync12 } from "fs";
51373
- import { basename as basename7, dirname as dirname18, join as join27 } from "path";
51374
- var REQUIRED_TABLES2 = [
51754
+ var REQUIRED_TABLES3 = [
51375
51755
  "_migrations",
51376
51756
  "projects",
51377
51757
  "tasks",
@@ -51384,7 +51764,7 @@ var REQUIRED_TABLES2 = [
51384
51764
  "task_run_commands",
51385
51765
  "task_run_artifacts"
51386
51766
  ];
51387
- function tableExists(db, table) {
51767
+ function tableExists2(db, table) {
51388
51768
  return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
51389
51769
  }
51390
51770
  function quoteIdent(identifier) {
@@ -51468,7 +51848,7 @@ function findDuplicateIndexes(db) {
51468
51848
  return duplicates;
51469
51849
  }
51470
51850
  function findMissingProjectRoots(db) {
51471
- if (!tableExists(db, "projects"))
51851
+ if (!tableExists2(db, "projects"))
51472
51852
  return 0;
51473
51853
  let missing = 0;
51474
51854
  const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
@@ -51483,7 +51863,7 @@ function findMissingProjectRoots(db) {
51483
51863
  return missing;
51484
51864
  }
51485
51865
  function addTaskStateChecks(db, checks) {
51486
- if (!tableExists(db, "tasks"))
51866
+ if (!tableExists2(db, "tasks"))
51487
51867
  return;
51488
51868
  const columns = getTableColumns(db, "tasks");
51489
51869
  const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
@@ -51555,15 +51935,42 @@ function createBackup(dbPath) {
51555
51935
  function pushRepair(repairs, type, message, applied, count3) {
51556
51936
  repairs.push({ type, message, applied, count: count3 });
51557
51937
  }
51558
- function summarize3(checks, repairs) {
51938
+ function summarize3(checks, repairs, integrity) {
51559
51939
  return {
51560
51940
  errors: checks.filter((check) => check.severity === "error").length,
51561
51941
  warnings: checks.filter((check) => check.severity === "warn").length,
51562
51942
  infos: checks.filter((check) => check.severity === "info").length,
51563
51943
  repairable: checks.filter((check) => check.repairable).length,
51564
- applied: repairs.filter((repair) => repair.applied).length
51944
+ applied: repairs.filter((repair) => repair.applied).length,
51945
+ integrity_findings: integrity.summary.findings,
51946
+ integrity_rows: integrity.summary.rows,
51947
+ integrity_unverified: integrity.summary.unverified
51565
51948
  };
51566
51949
  }
51950
+ function addIntegrityChecks(integrity, checks) {
51951
+ for (const condition of integrity.conditions) {
51952
+ if (!condition.verified) {
51953
+ addCheck(checks, {
51954
+ severity: "warn",
51955
+ type: `${condition.id}_unverified`,
51956
+ message: condition.message,
51957
+ repairable: false,
51958
+ integrity: true
51959
+ });
51960
+ continue;
51961
+ }
51962
+ if ((condition.count ?? 0) === 0)
51963
+ continue;
51964
+ addCheck(checks, {
51965
+ severity: condition.severity ?? "warn",
51966
+ type: condition.id,
51967
+ message: `${condition.message} \u2014 ${condition.impact}`,
51968
+ count: condition.count ?? undefined,
51969
+ repairable: false,
51970
+ integrity: true
51971
+ });
51972
+ }
51973
+ }
51567
51974
  function deleteOrphans(db, table, where) {
51568
51975
  const before = countQuery2(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
51569
51976
  if (before > 0)
@@ -51592,7 +51999,7 @@ function runTodosDoctor(options = {}) {
51592
51999
  message: `Schema at migration ${migrationCurrent}`
51593
52000
  });
51594
52001
  }
51595
- const missingTables = REQUIRED_TABLES2.filter((table) => !tableExists(db, table));
52002
+ const missingTables = REQUIRED_TABLES3.filter((table) => !tableExists2(db, table));
51596
52003
  if (missingTables.length > 0) {
51597
52004
  addCheck(checks, {
51598
52005
  severity: "error",
@@ -51602,7 +52009,7 @@ function runTodosDoctor(options = {}) {
51602
52009
  repairable: true
51603
52010
  });
51604
52011
  }
51605
- const orphanedParents = tableExists(db, "tasks") ? countQuery2(db, "SELECT COUNT(*) as count FROM tasks t WHERE t.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = t.parent_id)") : 0;
52012
+ const orphanedParents = tableExists2(db, "tasks") ? countQuery2(db, "SELECT COUNT(*) as count FROM tasks t WHERE t.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = t.parent_id)") : 0;
51606
52013
  if (orphanedParents > 0) {
51607
52014
  addCheck(checks, {
51608
52015
  severity: "error",
@@ -51612,7 +52019,7 @@ function runTodosDoctor(options = {}) {
51612
52019
  repairable: true
51613
52020
  });
51614
52021
  }
51615
- const orphanedDependencies = tableExists(db, "task_dependencies") && tableExists(db, "tasks") ? countQuery2(db, "SELECT COUNT(*) as count FROM task_dependencies d WHERE NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.depends_on)") : 0;
52022
+ const orphanedDependencies = tableExists2(db, "task_dependencies") && tableExists2(db, "tasks") ? countQuery2(db, "SELECT COUNT(*) as count FROM task_dependencies d WHERE NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.depends_on)") : 0;
51616
52023
  if (orphanedDependencies > 0) {
51617
52024
  addCheck(checks, {
51618
52025
  severity: "error",
@@ -51631,7 +52038,7 @@ function runTodosDoctor(options = {}) {
51631
52038
  ];
51632
52039
  let orphanedRows2 = 0;
51633
52040
  for (const [table, where] of orphanTables) {
51634
- if (!tableExists(db, table))
52041
+ if (!tableExists2(db, table))
51635
52042
  continue;
51636
52043
  orphanedRows2 += countQuery2(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
51637
52044
  }
@@ -51645,6 +52052,8 @@ function runTodosDoctor(options = {}) {
51645
52052
  });
51646
52053
  }
51647
52054
  addTaskStateChecks(db, checks);
52055
+ const integrity = scanSqliteIntegrity(db);
52056
+ addIntegrityChecks(integrity, checks);
51648
52057
  const corruptJson = findCorruptJsonMetadata(db);
51649
52058
  if (corruptJson.length > 0) {
51650
52059
  addCheck(checks, {
@@ -51703,12 +52112,12 @@ function runTodosDoctor(options = {}) {
51703
52112
  db.run("UPDATE tasks SET parent_id = NULL WHERE parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = tasks.parent_id)");
51704
52113
  pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
51705
52114
  }
51706
- if (orphanedDependencies > 0 && tableExists(db, "task_dependencies")) {
52115
+ if (orphanedDependencies > 0 && tableExists2(db, "task_dependencies")) {
51707
52116
  const count3 = deleteOrphans(db, "task_dependencies", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.depends_on)");
51708
52117
  pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count3);
51709
52118
  }
51710
52119
  for (const [table, where] of orphanTables) {
51711
- if (!tableExists(db, table))
52120
+ if (!tableExists2(db, table))
51712
52121
  continue;
51713
52122
  const count3 = deleteOrphans(db, table, where);
51714
52123
  if (count3 > 0)
@@ -51737,16 +52146,19 @@ function runTodosDoctor(options = {}) {
51737
52146
  }
51738
52147
  }
51739
52148
  }
51740
- const finalChecks = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }).checks : checks;
51741
- const summary = summarize3(finalChecks, repairs);
52149
+ const reread = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }) : undefined;
52150
+ const finalChecks = reread?.checks ?? checks;
52151
+ const finalIntegrity = reread?.integrity ?? integrity;
52152
+ const summary = summarize3(finalChecks, repairs, finalIntegrity);
51742
52153
  return {
51743
- ok: !finalChecks.some((check) => check.severity === "error"),
52154
+ ok: !finalChecks.some((check) => check.severity === "error") && finalIntegrity.summary.ok,
51744
52155
  dry_run: !apply,
51745
52156
  database_path: dbPath,
51746
52157
  migration: { current: getMigrationLevel(db), expected: migrationExpected },
51747
52158
  backup,
51748
52159
  checks: finalChecks,
51749
52160
  repairs,
52161
+ integrity: finalIntegrity,
51750
52162
  summary
51751
52163
  };
51752
52164
  }
@@ -52292,6 +52704,7 @@ export {
52292
52704
  updateArtifactRedaction,
52293
52705
  updateAgentActivity,
52294
52706
  updateAgent,
52707
+ unverifiedCondition,
52295
52708
  unsubscribeResource,
52296
52709
  unlockTask,
52297
52710
  unlinkTaskCommit,
@@ -52312,6 +52725,7 @@ export {
52312
52725
  summarizeTaskRun,
52313
52726
  summarizeRoadmap,
52314
52727
  summarizeMilestone,
52728
+ summarizeIntegrity,
52315
52729
  suggestAgentNames,
52316
52730
  subscribeResource,
52317
52731
  storeArtifactContent,
@@ -52352,6 +52766,7 @@ export {
52352
52766
  scorePlanHealth,
52353
52767
  scheduleTask,
52354
52768
  scanTextForSecrets,
52769
+ scanSqliteIntegrity,
52355
52770
  scanSourceForForbiddenWebPatterns,
52356
52771
  scanReminders,
52357
52772
  scanPreWriteText,
@@ -52399,6 +52814,7 @@ export {
52399
52814
  resolvePartialId,
52400
52815
  resolveMissingTaskFindings,
52401
52816
  resolveMentions,
52817
+ resolveIntegritySeverity,
52402
52818
  resolveGitRoot,
52403
52819
  resolveCommandQuery,
52404
52820
  resolveAccessProfile,
@@ -52513,6 +52929,7 @@ export {
52513
52929
  patrolTasks,
52514
52930
  parseTmuxTarget,
52515
52931
  parseStorageMode,
52932
+ parseStorageBackend,
52516
52933
  parseRecurrenceRule,
52517
52934
  parseQuietHours,
52518
52935
  parsePlanArtifactMarkdown,
@@ -52541,6 +52958,8 @@ export {
52541
52958
  migrationDryRun,
52542
52959
  migrateWorkflowStates,
52543
52960
  mergeDuplicateTask,
52961
+ measuredCondition,
52962
+ measureIntegrityRows,
52544
52963
  materializePlanSteps,
52545
52964
  matchCapabilities,
52546
52965
  looksSensitiveKey,
@@ -52659,12 +53078,14 @@ export {
52659
53078
  isWorktreePath,
52660
53079
  isValidRecurrenceRule,
52661
53080
  isTodosRemoteStorageEnabled,
53081
+ isTodosPostgresBackend,
52662
53082
  isSnapshotStale,
52663
53083
  isEncryptedValue,
52664
53084
  isEncryptedBridgeBundle,
52665
53085
  isArtifactExpired,
52666
53086
  isAllowedLocalApiUrl,
52667
53087
  isAgentConflict,
53088
+ integrityCondition,
52668
53089
  installTemplateLibrary,
52669
53090
  installLocalExtension,
52670
53091
  inspectPlanArtifact,
@@ -52707,6 +53128,7 @@ export {
52707
53128
  getTodosStorageEnvName,
52708
53129
  getTodosStorageDatabaseUrl,
52709
53130
  getTodosStorageDatabaseEnv,
53131
+ getTodosStorageBackend,
52710
53132
  getTimeReport,
52711
53133
  getTimeLogs,
52712
53134
  getTerminalNotificationRule,
@@ -52914,6 +53336,7 @@ export {
52914
53336
  formatLockConflict,
52915
53337
  formatKnowledgeSnapshotMarkdown,
52916
53338
  formatIssueImportPreviewText,
53339
+ formatIntegrityMessage,
52917
53340
  formatIntakePreviewText,
52918
53341
  formatHandoffPacket,
52919
53342
  formatGoalHandoff,
@@ -53144,16 +53567,19 @@ export {
53144
53567
  bulkAddTaskFiles,
53145
53568
  buildTaskDependencyEdges,
53146
53569
  buildTaskBoardSnapshot,
53570
+ buildSqliteIntegritySql,
53147
53571
  buildS3ObjectUrl,
53148
53572
  buildS3ObjectKey,
53149
53573
  buildRunReplayBundle,
53150
53574
  buildResourceSnapshot,
53151
53575
  buildReportExportData,
53152
53576
  buildProjectDependencyGraph,
53577
+ buildPostgresIntegritySql,
53153
53578
  buildPlanArtifactSnapshot,
53154
53579
  buildMcpToolGroups,
53155
53580
  buildMachineTopologyReport,
53156
53581
  buildKnowledgeSnapshotPayload,
53582
+ buildIntegrityReport,
53157
53583
  buildHandoffPacket,
53158
53584
  buildFeatureManifest,
53159
53585
  buildFailureTriageReport,
@@ -53188,6 +53614,7 @@ export {
53188
53614
  appendRunCommand,
53189
53615
  analyzeDependencyGraph,
53190
53616
  agentClaimNextSafe,
53617
+ adoptRemoteIntegrityReport,
53191
53618
  addTemplateTasks,
53192
53619
  addTaskRunFile,
53193
53620
  addTaskRunEvent,
@@ -53234,6 +53661,7 @@ export {
53234
53661
  TODOS_LOCAL_BACKUP_KIND,
53235
53662
  TODOS_JSON_CONTRACTS_MANIFEST,
53236
53663
  TODOS_JSON_CONTRACTS,
53664
+ TODOS_INTEGRITY_SCHEMA_VERSION,
53237
53665
  TODOS_ERROR_CODES,
53238
53666
  TODOS_ENCRYPTION_SCHEMA_VERSION,
53239
53667
  TODOS_ENCRYPTED_VALUE_KIND,
@@ -53290,6 +53718,7 @@ export {
53290
53718
  PLAN_EXECUTION_SCHEMA,
53291
53719
  PLAN_EXECUTION_MODES,
53292
53720
  PARITY_SCHEMA_VERSION,
53721
+ OPEN_TASK_STATUSES,
53293
53722
  NOTIFICATION_REMINDERS_SCHEMA,
53294
53723
  NL_INTAKE_SCHEMA,
53295
53724
  NESTED_SUBCOMMANDS,
@@ -53318,6 +53747,7 @@ export {
53318
53747
  JSON_OUTPUT_CONTRACT,
53319
53748
  ISSUE_SOURCES,
53320
53749
  ISSUE_IMPORT_SCHEMA,
53750
+ INTEGRITY_CONDITIONS,
53321
53751
  INTAKE_TRIAGE_STATUSES,
53322
53752
  INTAKE_SOURCE_TYPES,
53323
53753
  INBOX_INTAKE_SCHEMA,