@hasna/todos 0.13.2 → 0.13.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +78 -14
  2. package/dist/cli/cloud-router.d.ts +100 -7
  3. package/dist/cli/cloud-router.d.ts.map +1 -1
  4. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  6. package/dist/cli/doctor-integrity.d.ts +106 -0
  7. package/dist/cli/doctor-integrity.d.ts.map +1 -0
  8. package/dist/cli/helpers.d.ts +2 -2
  9. package/dist/cli/index.js +1509 -786
  10. package/dist/cli/stage-a.d.ts.map +1 -1
  11. package/dist/contracts.js +118 -30
  12. package/dist/db/agent-names.d.ts +2 -1
  13. package/dist/db/agent-names.d.ts.map +1 -1
  14. package/dist/db/database.d.ts.map +1 -1
  15. package/dist/db/findings.d.ts +1 -0
  16. package/dist/db/findings.d.ts.map +1 -1
  17. package/dist/db/integrity.d.ts +20 -0
  18. package/dist/db/integrity.d.ts.map +1 -0
  19. package/dist/index.d.ts +5 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +584 -140
  22. package/dist/lib/agent-name-normalize.d.ts +17 -0
  23. package/dist/lib/agent-name-normalize.d.ts.map +1 -0
  24. package/dist/lib/context-packs.d.ts +1 -0
  25. package/dist/lib/context-packs.d.ts.map +1 -1
  26. package/dist/lib/dependency-graph.d.ts +1 -1
  27. package/dist/lib/doctor.d.ts +20 -0
  28. package/dist/lib/doctor.d.ts.map +1 -1
  29. package/dist/lib/integrity.d.ts +215 -0
  30. package/dist/lib/integrity.d.ts.map +1 -0
  31. package/dist/lib/native-storage-status.d.ts +2 -2
  32. package/dist/lib/native-storage-status.d.ts.map +1 -1
  33. package/dist/lib/release-compatibility.d.ts.map +1 -1
  34. package/dist/lib/task-dedupe.d.ts +15 -0
  35. package/dist/lib/task-dedupe.d.ts.map +1 -1
  36. package/dist/mcp/index.d.ts.map +1 -1
  37. package/dist/mcp/index.js +554 -92
  38. package/dist/mcp.js +7 -3
  39. package/dist/registry.d.ts +1 -1
  40. package/dist/registry.d.ts.map +1 -1
  41. package/dist/registry.js +125 -30
  42. package/dist/release-provenance.json +5 -5
  43. package/dist/sdk/types.d.ts +42 -0
  44. package/dist/sdk/types.d.ts.map +1 -1
  45. package/dist/sdk/v1.generated.d.ts +1 -0
  46. package/dist/sdk/v1.generated.d.ts.map +1 -1
  47. package/dist/server/cloud.d.ts +8 -1
  48. package/dist/server/cloud.d.ts.map +1 -1
  49. package/dist/server/index.js +560 -98
  50. package/dist/server/openapi.d.ts +12 -0
  51. package/dist/server/openapi.d.ts.map +1 -1
  52. package/dist/server/serve.d.ts.map +1 -1
  53. package/dist/server/v1.d.ts.map +1 -1
  54. package/dist/storage/config.d.ts +23 -5
  55. package/dist/storage/config.d.ts.map +1 -1
  56. package/dist/storage/factory.d.ts +9 -3
  57. package/dist/storage/factory.d.ts.map +1 -1
  58. package/dist/storage/hybrid.d.ts +7 -0
  59. package/dist/storage/hybrid.d.ts.map +1 -1
  60. package/dist/storage/index.d.ts +3 -3
  61. package/dist/storage/index.d.ts.map +1 -1
  62. package/dist/storage/interfaces.d.ts +29 -8
  63. package/dist/storage/interfaces.d.ts.map +1 -1
  64. package/dist/storage/local-sqlite.d.ts.map +1 -1
  65. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  66. package/dist/storage.d.ts +3 -3
  67. package/dist/storage.d.ts.map +1 -1
  68. package/dist/storage.js +623 -258
  69. package/dist/testing.d.ts +79 -0
  70. package/dist/testing.d.ts.map +1 -0
  71. package/dist/testing.js +107 -0
  72. package/dist/types/index.d.ts +12 -1
  73. package/dist/types/index.d.ts.map +1 -1
  74. package/package.json +7 -3
package/dist/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.4",
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) {
@@ -23798,6 +23896,11 @@ init_database();
23798
23896
  init_identity_mapping();
23799
23897
  init_storage_tombstones();
23800
23898
 
23899
+ // src/lib/agent-name-normalize.ts
23900
+ function normalizeAgentNameInput(name) {
23901
+ return name.trim().toLowerCase();
23902
+ }
23903
+
23801
23904
  // src/db/agent-names.ts
23802
23905
  class InvalidAgentNameError extends Error {
23803
23906
  suggestions;
@@ -23942,9 +24045,6 @@ var RESERVED_GENERIC_NAMES = new Set([
23942
24045
  ]);
23943
24046
  var NUMERIC_SUFFIX_RE = /[-_]\d+$/;
23944
24047
  var ONE_WORD_NAME_RE = /^[a-z]+$/;
23945
- function normalizeAgentNameInput(name) {
23946
- return name.trim().toLowerCase();
23947
- }
23948
24048
  function hasGeneratedNumericSuffix(name) {
23949
24049
  return NUMERIC_SUFFIX_RE.test(normalizeAgentNameInput(name));
23950
24050
  }
@@ -24449,6 +24549,267 @@ init_audit();
24449
24549
  init_comments();
24450
24550
  init_database();
24451
24551
 
24552
+ // src/db/integrity.ts
24553
+ init_database();
24554
+
24555
+ // src/lib/integrity.ts
24556
+ init_types();
24557
+ var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1";
24558
+ var TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
24559
+ var OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
24560
+ var INTEGRITY_CONDITIONS = [
24561
+ {
24562
+ id: "tasks_without_project",
24563
+ entity: "task",
24564
+ field: "project_id",
24565
+ target: "project",
24566
+ kind: "missing",
24567
+ base_severity: "warn",
24568
+ escalate_when_open: true,
24569
+ impact: "invisible to every project-scoped read (list, status, next, claim)"
24570
+ },
24571
+ {
24572
+ id: "tasks_without_task_list",
24573
+ entity: "task",
24574
+ field: "task_list_id",
24575
+ target: "task_list",
24576
+ kind: "missing",
24577
+ base_severity: "warn",
24578
+ escalate_when_open: true,
24579
+ impact: "invisible to every task-list read \u2014 the list reports zero open work"
24580
+ },
24581
+ {
24582
+ id: "tasks_with_unregistered_project",
24583
+ entity: "task",
24584
+ field: "project_id",
24585
+ target: "project",
24586
+ kind: "dangling",
24587
+ base_severity: "error",
24588
+ escalate_when_open: false,
24589
+ impact: "points at a project id that does not exist; the reference can never resolve"
24590
+ },
24591
+ {
24592
+ id: "tasks_with_unregistered_task_list",
24593
+ entity: "task",
24594
+ field: "task_list_id",
24595
+ target: "task_list",
24596
+ kind: "dangling",
24597
+ base_severity: "error",
24598
+ escalate_when_open: false,
24599
+ impact: "points at a task-list id that does not exist; the reference can never resolve"
24600
+ },
24601
+ {
24602
+ id: "task_lists_without_project",
24603
+ entity: "task_list",
24604
+ field: "project_id",
24605
+ target: "project",
24606
+ kind: "missing",
24607
+ base_severity: "warn",
24608
+ escalate_when_open: false,
24609
+ impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
24610
+ },
24611
+ {
24612
+ id: "task_lists_with_unregistered_project",
24613
+ entity: "task_list",
24614
+ field: "project_id",
24615
+ target: "project",
24616
+ kind: "dangling",
24617
+ base_severity: "error",
24618
+ escalate_when_open: false,
24619
+ impact: "points at a project id that does not exist; the list can never be reached"
24620
+ }
24621
+ ];
24622
+ function integrityCondition(id) {
24623
+ const spec = INTEGRITY_CONDITIONS.find((condition) => condition.id === id);
24624
+ if (!spec)
24625
+ throw new Error(`unknown integrity condition: ${id}`);
24626
+ return spec;
24627
+ }
24628
+ function resolveIntegritySeverity(spec, measurement) {
24629
+ if (spec.kind === "dangling")
24630
+ return "error";
24631
+ if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
24632
+ return "error";
24633
+ return spec.base_severity;
24634
+ }
24635
+ var ENTITY_LABEL = {
24636
+ task: ["task", "tasks"],
24637
+ task_list: ["task list", "task lists"]
24638
+ };
24639
+ function plural(entity, count2) {
24640
+ const [one, many] = ENTITY_LABEL[entity];
24641
+ return count2 === 1 ? one : many;
24642
+ }
24643
+ function formatIntegrityMessage(spec, measurement) {
24644
+ const noun = plural(spec.entity, measurement.count);
24645
+ const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
24646
+ const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
24647
+ 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}`;
24648
+ }
24649
+ function measuredCondition(spec, measurement, source9) {
24650
+ return {
24651
+ id: spec.id,
24652
+ entity: spec.entity,
24653
+ field: spec.field,
24654
+ kind: spec.kind,
24655
+ count: measurement.count,
24656
+ open_count: measurement.open_count,
24657
+ severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
24658
+ verified: true,
24659
+ source: source9,
24660
+ message: formatIntegrityMessage(spec, measurement),
24661
+ impact: spec.impact
24662
+ };
24663
+ }
24664
+ function unverifiedCondition(spec, reason) {
24665
+ return {
24666
+ id: spec.id,
24667
+ entity: spec.entity,
24668
+ field: spec.field,
24669
+ kind: spec.kind,
24670
+ count: null,
24671
+ open_count: null,
24672
+ severity: null,
24673
+ verified: false,
24674
+ source: "unverified",
24675
+ unverified_reason: reason,
24676
+ message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
24677
+ impact: spec.impact
24678
+ };
24679
+ }
24680
+ function summarizeIntegrity(conditions) {
24681
+ const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
24682
+ const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
24683
+ const unverified = conditions.length - measured.length;
24684
+ return {
24685
+ ok: unverified === 0 && findings.length === 0,
24686
+ findings: findings.length,
24687
+ rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
24688
+ errors: findings.filter((condition) => condition.severity === "error").length,
24689
+ warnings: findings.filter((condition) => condition.severity === "warn").length,
24690
+ unverified,
24691
+ complete: unverified === 0
24692
+ };
24693
+ }
24694
+ function buildIntegrityReport(conditions, generatedAt) {
24695
+ const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source9) => source9 !== "unverified");
24696
+ return {
24697
+ schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
24698
+ generated_at: generatedAt,
24699
+ source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
24700
+ conditions,
24701
+ summary: summarizeIntegrity(conditions)
24702
+ };
24703
+ }
24704
+ function adoptRemoteIntegrityReport(raw, fallbackGeneratedAt) {
24705
+ const received = new Map;
24706
+ if (Array.isArray(raw.conditions)) {
24707
+ for (const entry of raw.conditions) {
24708
+ if (entry && typeof entry["id"] === "string")
24709
+ received.set(entry["id"], entry);
24710
+ }
24711
+ }
24712
+ const conditions = INTEGRITY_CONDITIONS.map((spec) => {
24713
+ const entry = received.get(spec.id);
24714
+ const count2 = entry?.["count"];
24715
+ const verified = entry?.["verified"];
24716
+ if (!entry || verified === false || typeof count2 !== "number" || !Number.isFinite(count2)) {
24717
+ 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");
24718
+ }
24719
+ const openRaw = entry["open_count"];
24720
+ return measuredCondition(spec, {
24721
+ count: count2,
24722
+ open_count: spec.entity === "task" ? typeof openRaw === "number" && Number.isFinite(openRaw) ? openRaw : 0 : null
24723
+ }, typeof raw.source === "string" && raw.source === "postgres" ? "postgres" : "remote-authority");
24724
+ });
24725
+ const report = buildIntegrityReport(conditions, typeof raw.generated_at === "string" ? raw.generated_at : fallbackGeneratedAt);
24726
+ return typeof raw.source === "string" && (raw.source === "sqlite" || raw.source === "postgres") ? { ...report, source: raw.source } : report;
24727
+ }
24728
+ var SQLITE_TABLE = {
24729
+ task: "tasks",
24730
+ task_list: "task_lists",
24731
+ project: "projects"
24732
+ };
24733
+ function sqliteOpenStatusList() {
24734
+ return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
24735
+ }
24736
+ function buildSqliteIntegritySql(spec) {
24737
+ const table = SQLITE_TABLE[spec.entity];
24738
+ const target = SQLITE_TABLE[spec.target];
24739
+ const column = `t."${spec.field}"`;
24740
+ 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})`;
24741
+ const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
24742
+ return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
24743
+ }
24744
+ var RECORD_TYPE = {
24745
+ task: "tasks",
24746
+ task_list: "task_lists",
24747
+ project: "projects"
24748
+ };
24749
+ function buildPostgresIntegritySql(spec, options) {
24750
+ const params = [options.service, RECORD_TYPE[spec.entity]];
24751
+ const p = (value) => {
24752
+ params.push(value);
24753
+ return `$${params.length}`;
24754
+ };
24755
+ const column = `t.payload->>'${spec.field}'`;
24756
+ 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})`;
24757
+ const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
24758
+ 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}`;
24759
+ return { sql, params };
24760
+ }
24761
+ function referenceOf(row, field2) {
24762
+ const raw = row[field2];
24763
+ if (typeof raw !== "string")
24764
+ return null;
24765
+ const trimmed = raw.trim();
24766
+ return trimmed === "" ? null : trimmed;
24767
+ }
24768
+ function measureIntegrityRows(spec, sets) {
24769
+ const rows = spec.entity === "task" ? sets.tasks : sets.taskLists;
24770
+ if (!rows)
24771
+ return null;
24772
+ const registered = spec.kind === "dangling" ? spec.target === "project" ? sets.projectIds : sets.taskListIds : undefined;
24773
+ if (spec.kind === "dangling" && !registered)
24774
+ return null;
24775
+ let count2 = 0;
24776
+ let open = 0;
24777
+ for (const row of rows) {
24778
+ const reference = referenceOf(row, spec.field);
24779
+ const matches = spec.kind === "missing" ? reference === null : reference !== null && !registered.has(reference);
24780
+ if (!matches)
24781
+ continue;
24782
+ count2++;
24783
+ if (spec.entity === "task") {
24784
+ const status = row.status;
24785
+ if (typeof status === "string" && OPEN_TASK_STATUSES.includes(status))
24786
+ open++;
24787
+ }
24788
+ }
24789
+ return { count: count2, open_count: spec.entity === "task" ? open : null };
24790
+ }
24791
+
24792
+ // src/db/integrity.ts
24793
+ var REQUIRED_TABLES2 = { tasks: true, task_lists: true, projects: true };
24794
+ function tableExists(db, table) {
24795
+ return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
24796
+ }
24797
+ function scanSqliteIntegrity(db = getDatabase()) {
24798
+ const missing = Object.keys(REQUIRED_TABLES2).filter((table) => !tableExists(db, table));
24799
+ const conditions = INTEGRITY_CONDITIONS.map((spec) => {
24800
+ if (missing.length > 0) {
24801
+ return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
24802
+ }
24803
+ try {
24804
+ const row = db.query(buildSqliteIntegritySql(spec)).get();
24805
+ return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
24806
+ } catch (error) {
24807
+ return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
24808
+ }
24809
+ });
24810
+ return buildIntegrityReport(conditions, now());
24811
+ }
24812
+
24452
24813
  // src/storage/sqlite-snapshot.ts
24453
24814
  init_database();
24454
24815
  init_audit();
@@ -24840,15 +25201,15 @@ function resolveTaskRefLocal(db, ref) {
24840
25201
  return null;
24841
25202
  if (TASK_UUID_RE.test(raw))
24842
25203
  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}`)}%`);
25204
+ 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
25205
  if (prefixRows.length > 1) {
24845
- throw new Error(`Task reference is ambiguous: "${ref}"`);
25206
+ throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
24846
25207
  }
24847
25208
  if (prefixRows.length === 1)
24848
25209
  return getTask(prefixRows[0].id, db);
24849
- const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
25210
+ const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
24850
25211
  if (shortIdRows.length > 1) {
24851
- throw new Error(`Task reference is ambiguous: "${ref}"`);
25212
+ throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
24852
25213
  }
24853
25214
  if (shortIdRows.length === 1)
24854
25215
  return getTask(shortIdRows[0].id, db);
@@ -24997,6 +25358,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
24997
25358
  exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
24998
25359
  importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
24999
25360
  },
25361
+ integrity: {
25362
+ report: () => scanSqliteIntegrity(database())
25363
+ },
25000
25364
  transaction: (fn) => {
25001
25365
  const tx = database().transaction(() => fn(adapter));
25002
25366
  return tx();
@@ -25005,6 +25369,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
25005
25369
  return adapter;
25006
25370
  }
25007
25371
 
25372
+ // src/storage/postgres-adapter.ts
25373
+ init_types();
25374
+ import { randomUUID as randomUUID3 } from "crypto";
25375
+
25008
25376
  // src/storage/postgres-sync.ts
25009
25377
  var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
25010
25378
  var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
@@ -25394,54 +25762,7 @@ function assertSafeIdentifier(value) {
25394
25762
  throw new Error(`Unsafe Postgres identifier: ${value}`);
25395
25763
  }
25396
25764
 
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
25765
  // src/storage/postgres-adapter.ts
25443
- init_types();
25444
- import { randomUUID as randomUUID3 } from "crypto";
25445
25766
  init_redaction();
25446
25767
  function createPostgresTodosStorageAdapter(options) {
25447
25768
  const store = new PostgresJsonRecordStore(options);
@@ -25512,7 +25833,7 @@ function createPostgresTodosStorageAdapter(options) {
25512
25833
  agents: {
25513
25834
  register: (input, context) => registerAgent2(input, store, context),
25514
25835
  get: (id) => store.get("agents", id),
25515
- getByName: async (name) => (await store.list("agents")).find((agent) => agent.name === name) ?? null,
25836
+ getByName: async (name) => matchAgentByName(await store.list("agents"), name),
25516
25837
  list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
25517
25838
  update: (id, input) => updateAgent2(id, input, store),
25518
25839
  heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
@@ -25569,6 +25890,9 @@ function createPostgresTodosStorageAdapter(options) {
25569
25890
  exportSnapshot: () => exportSnapshot(store),
25570
25891
  importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
25571
25892
  },
25893
+ integrity: {
25894
+ report: () => store.integrityReport()
25895
+ },
25572
25896
  transaction: (fn) => fn(adapter)
25573
25897
  };
25574
25898
  return adapter;
@@ -25675,8 +25999,9 @@ class PostgresJsonRecordStore {
25675
25999
  conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
25676
26000
  if (filter.session_id !== undefined)
25677
26001
  conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
25678
- if (filter.tags?.length)
25679
- conds.push(`payload->'tags' @> ${p(filter.tags)}::jsonb`);
26002
+ if (filter.tags?.length) {
26003
+ 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)})`);
26004
+ }
25680
26005
  if (filter.has_recurrence !== undefined) {
25681
26006
  conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
25682
26007
  }
@@ -25737,7 +26062,8 @@ class PostgresJsonRecordStore {
25737
26062
  AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
25738
26063
  LIMIT 2`, [this.service, raw, upper]);
25739
26064
  if (prefixResult.rows.length > 1) {
25740
- throw new Error(`Task reference is ambiguous: "${ref}"`);
26065
+ const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
26066
+ throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
25741
26067
  }
25742
26068
  if (prefixResult.rows.length === 1) {
25743
26069
  return payloadRecord2(prefixResult.rows[0].payload);
@@ -25748,7 +26074,8 @@ class PostgresJsonRecordStore {
25748
26074
  AND LOWER(payload->>'short_id') = $2
25749
26075
  LIMIT 2`, [this.service, raw]);
25750
26076
  if (shortIdResult.rows.length > 1) {
25751
- throw new Error(`Task reference is ambiguous: "${ref}"`);
26077
+ const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
26078
+ throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
25752
26079
  }
25753
26080
  if (shortIdResult.rows.length === 1) {
25754
26081
  return payloadRecord2(shortIdResult.rows[0].payload);
@@ -25762,6 +26089,24 @@ class PostgresJsonRecordStore {
25762
26089
  const result = await this.options.client.query(sql, params);
25763
26090
  return Number(result.rows[0]?.count ?? 0);
25764
26091
  }
26092
+ async integrityReport() {
26093
+ await this.ensureSchema();
26094
+ const conditions = [];
26095
+ for (const spec of INTEGRITY_CONDITIONS) {
26096
+ const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
26097
+ try {
26098
+ const result = await this.options.client.query(sql, params);
26099
+ const row = result.rows[0];
26100
+ conditions.push(measuredCondition(spec, {
26101
+ count: Number(row?.count ?? 0),
26102
+ open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
26103
+ }, "postgres"));
26104
+ } catch (error) {
26105
+ conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
26106
+ }
26107
+ }
26108
+ return buildIntegrityReport(conditions, new Date().toISOString());
26109
+ }
25765
26110
  async listTombstones() {
25766
26111
  await this.ensureSchema();
25767
26112
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
@@ -26237,7 +26582,7 @@ async function startTask2(id, agentId, store) {
26237
26582
  if (task2.status !== "pending" && task2.status !== "in_progress") {
26238
26583
  throw new Error(`Task is ${task2.status} and cannot be started by ${agentId}`);
26239
26584
  }
26240
- return patchTask(task2, {
26585
+ const started = await patchTask(task2, {
26241
26586
  status: "in_progress",
26242
26587
  assigned_to: task2.assigned_to ?? agentId,
26243
26588
  agent_id: task2.agent_id ?? agentId,
@@ -26245,6 +26590,8 @@ async function startTask2(id, agentId, store) {
26245
26590
  locked_at: new Date().toISOString(),
26246
26591
  started_at: task2.started_at ?? new Date().toISOString()
26247
26592
  }, store);
26593
+ await logTaskChange2(task2.id, "start", "status", task2.status, "in_progress", agentId, store);
26594
+ return started;
26248
26595
  }
26249
26596
  async function completeTask2(id, agentId, options, store) {
26250
26597
  const task2 = await store.completeTask(id, agentId, options);
@@ -26550,15 +26897,27 @@ async function updatePlan2(id, input, store) {
26550
26897
  }
26551
26898
  return store.upsert("plans", { ...plan, ...patch, updated_at: new Date().toISOString() });
26552
26899
  }
26900
+ function matchAgentByName(agents, name, options) {
26901
+ const target = normalizeAgentNameInput(name);
26902
+ if (!target)
26903
+ return null;
26904
+ const matches = agents.filter((agent) => normalizeAgentNameInput(agent.name) === target && (options?.includeArchived !== false || agent.status !== "archived"));
26905
+ if (matches.length === 0)
26906
+ return null;
26907
+ return matches.reduce((freshest, candidate) => new Date(candidate.last_seen_at).getTime() > new Date(freshest.last_seen_at).getTime() ? candidate : freshest);
26908
+ }
26553
26909
  async function registerAgent2(input, store, context) {
26554
- const existing = (await store.list("agents")).find((agent2) => agent2.name === input.name && agent2.status !== "archived");
26910
+ const canonicalName = normalizeAgentNameInput(input.name);
26911
+ const existing = matchAgentByName(await store.list("agents"), canonicalName, {
26912
+ includeArchived: false
26913
+ });
26555
26914
  if (existing && !input.force && existing.session_id && existing.session_id !== input.session_id) {
26556
- return { conflict: true, message: `Agent name '${input.name}' is already active` };
26915
+ return { conflict: true, message: `Agent name '${canonicalName}' is already active` };
26557
26916
  }
26558
26917
  const timestamp3 = new Date().toISOString();
26559
26918
  const agent = {
26560
26919
  id: existing?.id ?? randomUUID3().slice(0, 8),
26561
- name: input.name,
26920
+ name: canonicalName,
26562
26921
  description: input.description ?? existing?.description ?? null,
26563
26922
  role: input.role ?? existing?.role ?? null,
26564
26923
  title: input.title ?? existing?.title ?? null,
@@ -26596,7 +26955,7 @@ async function resolveAgent(idOrName, store) {
26596
26955
  const byId = await store.get("agents", idOrName);
26597
26956
  if (byId)
26598
26957
  return byId;
26599
- return (await store.list("agents")).find((agent) => agent.name === idOrName) ?? null;
26958
+ return matchAgentByName(await store.list("agents"), idOrName);
26600
26959
  }
26601
26960
  async function heartbeatAgent(idOrName, store, context) {
26602
26961
  const agent = await resolveAgent(idOrName, store);
@@ -27353,17 +27712,17 @@ function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
27353
27712
  init_config();
27354
27713
  function createTodosStorageAdapter(options = {}) {
27355
27714
  const config = options.config ?? loadTodosStorageConfig(options.env);
27356
- if (config.mode === "local") {
27715
+ if (config.mode === "sqlite") {
27357
27716
  if (isTodosShadowEnabled(options.env))
27358
27717
  return createShadowAdapter(options, config);
27359
27718
  return createLocalSqliteTodosStorageAdapter(options.local);
27360
27719
  }
27361
27720
  assertTodosRemoteStorageConfig(config);
27362
- const adapter = config.mode === "hybrid" ? options.hybridAdapter ?? createImplicitHybridAdapter(options) ?? options.remoteAdapter : options.remoteAdapter ?? createImplicitPostgresAdapter(options);
27721
+ const adapter = options.remoteAdapter ?? createImplicitPostgresAdapter(options);
27363
27722
  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.");
27723
+ throw new Error("postgres storage requires a repo-native Postgres adapter. " + "Pass remoteAdapter or postgresClient after wiring Postgres RDS support.");
27365
27724
  }
27366
- assertRemoteAdapterCapabilities(adapter, config.mode);
27725
+ assertPostgresAdapterCapabilities(adapter);
27367
27726
  return adapter;
27368
27727
  }
27369
27728
  function createShadowAdapter(options, config) {
@@ -27377,7 +27736,7 @@ function createShadowAdapter(options, config) {
27377
27736
  ...options.local ? { local: options.local } : {},
27378
27737
  ...options.postgresSyncStore ? { syncStore: options.postgresSyncStore } : {},
27379
27738
  ...postgresClient ? { postgresClient } : {},
27380
- ...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
27739
+ ...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
27381
27740
  });
27382
27741
  }
27383
27742
  function createImplicitPostgresAdapter(options) {
@@ -27385,25 +27744,12 @@ function createImplicitPostgresAdapter(options) {
27385
27744
  return null;
27386
27745
  return createPostgresTodosStorageAdapter({
27387
27746
  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
27747
+ ...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
27399
27748
  });
27400
27749
  }
27401
- function assertRemoteAdapterCapabilities(adapter, mode) {
27750
+ function assertPostgresAdapterCapabilities(adapter) {
27402
27751
  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");
27752
+ throw new Error("postgres storage adapter must set capabilities.remotePersistence=true");
27407
27753
  }
27408
27754
  }
27409
27755
  // src/storage/shadow-outbox.ts
@@ -27730,6 +28076,50 @@ async function closeRuntimeShadowCloud() {
27730
28076
  await cloud.close().catch(() => {});
27731
28077
  }
27732
28078
  }
28079
+ // src/storage/hybrid.ts
28080
+ function createHybridTodosStorageAdapter(options) {
28081
+ const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
28082
+ const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
28083
+ if (!syncStore)
28084
+ throw new Error("hybrid storage requires a Postgres sync store or query client");
28085
+ if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
28086
+ throw new Error("hybrid storage requires local snapshot export/import support");
28087
+ }
28088
+ const exportSnapshot2 = async (context) => await local.sync.exportSnapshot(context);
28089
+ const importSnapshot2 = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
28090
+ const remote = {
28091
+ ensureSchema: () => syncStore.ensureSchema(),
28092
+ async pushSnapshot(context) {
28093
+ const snapshot = await exportSnapshot2(context);
28094
+ return syncStore.pushSnapshot(snapshot, context);
28095
+ },
28096
+ async pullSnapshot(options2, context) {
28097
+ const snapshot = await syncStore.pullSnapshot(options2);
28098
+ return importSnapshot2(snapshot, context);
28099
+ },
28100
+ async syncOnce(options2, context) {
28101
+ const pulled = await remote.pullSnapshot(options2, context);
28102
+ const pushed = await remote.pushSnapshot(context);
28103
+ return { pulled, pushed };
28104
+ }
28105
+ };
28106
+ return {
28107
+ ...local,
28108
+ kind: "hybrid",
28109
+ capabilities: {
28110
+ ...local.capabilities,
28111
+ localPersistence: true,
28112
+ remotePersistence: true,
28113
+ sync: true
28114
+ },
28115
+ sync: {
28116
+ ...local.sync,
28117
+ exportSnapshot: exportSnapshot2,
28118
+ importSnapshot: importSnapshot2
28119
+ },
28120
+ remote
28121
+ };
28122
+ }
27733
28123
  // src/storage/comment-redaction-backfill.ts
27734
28124
  init_redaction();
27735
28125
  var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
@@ -31308,10 +31698,10 @@ function getNativeStorageStatus(env = process.env) {
31308
31698
  } catch (error) {
31309
31699
  issues.push(error instanceof Error ? error.message : String(error));
31310
31700
  }
31311
- const remoteEnabled = isTodosRemoteStorageEnabled(config);
31701
+ const remoteEnabled = isTodosPostgresBackend(config);
31312
31702
  const remoteFieldsConfigured = Boolean(config.database || config.objectStorage);
31313
31703
  if (!remoteEnabled && remoteFieldsConfigured) {
31314
- warnings.push(`${TODOS_STORAGE_ENV.mode}=local ignores configured remote storage fields`);
31704
+ warnings.push(`the sqlite backend (${TODOS_STORAGE_ENV.mode}) ignores configured remote storage fields`);
31315
31705
  }
31316
31706
  if (remoteEnabled && !config.objectStorage) {
31317
31707
  warnings.push(`${TODOS_STORAGE_ENV.s3Bucket} is not configured, so artifact sync will stay local`);
@@ -31320,7 +31710,7 @@ function getNativeStorageStatus(env = process.env) {
31320
31710
  ok: issues.length === 0,
31321
31711
  service: "todos",
31322
31712
  mode: config.mode,
31323
- local_default: config.mode === "local",
31713
+ local_default: config.mode === "sqlite",
31324
31714
  remote_enabled: remoteEnabled,
31325
31715
  database: {
31326
31716
  configured: Boolean(config.database),
@@ -31402,8 +31792,12 @@ function storageEnvStatus(env) {
31402
31792
  ]));
31403
31793
  }
31404
31794
  function fallbackConfig(env) {
31405
- const modeValue = clean2(env[TODOS_STORAGE_ENV.mode]);
31406
- const mode = modeValue === "remote" || modeValue === "hybrid" ? modeValue : "local";
31795
+ let mode;
31796
+ try {
31797
+ mode = parseStorageBackend(clean2(env[TODOS_STORAGE_ENV.mode]));
31798
+ } catch {
31799
+ mode = "sqlite";
31800
+ }
31407
31801
  return {
31408
31802
  service: "todos",
31409
31803
  mode,
@@ -50582,7 +50976,7 @@ function parseObject4(value) {
50582
50976
  function normalizeKey(value) {
50583
50977
  return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
50584
50978
  }
50585
- function normalizeFingerprint(value) {
50979
+ function normalizeTaskFindingFingerprint(value) {
50586
50980
  const normalized = normalizeKey(value);
50587
50981
  if (!normalized)
50588
50982
  throw new Error("finding fingerprint is required");
@@ -50691,7 +51085,7 @@ function assertTask(taskId, db) {
50691
51085
  throw new TaskNotFoundError(taskId);
50692
51086
  }
50693
51087
  function nextFinding(input, db) {
50694
- const fingerprint4 = normalizeFingerprint(input.fingerprint);
51088
+ const fingerprint4 = normalizeTaskFindingFingerprint(input.fingerprint);
50695
51089
  const title = redactOptional(input.title, 300);
50696
51090
  if (!title)
50697
51091
  throw new Error("finding title is required");
@@ -50839,7 +51233,7 @@ function resolveMissingTaskFindings(input, db) {
50839
51233
  const timestamp3 = input.resolved_at || now();
50840
51234
  const status = normalizeResolutionStatus(input.status);
50841
51235
  const runId = resolveRunForTask(input.run_id, input.task_id, d);
50842
- const present = new Set(input.fingerprints.map(normalizeFingerprint));
51236
+ const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
50843
51237
  const warnings = [];
50844
51238
  const conditions = ["task_id = ?", "status = 'open'"];
50845
51239
  const params = [input.task_id];
@@ -51366,12 +51760,12 @@ init_artifact_store();
51366
51760
 
51367
51761
  // src/lib/doctor.ts
51368
51762
  init_database();
51763
+ import { chmodSync, copyFileSync as copyFileSync2, existsSync as existsSync30, mkdirSync as mkdirSync22, statSync as statSync12 } from "fs";
51764
+ import { basename as basename7, dirname as dirname18, join as join27 } from "path";
51369
51765
  init_migrations();
51370
51766
  init_schema();
51371
51767
  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 = [
51768
+ var REQUIRED_TABLES3 = [
51375
51769
  "_migrations",
51376
51770
  "projects",
51377
51771
  "tasks",
@@ -51384,7 +51778,7 @@ var REQUIRED_TABLES2 = [
51384
51778
  "task_run_commands",
51385
51779
  "task_run_artifacts"
51386
51780
  ];
51387
- function tableExists(db, table) {
51781
+ function tableExists2(db, table) {
51388
51782
  return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
51389
51783
  }
51390
51784
  function quoteIdent(identifier) {
@@ -51468,7 +51862,7 @@ function findDuplicateIndexes(db) {
51468
51862
  return duplicates;
51469
51863
  }
51470
51864
  function findMissingProjectRoots(db) {
51471
- if (!tableExists(db, "projects"))
51865
+ if (!tableExists2(db, "projects"))
51472
51866
  return 0;
51473
51867
  let missing = 0;
51474
51868
  const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
@@ -51483,7 +51877,7 @@ function findMissingProjectRoots(db) {
51483
51877
  return missing;
51484
51878
  }
51485
51879
  function addTaskStateChecks(db, checks) {
51486
- if (!tableExists(db, "tasks"))
51880
+ if (!tableExists2(db, "tasks"))
51487
51881
  return;
51488
51882
  const columns = getTableColumns(db, "tasks");
51489
51883
  const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
@@ -51555,15 +51949,42 @@ function createBackup(dbPath) {
51555
51949
  function pushRepair(repairs, type, message, applied, count3) {
51556
51950
  repairs.push({ type, message, applied, count: count3 });
51557
51951
  }
51558
- function summarize3(checks, repairs) {
51952
+ function summarize3(checks, repairs, integrity) {
51559
51953
  return {
51560
51954
  errors: checks.filter((check) => check.severity === "error").length,
51561
51955
  warnings: checks.filter((check) => check.severity === "warn").length,
51562
51956
  infos: checks.filter((check) => check.severity === "info").length,
51563
51957
  repairable: checks.filter((check) => check.repairable).length,
51564
- applied: repairs.filter((repair) => repair.applied).length
51958
+ applied: repairs.filter((repair) => repair.applied).length,
51959
+ integrity_findings: integrity.summary.findings,
51960
+ integrity_rows: integrity.summary.rows,
51961
+ integrity_unverified: integrity.summary.unverified
51565
51962
  };
51566
51963
  }
51964
+ function addIntegrityChecks(integrity, checks) {
51965
+ for (const condition of integrity.conditions) {
51966
+ if (!condition.verified) {
51967
+ addCheck(checks, {
51968
+ severity: "warn",
51969
+ type: `${condition.id}_unverified`,
51970
+ message: condition.message,
51971
+ repairable: false,
51972
+ integrity: true
51973
+ });
51974
+ continue;
51975
+ }
51976
+ if ((condition.count ?? 0) === 0)
51977
+ continue;
51978
+ addCheck(checks, {
51979
+ severity: condition.severity ?? "warn",
51980
+ type: condition.id,
51981
+ message: `${condition.message} \u2014 ${condition.impact}`,
51982
+ count: condition.count ?? undefined,
51983
+ repairable: false,
51984
+ integrity: true
51985
+ });
51986
+ }
51987
+ }
51567
51988
  function deleteOrphans(db, table, where) {
51568
51989
  const before = countQuery2(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
51569
51990
  if (before > 0)
@@ -51592,7 +52013,7 @@ function runTodosDoctor(options = {}) {
51592
52013
  message: `Schema at migration ${migrationCurrent}`
51593
52014
  });
51594
52015
  }
51595
- const missingTables = REQUIRED_TABLES2.filter((table) => !tableExists(db, table));
52016
+ const missingTables = REQUIRED_TABLES3.filter((table) => !tableExists2(db, table));
51596
52017
  if (missingTables.length > 0) {
51597
52018
  addCheck(checks, {
51598
52019
  severity: "error",
@@ -51602,7 +52023,7 @@ function runTodosDoctor(options = {}) {
51602
52023
  repairable: true
51603
52024
  });
51604
52025
  }
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;
52026
+ 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
52027
  if (orphanedParents > 0) {
51607
52028
  addCheck(checks, {
51608
52029
  severity: "error",
@@ -51612,7 +52033,7 @@ function runTodosDoctor(options = {}) {
51612
52033
  repairable: true
51613
52034
  });
51614
52035
  }
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;
52036
+ 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
52037
  if (orphanedDependencies > 0) {
51617
52038
  addCheck(checks, {
51618
52039
  severity: "error",
@@ -51631,7 +52052,7 @@ function runTodosDoctor(options = {}) {
51631
52052
  ];
51632
52053
  let orphanedRows2 = 0;
51633
52054
  for (const [table, where] of orphanTables) {
51634
- if (!tableExists(db, table))
52055
+ if (!tableExists2(db, table))
51635
52056
  continue;
51636
52057
  orphanedRows2 += countQuery2(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
51637
52058
  }
@@ -51645,6 +52066,8 @@ function runTodosDoctor(options = {}) {
51645
52066
  });
51646
52067
  }
51647
52068
  addTaskStateChecks(db, checks);
52069
+ const integrity = scanSqliteIntegrity(db);
52070
+ addIntegrityChecks(integrity, checks);
51648
52071
  const corruptJson = findCorruptJsonMetadata(db);
51649
52072
  if (corruptJson.length > 0) {
51650
52073
  addCheck(checks, {
@@ -51703,12 +52126,12 @@ function runTodosDoctor(options = {}) {
51703
52126
  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
52127
  pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
51705
52128
  }
51706
- if (orphanedDependencies > 0 && tableExists(db, "task_dependencies")) {
52129
+ if (orphanedDependencies > 0 && tableExists2(db, "task_dependencies")) {
51707
52130
  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
52131
  pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count3);
51709
52132
  }
51710
52133
  for (const [table, where] of orphanTables) {
51711
- if (!tableExists(db, table))
52134
+ if (!tableExists2(db, table))
51712
52135
  continue;
51713
52136
  const count3 = deleteOrphans(db, table, where);
51714
52137
  if (count3 > 0)
@@ -51737,16 +52160,19 @@ function runTodosDoctor(options = {}) {
51737
52160
  }
51738
52161
  }
51739
52162
  }
51740
- const finalChecks = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }).checks : checks;
51741
- const summary = summarize3(finalChecks, repairs);
52163
+ const reread = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }) : undefined;
52164
+ const finalChecks = reread?.checks ?? checks;
52165
+ const finalIntegrity = reread?.integrity ?? integrity;
52166
+ const summary = summarize3(finalChecks, repairs, finalIntegrity);
51742
52167
  return {
51743
- ok: !finalChecks.some((check) => check.severity === "error"),
52168
+ ok: !finalChecks.some((check) => check.severity === "error") && finalIntegrity.summary.ok,
51744
52169
  dry_run: !apply,
51745
52170
  database_path: dbPath,
51746
52171
  migration: { current: getMigrationLevel(db), expected: migrationExpected },
51747
52172
  backup,
51748
52173
  checks: finalChecks,
51749
52174
  repairs,
52175
+ integrity: finalIntegrity,
51750
52176
  summary
51751
52177
  };
51752
52178
  }
@@ -52292,6 +52718,7 @@ export {
52292
52718
  updateArtifactRedaction,
52293
52719
  updateAgentActivity,
52294
52720
  updateAgent,
52721
+ unverifiedCondition,
52295
52722
  unsubscribeResource,
52296
52723
  unlockTask,
52297
52724
  unlinkTaskCommit,
@@ -52312,6 +52739,7 @@ export {
52312
52739
  summarizeTaskRun,
52313
52740
  summarizeRoadmap,
52314
52741
  summarizeMilestone,
52742
+ summarizeIntegrity,
52315
52743
  suggestAgentNames,
52316
52744
  subscribeResource,
52317
52745
  storeArtifactContent,
@@ -52352,6 +52780,7 @@ export {
52352
52780
  scorePlanHealth,
52353
52781
  scheduleTask,
52354
52782
  scanTextForSecrets,
52783
+ scanSqliteIntegrity,
52355
52784
  scanSourceForForbiddenWebPatterns,
52356
52785
  scanReminders,
52357
52786
  scanPreWriteText,
@@ -52399,6 +52828,7 @@ export {
52399
52828
  resolvePartialId,
52400
52829
  resolveMissingTaskFindings,
52401
52830
  resolveMentions,
52831
+ resolveIntegritySeverity,
52402
52832
  resolveGitRoot,
52403
52833
  resolveCommandQuery,
52404
52834
  resolveAccessProfile,
@@ -52513,6 +52943,7 @@ export {
52513
52943
  patrolTasks,
52514
52944
  parseTmuxTarget,
52515
52945
  parseStorageMode,
52946
+ parseStorageBackend,
52516
52947
  parseRecurrenceRule,
52517
52948
  parseQuietHours,
52518
52949
  parsePlanArtifactMarkdown,
@@ -52541,6 +52972,8 @@ export {
52541
52972
  migrationDryRun,
52542
52973
  migrateWorkflowStates,
52543
52974
  mergeDuplicateTask,
52975
+ measuredCondition,
52976
+ measureIntegrityRows,
52544
52977
  materializePlanSteps,
52545
52978
  matchCapabilities,
52546
52979
  looksSensitiveKey,
@@ -52659,12 +53092,14 @@ export {
52659
53092
  isWorktreePath,
52660
53093
  isValidRecurrenceRule,
52661
53094
  isTodosRemoteStorageEnabled,
53095
+ isTodosPostgresBackend,
52662
53096
  isSnapshotStale,
52663
53097
  isEncryptedValue,
52664
53098
  isEncryptedBridgeBundle,
52665
53099
  isArtifactExpired,
52666
53100
  isAllowedLocalApiUrl,
52667
53101
  isAgentConflict,
53102
+ integrityCondition,
52668
53103
  installTemplateLibrary,
52669
53104
  installLocalExtension,
52670
53105
  inspectPlanArtifact,
@@ -52707,6 +53142,7 @@ export {
52707
53142
  getTodosStorageEnvName,
52708
53143
  getTodosStorageDatabaseUrl,
52709
53144
  getTodosStorageDatabaseEnv,
53145
+ getTodosStorageBackend,
52710
53146
  getTimeReport,
52711
53147
  getTimeLogs,
52712
53148
  getTerminalNotificationRule,
@@ -52914,6 +53350,7 @@ export {
52914
53350
  formatLockConflict,
52915
53351
  formatKnowledgeSnapshotMarkdown,
52916
53352
  formatIssueImportPreviewText,
53353
+ formatIntegrityMessage,
52917
53354
  formatIntakePreviewText,
52918
53355
  formatHandoffPacket,
52919
53356
  formatGoalHandoff,
@@ -53144,16 +53581,19 @@ export {
53144
53581
  bulkAddTaskFiles,
53145
53582
  buildTaskDependencyEdges,
53146
53583
  buildTaskBoardSnapshot,
53584
+ buildSqliteIntegritySql,
53147
53585
  buildS3ObjectUrl,
53148
53586
  buildS3ObjectKey,
53149
53587
  buildRunReplayBundle,
53150
53588
  buildResourceSnapshot,
53151
53589
  buildReportExportData,
53152
53590
  buildProjectDependencyGraph,
53591
+ buildPostgresIntegritySql,
53153
53592
  buildPlanArtifactSnapshot,
53154
53593
  buildMcpToolGroups,
53155
53594
  buildMachineTopologyReport,
53156
53595
  buildKnowledgeSnapshotPayload,
53596
+ buildIntegrityReport,
53157
53597
  buildHandoffPacket,
53158
53598
  buildFeatureManifest,
53159
53599
  buildFailureTriageReport,
@@ -53188,6 +53628,7 @@ export {
53188
53628
  appendRunCommand,
53189
53629
  analyzeDependencyGraph,
53190
53630
  agentClaimNextSafe,
53631
+ adoptRemoteIntegrityReport,
53191
53632
  addTemplateTasks,
53192
53633
  addTaskRunFile,
53193
53634
  addTaskRunEvent,
@@ -53234,6 +53675,7 @@ export {
53234
53675
  TODOS_LOCAL_BACKUP_KIND,
53235
53676
  TODOS_JSON_CONTRACTS_MANIFEST,
53236
53677
  TODOS_JSON_CONTRACTS,
53678
+ TODOS_INTEGRITY_SCHEMA_VERSION,
53237
53679
  TODOS_ERROR_CODES,
53238
53680
  TODOS_ENCRYPTION_SCHEMA_VERSION,
53239
53681
  TODOS_ENCRYPTED_VALUE_KIND,
@@ -53290,6 +53732,7 @@ export {
53290
53732
  PLAN_EXECUTION_SCHEMA,
53291
53733
  PLAN_EXECUTION_MODES,
53292
53734
  PARITY_SCHEMA_VERSION,
53735
+ OPEN_TASK_STATUSES,
53293
53736
  NOTIFICATION_REMINDERS_SCHEMA,
53294
53737
  NL_INTAKE_SCHEMA,
53295
53738
  NESTED_SUBCOMMANDS,
@@ -53318,6 +53761,7 @@ export {
53318
53761
  JSON_OUTPUT_CONTRACT,
53319
53762
  ISSUE_SOURCES,
53320
53763
  ISSUE_IMPORT_SCHEMA,
53764
+ INTEGRITY_CONDITIONS,
53321
53765
  INTAKE_TRIAGE_STATUSES,
53322
53766
  INTAKE_SOURCE_TYPES,
53323
53767
  INBOX_INTAKE_SCHEMA,