@hasna/todos 0.13.1 → 0.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +80 -15
  2. package/dist/cli/cloud-router.d.ts +78 -12
  3. package/dist/cli/cloud-router.d.ts.map +1 -1
  4. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  7. package/dist/cli/doctor-integrity.d.ts +106 -0
  8. package/dist/cli/doctor-integrity.d.ts.map +1 -0
  9. package/dist/cli/helpers.d.ts +2 -2
  10. package/dist/cli/index.js +1748 -1012
  11. package/dist/cli/stage-a.d.ts.map +1 -1
  12. package/dist/contracts.js +137 -39
  13. package/dist/db/database.d.ts.map +1 -1
  14. package/dist/db/findings.d.ts +1 -0
  15. package/dist/db/findings.d.ts.map +1 -1
  16. package/dist/db/integrity.d.ts +20 -0
  17. package/dist/db/integrity.d.ts.map +1 -0
  18. package/dist/db/task-crud.d.ts.map +1 -1
  19. package/dist/index.d.ts +5 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +592 -147
  22. package/dist/lib/context-packs.d.ts +1 -0
  23. package/dist/lib/context-packs.d.ts.map +1 -1
  24. package/dist/lib/dependency-graph.d.ts +19 -11
  25. package/dist/lib/dependency-graph.d.ts.map +1 -1
  26. package/dist/lib/doctor.d.ts +20 -0
  27. package/dist/lib/doctor.d.ts.map +1 -1
  28. package/dist/lib/integrity.d.ts +215 -0
  29. package/dist/lib/integrity.d.ts.map +1 -0
  30. package/dist/lib/native-storage-status.d.ts +2 -2
  31. package/dist/lib/native-storage-status.d.ts.map +1 -1
  32. package/dist/lib/redaction.d.ts.map +1 -1
  33. package/dist/lib/release-compatibility.d.ts.map +1 -1
  34. package/dist/lib/task-dedupe.d.ts +15 -0
  35. package/dist/lib/task-dedupe.d.ts.map +1 -1
  36. package/dist/mcp/index.d.ts.map +1 -1
  37. package/dist/mcp/index.js +544 -97
  38. package/dist/mcp.js +8 -4
  39. package/dist/registry.d.ts +1 -1
  40. package/dist/registry.d.ts.map +1 -1
  41. package/dist/registry.js +144 -39
  42. package/dist/release-provenance.json +5 -5
  43. package/dist/sdk/types.d.ts +42 -0
  44. package/dist/sdk/types.d.ts.map +1 -1
  45. package/dist/sdk/v1.generated.d.ts +1 -0
  46. package/dist/sdk/v1.generated.d.ts.map +1 -1
  47. package/dist/server/cloud.d.ts +8 -1
  48. package/dist/server/cloud.d.ts.map +1 -1
  49. package/dist/server/index.js +585 -118
  50. package/dist/server/openapi.d.ts +12 -0
  51. package/dist/server/openapi.d.ts.map +1 -1
  52. package/dist/server/port.d.ts +52 -0
  53. package/dist/server/port.d.ts.map +1 -0
  54. package/dist/server/serve.d.ts.map +1 -1
  55. package/dist/server/v1.d.ts.map +1 -1
  56. package/dist/storage/config.d.ts +23 -5
  57. package/dist/storage/config.d.ts.map +1 -1
  58. package/dist/storage/factory.d.ts +9 -3
  59. package/dist/storage/factory.d.ts.map +1 -1
  60. package/dist/storage/hybrid.d.ts +7 -0
  61. package/dist/storage/hybrid.d.ts.map +1 -1
  62. package/dist/storage/index.d.ts +3 -3
  63. package/dist/storage/index.d.ts.map +1 -1
  64. package/dist/storage/interfaces.d.ts +43 -8
  65. package/dist/storage/interfaces.d.ts.map +1 -1
  66. package/dist/storage/local-sqlite.d.ts.map +1 -1
  67. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  68. package/dist/storage.d.ts +3 -3
  69. package/dist/storage.d.ts.map +1 -1
  70. package/dist/storage.js +619 -256
  71. package/dist/testing.d.ts +79 -0
  72. package/dist/testing.d.ts.map +1 -0
  73. package/dist/testing.js +107 -0
  74. package/dist/types/index.d.ts +27 -1
  75. package/dist/types/index.d.ts.map +1 -1
  76. package/package.json +8 -4
package/dist/index.js CHANGED
@@ -3424,7 +3424,10 @@ var init_machines = __esm(() => {
3424
3424
  });
3425
3425
 
3426
3426
  // src/types/index.ts
3427
- var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3427
+ function isBlockingDependencyStatus(status) {
3428
+ return status !== "completed" && status !== "cancelled";
3429
+ }
3430
+ var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3428
3431
  var init_types = __esm(() => {
3429
3432
  TASK_STATUSES = [
3430
3433
  "pending",
@@ -3464,6 +3467,22 @@ var init_types = __esm(() => {
3464
3467
  this.name = "TaskNotFoundError";
3465
3468
  }
3466
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
+ };
3467
3486
  ProjectNotFoundError = class ProjectNotFoundError extends Error {
3468
3487
  projectId;
3469
3488
  static code = "PROJECT_NOT_FOUND";
@@ -3755,15 +3774,18 @@ var init_identity_mapping = __esm(() => {
3755
3774
  var exports_config = {};
3756
3775
  __export(exports_config, {
3757
3776
  parseStorageMode: () => parseStorageMode,
3777
+ parseStorageBackend: () => parseStorageBackend,
3758
3778
  loadTodosStorageConfig: () => loadTodosStorageConfig,
3759
3779
  loadStorageConfig: () => loadStorageConfig,
3760
3780
  isTodosShadowEnabled: () => isTodosShadowEnabled,
3761
3781
  isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
3782
+ isTodosPostgresBackend: () => isTodosPostgresBackend,
3762
3783
  getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
3763
3784
  getTodosStorageMode: () => getTodosStorageMode,
3764
3785
  getTodosStorageEnvName: () => getTodosStorageEnvName,
3765
3786
  getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
3766
3787
  getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
3788
+ getTodosStorageBackend: () => getTodosStorageBackend,
3767
3789
  getStorageMode: () => getStorageMode,
3768
3790
  getStorageDatabaseUrl: () => getStorageDatabaseUrl,
3769
3791
  getStorageDatabaseEnv: () => getStorageDatabaseEnv,
@@ -3788,7 +3810,7 @@ function getCanonicalTodosRdsConfig(env = process.env) {
3788
3810
  };
3789
3811
  }
3790
3812
  function loadTodosStorageConfig(env = process.env) {
3791
- const mode = getTodosStorageMode(env);
3813
+ const mode = getTodosStorageBackend(env);
3792
3814
  const databaseUrl = getTodosStorageDatabaseUrl(env);
3793
3815
  const bucket = readStorageEnv(env, "s3Bucket").value;
3794
3816
  const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
@@ -3825,29 +3847,41 @@ function loadTodosStorageConfig(env = process.env) {
3825
3847
  function loadStorageConfig(env = process.env) {
3826
3848
  return loadTodosStorageConfig(env);
3827
3849
  }
3850
+ function isTodosPostgresBackend(config) {
3851
+ return config.mode === "postgres";
3852
+ }
3828
3853
  function isTodosRemoteStorageEnabled(config) {
3829
- return config.mode === "remote" || config.mode === "hybrid";
3854
+ return isTodosPostgresBackend(config);
3830
3855
  }
3831
3856
  function assertTodosRemoteStorageConfig(config) {
3832
- if (!isTodosRemoteStorageEnabled(config))
3857
+ if (!isTodosPostgresBackend(config))
3833
3858
  return;
3834
3859
  if (!config.database?.url) {
3835
3860
  throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
3836
3861
  }
3837
3862
  }
3838
- function parseStorageMode(value) {
3863
+ function parseStorageBackend(value) {
3839
3864
  const normalized = clean(value)?.toLowerCase();
3840
3865
  if (!normalized)
3841
- return "local";
3842
- if (normalized === "local" || normalized === "remote" || normalized === "hybrid")
3866
+ return "sqlite";
3867
+ if (normalized === "sqlite" || normalized === "postgres")
3843
3868
  return normalized;
3844
- 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);
3845
3879
  }
3846
3880
  function getTodosStorageMode(env = process.env) {
3847
- return parseStorageMode(readStorageEnv(env, "mode").value);
3881
+ return getTodosStorageBackend(env);
3848
3882
  }
3849
3883
  function getStorageMode(env = process.env) {
3850
- return getTodosStorageMode(env);
3884
+ return getTodosStorageBackend(env);
3851
3885
  }
3852
3886
  function isTodosShadowEnabled(env = process.env) {
3853
3887
  return parseBoolean(readStorageEnv(env, "shadow").value, false);
@@ -3858,8 +3892,8 @@ function getTodosStorageShadowEnvName(env = process.env) {
3858
3892
  function assertTodosShadowConfig(config, env = process.env) {
3859
3893
  if (!isTodosShadowEnabled(env))
3860
3894
  return;
3861
- if (config.mode !== "local") {
3862
- 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})`);
3863
3897
  }
3864
3898
  if (!config.database?.url) {
3865
3899
  throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
@@ -3915,7 +3949,7 @@ function parsePositiveInteger(value, fallback) {
3915
3949
  }
3916
3950
  return parsed;
3917
3951
  }
3918
- 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;
3919
3953
  var init_config = __esm(() => {
3920
3954
  TODOS_STORAGE_TABLES = [
3921
3955
  "todos_sync_records",
@@ -3956,6 +3990,14 @@ var init_config = __esm(() => {
3956
3990
  syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
3957
3991
  syncDryRun: "TODOS_SYNC_DRY_RUN"
3958
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
+ };
3959
4001
  });
3960
4002
 
3961
4003
  // src/storage/shadow-outbox-schema.ts
@@ -4089,6 +4131,30 @@ function findGitRoot(startDir) {
4089
4131
  }
4090
4132
  return null;
4091
4133
  }
4134
+ function getGlobalDbPath() {
4135
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4136
+ return join(home, ".hasna", "todos", "todos.db");
4137
+ }
4138
+ function hasExplicitProjectArg(args = process.argv.slice(2)) {
4139
+ return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
4140
+ }
4141
+ function getCliCommand(args = process.argv.slice(2)) {
4142
+ const globalOptionsWithValues = new Set(["--project", "--agent", "--session"]);
4143
+ for (let index = 0;index < args.length; index += 1) {
4144
+ const arg = args[index];
4145
+ if (globalOptionsWithValues.has(arg)) {
4146
+ index += 1;
4147
+ continue;
4148
+ }
4149
+ if (arg.startsWith("-"))
4150
+ continue;
4151
+ return arg;
4152
+ }
4153
+ return null;
4154
+ }
4155
+ function canCreateScopedProjectDb(args = process.argv.slice(2)) {
4156
+ return getCliCommand(args) === "project-bootstrap" && !args.some((arg) => arg === "--dry-run" || arg.startsWith("--dry-run="));
4157
+ }
4092
4158
  function getDbPath() {
4093
4159
  if (process.env["HASNA_TODOS_DB_PATH"]) {
4094
4160
  return process.env["HASNA_TODOS_DB_PATH"];
@@ -4096,18 +4162,19 @@ function getDbPath() {
4096
4162
  if (process.env["TODOS_DB_PATH"]) {
4097
4163
  return process.env["TODOS_DB_PATH"];
4098
4164
  }
4165
+ if (hasExplicitProjectArg())
4166
+ return getGlobalDbPath();
4099
4167
  const cwd = process.cwd();
4100
4168
  const nearest = findNearestProjectDb(cwd);
4101
4169
  if (nearest)
4102
4170
  return nearest;
4103
4171
  if (process.env["TODOS_DB_SCOPE"] === "project") {
4104
4172
  const gitRoot = findGitRoot(cwd);
4105
- if (gitRoot) {
4173
+ if (gitRoot && canCreateScopedProjectDb()) {
4106
4174
  return join(gitRoot, ".hasna", "todos", "todos.db");
4107
4175
  }
4108
4176
  }
4109
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4110
- return join(home, ".hasna", "todos", "todos.db");
4177
+ return getGlobalDbPath();
4111
4178
  }
4112
4179
  function getDatabasePath() {
4113
4180
  return getDbPath();
@@ -4123,8 +4190,8 @@ function ensureDir(filePath) {
4123
4190
  function openDatabase(path) {
4124
4191
  ensureDir(path);
4125
4192
  const db = new Database(path);
4126
- db.run("PRAGMA journal_mode = WAL");
4127
4193
  db.run("PRAGMA busy_timeout = 5000");
4194
+ db.run("PRAGMA journal_mode = WAL");
4128
4195
  db.run("PRAGMA foreign_keys = ON");
4129
4196
  runMigrations(db);
4130
4197
  ensureAgentIdentitySchema(db);
@@ -4210,10 +4277,13 @@ function resolvePartialId(db, table, partialId) {
4210
4277
  return null;
4211
4278
  }
4212
4279
  if (table === "tasks") {
4213
- const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
4280
+ const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = LOWER(?) ORDER BY project_id, id").all(partialId);
4214
4281
  if (shortIdRows.length === 1) {
4215
4282
  return shortIdRows[0].id;
4216
4283
  }
4284
+ if (shortIdRows.length > 1) {
4285
+ throw new TaskReferenceAmbiguousError(partialId, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
4286
+ }
4217
4287
  }
4218
4288
  if (table === "task_lists") {
4219
4289
  const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
@@ -4955,11 +5025,15 @@ function secretPatterns() {
4955
5025
  return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
4956
5026
  }
4957
5027
  function isRedactionPlaceholderMatch(match) {
4958
- const placeholder = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
4959
5028
  const trimmed = match.trim();
4960
- return new RegExp(`^${placeholder}$`).test(trimmed) || new RegExp(`=\\s*['"]?${placeholder}['"]?$`).test(trimmed);
5029
+ return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(trimmed) || new RegExp(`=\\s*['"]?${REDACTION_PLACEHOLDER}['"]?$`).test(trimmed);
5030
+ }
5031
+ function isRedactionPlaceholderKey(key) {
5032
+ return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(key.trim());
4961
5033
  }
4962
5034
  function isSecretKey(key) {
5035
+ if (isRedactionPlaceholderKey(key))
5036
+ return false;
4963
5037
  if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
4964
5038
  return false;
4965
5039
  if (DEFAULT_SECRET_KEY_PATTERN.test(key))
@@ -5020,7 +5094,7 @@ function upsertSecretSafetyConfig(input) {
5020
5094
  saveConfig({ ...config, secret_safety: next });
5021
5095
  return next;
5022
5096
  }
5023
- var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS;
5097
+ var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
5024
5098
  var init_redaction = __esm(() => {
5025
5099
  init_config2();
5026
5100
  DEFAULT_SECRET_PATTERNS = [
@@ -5044,6 +5118,7 @@ var init_redaction = __esm(() => {
5044
5118
  "completion_tokens",
5045
5119
  "cost_tokens"
5046
5120
  ]);
5121
+ REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
5047
5122
  });
5048
5123
 
5049
5124
  // src/lib/workspace-trust.ts
@@ -6683,7 +6758,7 @@ function scanTextForSecrets(text, options = {}) {
6683
6758
  };
6684
6759
  }
6685
6760
  function redactText(text, options = {}) {
6686
- const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER;
6761
+ const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER2;
6687
6762
  let out = text;
6688
6763
  for (const { pattern, allowlist_ok } of DEFAULT_PATTERNS) {
6689
6764
  out = out.replace(new RegExp(pattern.source, pattern.flags), (match) => {
@@ -6744,7 +6819,7 @@ function redactExportRecord(record) {
6744
6819
  function getDefaultSecretPatterns() {
6745
6820
  return DEFAULT_PATTERNS.map((p) => ({ name: p.name, source: p.pattern.source }));
6746
6821
  }
6747
- var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
6822
+ var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER2 = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
6748
6823
  var init_secret_redaction = __esm(() => {
6749
6824
  init_redaction();
6750
6825
  SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
@@ -8801,10 +8876,11 @@ function getTaskWithRelations(id, db) {
8801
8876
  JOIN task_dependencies td ON td.depends_on = t.id
8802
8877
  WHERE td.task_id = ?`).all(id);
8803
8878
  const dependencies = depRows.map(rowToTask);
8804
- const blockedByRows = d.query(`SELECT t.* FROM tasks t
8879
+ const blocked_by = dependencies.filter((dep) => isBlockingDependencyStatus(dep.status));
8880
+ const blocksRows = d.query(`SELECT t.* FROM tasks t
8805
8881
  JOIN task_dependencies td ON td.task_id = t.id
8806
8882
  WHERE td.depends_on = ?`).all(id);
8807
- const blocked_by = blockedByRows.map(rowToTask);
8883
+ const blocks = blocksRows.map(rowToTask);
8808
8884
  const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
8809
8885
  const parent = task.parent_id ? getTask(task.parent_id, d) : null;
8810
8886
  const checklist = getChecklist(id, d);
@@ -8813,6 +8889,7 @@ function getTaskWithRelations(id, db) {
8813
8889
  subtasks,
8814
8890
  dependencies,
8815
8891
  blocked_by,
8892
+ blocks,
8816
8893
  comments,
8817
8894
  parent,
8818
8895
  checklist
@@ -11936,7 +12013,7 @@ var init_dispatches = __esm(() => {
11936
12013
  // package.json
11937
12014
  var package_default = {
11938
12015
  name: "@hasna/todos",
11939
- version: "0.13.1",
12016
+ version: "0.13.3",
11940
12017
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
11941
12018
  type: "module",
11942
12019
  main: "dist/index.js",
@@ -11970,6 +12047,10 @@ var package_default = {
11970
12047
  "./storage": {
11971
12048
  types: "./dist/storage.d.ts",
11972
12049
  import: "./dist/storage.js"
12050
+ },
12051
+ "./testing": {
12052
+ types: "./dist/testing.d.ts",
12053
+ import: "./dist/testing.js"
11973
12054
  }
11974
12055
  },
11975
12056
  workspaces: [
@@ -11982,13 +12063,13 @@ var package_default = {
11982
12063
  "README.md"
11983
12064
  ],
11984
12065
  scripts: {
11985
- 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",
11986
- "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/*'",
11987
12068
  migrate: "bun run src/server/index.ts migrate",
11988
12069
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
11989
12070
  "generate:sdk": "bun run scripts/generate-sdk.ts",
11990
12071
  "build:dashboard": "cd dashboard && bun install --frozen-lockfile && bun run build",
11991
- typecheck: "tsc --noEmit",
12072
+ typecheck: "tsc --noEmit -p tsconfig.typecheck.json",
11992
12073
  test: "bun test",
11993
12074
  "test:no-cloud": "bun test src/no-cloud-boundary.test.ts src/local-first.test.ts src/lib/public-release-gate.test.ts",
11994
12075
  "dev:cli": "bun run src/cli/index.tsx",
@@ -17730,11 +17811,12 @@ var DEFAULT_LIMITS = {
17730
17811
  verification_limit: 10,
17731
17812
  run_limit: 3,
17732
17813
  dependency_limit: 12,
17733
- plan_task_limit: 20,
17814
+ plan_task_limit: 24,
17734
17815
  max_text_chars: 6000,
17735
17816
  summary_char_limit: 480,
17736
17817
  stale_after_hours: 72
17737
17818
  };
17819
+ var MAX_CONTEXT_TASKS = 24;
17738
17820
  var ALL_CONTEXT_SECTIONS = [
17739
17821
  "project",
17740
17822
  "plan",
@@ -17767,7 +17849,7 @@ function limits(input) {
17767
17849
  verification_limit: clamp(input.verification_limit, DEFAULT_LIMITS.verification_limit, 100),
17768
17850
  run_limit: clamp(input.run_limit, DEFAULT_LIMITS.run_limit, 20),
17769
17851
  dependency_limit: clamp(input.dependency_limit, DEFAULT_LIMITS.dependency_limit, 100),
17770
- 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),
17771
17853
  max_text_chars: clamp(input.max_text_chars, DEFAULT_LIMITS.max_text_chars, 50000),
17772
17854
  summary_char_limit: clamp(input.summary_char_limit, DEFAULT_LIMITS.summary_char_limit, 4000),
17773
17855
  stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
@@ -17790,6 +17872,13 @@ function taskSummary(task2) {
17790
17872
  priority: task2.priority
17791
17873
  };
17792
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
+ }
17793
17882
  function acceptanceCriteria(task2, maxText) {
17794
17883
  const metadata = task2.metadata || {};
17795
17884
  const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
@@ -17887,7 +17976,7 @@ function summarizeSection(pack, section, maxChars) {
17887
17976
  return "No plan was attached.";
17888
17977
  return summarizeStrings([
17889
17978
  `Plan ${pack.plan.name} is ${pack.plan.status}`,
17890
- `${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`,
17891
17980
  ...pack.plan.tasks.slice(0, 4).map((task2) => `${task2.status} ${task2.short_id || task2.id.slice(0, 8)} ${task2.title}`)
17892
17981
  ], maxChars);
17893
17982
  }
@@ -18037,7 +18126,8 @@ function createAgentContextPack(input, db) {
18037
18126
  const limit = limits(input);
18038
18127
  const project = task2.project_id ? getProject(task2.project_id, d) : null;
18039
18128
  const plan = task2.plan_id ? getPlan(task2.plan_id, d) : null;
18040
- 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;
18041
18131
  const upstream = getTaskDependencies(task2.id, d).map((dep) => taskSummary(getTask(dep.depends_on, d))).filter((item) => Boolean(item));
18042
18132
  const downstream = getTaskDependents(task2.id, d).map((dep) => taskSummary(getTask(dep.task_id, d))).filter((item) => Boolean(item));
18043
18133
  const comments = listComments(task2.id, d);
@@ -18080,6 +18170,9 @@ function createAgentContextPack(input, db) {
18080
18170
  warnings.push(`${runs.length - selectedRuns.length} older runs omitted`);
18081
18171
  if (fileMap.size > relevantFiles.length)
18082
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
+ }
18083
18176
  const contextTask = {
18084
18177
  id: task2.id,
18085
18178
  short_id: task2.short_id,
@@ -18111,8 +18204,9 @@ function createAgentContextPack(input, db) {
18111
18204
  description: truncate2(plan.description, limit.max_text_chars),
18112
18205
  status: plan.status,
18113
18206
  agent_id: plan.agent_id,
18114
- tasks: planTasks.slice(0, limit.plan_task_limit).map(taskSummary).filter((item) => Boolean(item)),
18115
- 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)
18116
18210
  } : null,
18117
18211
  acceptance_criteria: acceptanceCriteria(task2, limit.max_text_chars),
18118
18212
  dependencies: {
@@ -18222,6 +18316,8 @@ ${pack.task.description}` : null,
18222
18316
  "## Project And Plan",
18223
18317
  pack.project ? `- Project: ${pack.project.name} (${pack.project.path})` : "- Project: none",
18224
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,
18225
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,
18226
18322
  "",
18227
18323
  "## Acceptance Criteria",
@@ -19890,7 +19986,7 @@ function createReleaseCompatibilityReport(options = {}) {
19890
19986
  "todos --help",
19891
19987
  "todos-mcp --help",
19892
19988
  "todos-serve --help",
19893
- "todos doctor"
19989
+ "todos doctor --no-fail-on-findings"
19894
19990
  ],
19895
19991
  smoke_tests: [
19896
19992
  "command -v todos",
@@ -19898,7 +19994,7 @@ function createReleaseCompatibilityReport(options = {}) {
19898
19994
  "command -v todos-serve",
19899
19995
  "todos --version",
19900
19996
  "todos --help",
19901
- "todos doctor"
19997
+ "todos doctor --no-fail-on-findings"
19902
19998
  ],
19903
19999
  rollback: [
19904
20000
  "npm view @hasna/todos versions --json",
@@ -20392,12 +20488,11 @@ ${task2.description || ""}`.split(/\r?\n/);
20392
20488
  function normalizeStackLocation(value) {
20393
20489
  return value.replace(/:\d+(?::\d+)?/g, "").replace(/^file:\/\//, "").toLowerCase().trim();
20394
20490
  }
20395
- function fingerprint(task2) {
20491
+ function fingerprintContent(task2) {
20396
20492
  const body = task2.description || "";
20397
20493
  const text = `${task2.title}
20398
20494
  ${body}`;
20399
20495
  return {
20400
- task: task2,
20401
20496
  title: normalizeText(task2.title),
20402
20497
  body: normalizeText(body),
20403
20498
  text: normalizeText(text),
@@ -20406,6 +20501,9 @@ ${body}`;
20406
20501
  stackKeys: stackKeysFor(task2)
20407
20502
  };
20408
20503
  }
20504
+ function fingerprint(task2) {
20505
+ return { task: task2, ...fingerprintContent(task2) };
20506
+ }
20409
20507
  function intersects(left, right) {
20410
20508
  for (const value of left)
20411
20509
  if (right.has(value))
@@ -23592,6 +23690,13 @@ var TODOS_PACKAGE_EXPORTS = [
23592
23690
  types: "./dist/storage.d.ts",
23593
23691
  description: "Storage and service adapter contracts for local SQLite implementations.",
23594
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"
23595
23700
  }
23596
23701
  ];
23597
23702
  function source8(version) {
@@ -23623,6 +23728,9 @@ var TODOS_REGISTRY = createTodosRegistry({
23623
23728
  // src/storage/index.ts
23624
23729
  init_config();
23625
23730
 
23731
+ // src/storage/local-sqlite.ts
23732
+ init_types();
23733
+
23626
23734
  // src/lib/search.ts
23627
23735
  init_database();
23628
23736
  function rowToTask3(row) {
@@ -24439,6 +24547,267 @@ init_audit();
24439
24547
  init_comments();
24440
24548
  init_database();
24441
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
+
24442
24811
  // src/storage/sqlite-snapshot.ts
24443
24812
  init_database();
24444
24813
  init_audit();
@@ -24830,15 +25199,15 @@ function resolveTaskRefLocal(db, ref) {
24830
25199
  return null;
24831
25200
  if (TASK_UUID_RE.test(raw))
24832
25201
  return getTask(raw, db);
24833
- 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}`)}%`);
24834
25203
  if (prefixRows.length > 1) {
24835
- 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 })));
24836
25205
  }
24837
25206
  if (prefixRows.length === 1)
24838
25207
  return getTask(prefixRows[0].id, db);
24839
- 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);
24840
25209
  if (shortIdRows.length > 1) {
24841
- 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 })));
24842
25211
  }
24843
25212
  if (shortIdRows.length === 1)
24844
25213
  return getTask(shortIdRows[0].id, db);
@@ -24987,6 +25356,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
24987
25356
  exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
24988
25357
  importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
24989
25358
  },
25359
+ integrity: {
25360
+ report: () => scanSqliteIntegrity(database())
25361
+ },
24990
25362
  transaction: (fn) => {
24991
25363
  const tx = database().transaction(() => fn(adapter));
24992
25364
  return tx();
@@ -24995,6 +25367,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
24995
25367
  return adapter;
24996
25368
  }
24997
25369
 
25370
+ // src/storage/postgres-adapter.ts
25371
+ init_types();
25372
+ import { randomUUID as randomUUID3 } from "crypto";
25373
+
24998
25374
  // src/storage/postgres-sync.ts
24999
25375
  var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
25000
25376
  var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
@@ -25384,54 +25760,7 @@ function assertSafeIdentifier(value) {
25384
25760
  throw new Error(`Unsafe Postgres identifier: ${value}`);
25385
25761
  }
25386
25762
 
25387
- // src/storage/hybrid.ts
25388
- function createHybridTodosStorageAdapter(options) {
25389
- const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
25390
- const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
25391
- if (!syncStore)
25392
- throw new Error("hybrid storage requires a Postgres sync store or query client");
25393
- if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
25394
- throw new Error("hybrid storage requires local snapshot export/import support");
25395
- }
25396
- const exportSnapshot = async (context) => await local.sync.exportSnapshot(context);
25397
- const importSnapshot = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
25398
- const remote = {
25399
- ensureSchema: () => syncStore.ensureSchema(),
25400
- async pushSnapshot(context) {
25401
- const snapshot = await exportSnapshot(context);
25402
- return syncStore.pushSnapshot(snapshot, context);
25403
- },
25404
- async pullSnapshot(options2, context) {
25405
- const snapshot = await syncStore.pullSnapshot(options2);
25406
- return importSnapshot(snapshot, context);
25407
- },
25408
- async syncOnce(options2, context) {
25409
- const pulled = await remote.pullSnapshot(options2, context);
25410
- const pushed = await remote.pushSnapshot(context);
25411
- return { pulled, pushed };
25412
- }
25413
- };
25414
- return {
25415
- ...local,
25416
- kind: "hybrid",
25417
- capabilities: {
25418
- ...local.capabilities,
25419
- localPersistence: true,
25420
- remotePersistence: true,
25421
- sync: true
25422
- },
25423
- sync: {
25424
- ...local.sync,
25425
- exportSnapshot,
25426
- importSnapshot
25427
- },
25428
- remote
25429
- };
25430
- }
25431
-
25432
25763
  // src/storage/postgres-adapter.ts
25433
- init_types();
25434
- import { randomUUID as randomUUID3 } from "crypto";
25435
25764
  init_redaction();
25436
25765
  function createPostgresTodosStorageAdapter(options) {
25437
25766
  const store = new PostgresJsonRecordStore(options);
@@ -25559,6 +25888,9 @@ function createPostgresTodosStorageAdapter(options) {
25559
25888
  exportSnapshot: () => exportSnapshot(store),
25560
25889
  importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
25561
25890
  },
25891
+ integrity: {
25892
+ report: () => store.integrityReport()
25893
+ },
25562
25894
  transaction: (fn) => fn(adapter)
25563
25895
  };
25564
25896
  return adapter;
@@ -25665,8 +25997,9 @@ class PostgresJsonRecordStore {
25665
25997
  conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
25666
25998
  if (filter.session_id !== undefined)
25667
25999
  conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
25668
- if (filter.tags?.length)
25669
- 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
+ }
25670
26003
  if (filter.has_recurrence !== undefined) {
25671
26004
  conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
25672
26005
  }
@@ -25727,7 +26060,8 @@ class PostgresJsonRecordStore {
25727
26060
  AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
25728
26061
  LIMIT 2`, [this.service, raw, upper]);
25729
26062
  if (prefixResult.rows.length > 1) {
25730
- 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 })));
25731
26065
  }
25732
26066
  if (prefixResult.rows.length === 1) {
25733
26067
  return payloadRecord2(prefixResult.rows[0].payload);
@@ -25738,7 +26072,8 @@ class PostgresJsonRecordStore {
25738
26072
  AND LOWER(payload->>'short_id') = $2
25739
26073
  LIMIT 2`, [this.service, raw]);
25740
26074
  if (shortIdResult.rows.length > 1) {
25741
- 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 })));
25742
26077
  }
25743
26078
  if (shortIdResult.rows.length === 1) {
25744
26079
  return payloadRecord2(shortIdResult.rows[0].payload);
@@ -25752,6 +26087,24 @@ class PostgresJsonRecordStore {
25752
26087
  const result = await this.options.client.query(sql, params);
25753
26088
  return Number(result.rows[0]?.count ?? 0);
25754
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
+ }
25755
26108
  async listTombstones() {
25756
26109
  await this.ensureSchema();
25757
26110
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
@@ -26227,7 +26580,7 @@ async function startTask2(id, agentId, store) {
26227
26580
  if (task2.status !== "pending" && task2.status !== "in_progress") {
26228
26581
  throw new Error(`Task is ${task2.status} and cannot be started by ${agentId}`);
26229
26582
  }
26230
- return patchTask(task2, {
26583
+ const started = await patchTask(task2, {
26231
26584
  status: "in_progress",
26232
26585
  assigned_to: task2.assigned_to ?? agentId,
26233
26586
  agent_id: task2.agent_id ?? agentId,
@@ -26235,6 +26588,8 @@ async function startTask2(id, agentId, store) {
26235
26588
  locked_at: new Date().toISOString(),
26236
26589
  started_at: task2.started_at ?? new Date().toISOString()
26237
26590
  }, store);
26591
+ await logTaskChange2(task2.id, "start", "status", task2.status, "in_progress", agentId, store);
26592
+ return started;
26238
26593
  }
26239
26594
  async function completeTask2(id, agentId, options, store) {
26240
26595
  const task2 = await store.completeTask(id, agentId, options);
@@ -26352,9 +26707,11 @@ async function removeDependency2(taskId, dependsOn, store) {
26352
26707
  }
26353
26708
  async function listDependencies(taskId, store) {
26354
26709
  const edges = await store.list("dependencies");
26710
+ const incoming = edges.filter((edge) => edge.depends_on === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on }));
26355
26711
  return {
26356
26712
  dependencies: edges.filter((edge) => edge.task_id === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on })),
26357
- blocked_by: edges.filter((edge) => edge.depends_on === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on }))
26713
+ blocks: incoming,
26714
+ blocked_by: incoming
26358
26715
  };
26359
26716
  }
26360
26717
  async function addVerification(input, store, context) {
@@ -27341,17 +27698,17 @@ function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
27341
27698
  init_config();
27342
27699
  function createTodosStorageAdapter(options = {}) {
27343
27700
  const config = options.config ?? loadTodosStorageConfig(options.env);
27344
- if (config.mode === "local") {
27701
+ if (config.mode === "sqlite") {
27345
27702
  if (isTodosShadowEnabled(options.env))
27346
27703
  return createShadowAdapter(options, config);
27347
27704
  return createLocalSqliteTodosStorageAdapter(options.local);
27348
27705
  }
27349
27706
  assertTodosRemoteStorageConfig(config);
27350
- const adapter = config.mode === "hybrid" ? options.hybridAdapter ?? createImplicitHybridAdapter(options) ?? options.remoteAdapter : options.remoteAdapter ?? createImplicitPostgresAdapter(options);
27707
+ const adapter = options.remoteAdapter ?? createImplicitPostgresAdapter(options);
27351
27708
  if (!adapter) {
27352
- 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.");
27353
27710
  }
27354
- assertRemoteAdapterCapabilities(adapter, config.mode);
27711
+ assertPostgresAdapterCapabilities(adapter);
27355
27712
  return adapter;
27356
27713
  }
27357
27714
  function createShadowAdapter(options, config) {
@@ -27365,7 +27722,7 @@ function createShadowAdapter(options, config) {
27365
27722
  ...options.local ? { local: options.local } : {},
27366
27723
  ...options.postgresSyncStore ? { syncStore: options.postgresSyncStore } : {},
27367
27724
  ...postgresClient ? { postgresClient } : {},
27368
- ...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
27725
+ ...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
27369
27726
  });
27370
27727
  }
27371
27728
  function createImplicitPostgresAdapter(options) {
@@ -27373,25 +27730,12 @@ function createImplicitPostgresAdapter(options) {
27373
27730
  return null;
27374
27731
  return createPostgresTodosStorageAdapter({
27375
27732
  client: options.postgresClient,
27376
- ...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
27377
- });
27378
- }
27379
- function createImplicitHybridAdapter(options) {
27380
- if (!options.postgresClient && !options.postgresSyncStore)
27381
- return null;
27382
- return createHybridTodosStorageAdapter({
27383
- ...options.hybrid ?? {},
27384
- local: options.local,
27385
- postgresClient: options.postgresClient,
27386
- syncStore: options.postgresSyncStore
27733
+ ...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
27387
27734
  });
27388
27735
  }
27389
- function assertRemoteAdapterCapabilities(adapter, mode) {
27736
+ function assertPostgresAdapterCapabilities(adapter) {
27390
27737
  if (!adapter.capabilities.remotePersistence) {
27391
- throw new Error(`${mode} storage adapter must set capabilities.remotePersistence=true`);
27392
- }
27393
- if (mode === "hybrid" && !adapter.capabilities.localPersistence) {
27394
- throw new Error("hybrid storage adapter must also set capabilities.localPersistence=true");
27738
+ throw new Error("postgres storage adapter must set capabilities.remotePersistence=true");
27395
27739
  }
27396
27740
  }
27397
27741
  // src/storage/shadow-outbox.ts
@@ -27718,6 +28062,50 @@ async function closeRuntimeShadowCloud() {
27718
28062
  await cloud.close().catch(() => {});
27719
28063
  }
27720
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
+ }
27721
28109
  // src/storage/comment-redaction-backfill.ts
27722
28110
  init_redaction();
27723
28111
  var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
@@ -31296,10 +31684,10 @@ function getNativeStorageStatus(env = process.env) {
31296
31684
  } catch (error) {
31297
31685
  issues.push(error instanceof Error ? error.message : String(error));
31298
31686
  }
31299
- const remoteEnabled = isTodosRemoteStorageEnabled(config);
31687
+ const remoteEnabled = isTodosPostgresBackend(config);
31300
31688
  const remoteFieldsConfigured = Boolean(config.database || config.objectStorage);
31301
31689
  if (!remoteEnabled && remoteFieldsConfigured) {
31302
- 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`);
31303
31691
  }
31304
31692
  if (remoteEnabled && !config.objectStorage) {
31305
31693
  warnings.push(`${TODOS_STORAGE_ENV.s3Bucket} is not configured, so artifact sync will stay local`);
@@ -31308,7 +31696,7 @@ function getNativeStorageStatus(env = process.env) {
31308
31696
  ok: issues.length === 0,
31309
31697
  service: "todos",
31310
31698
  mode: config.mode,
31311
- local_default: config.mode === "local",
31699
+ local_default: config.mode === "sqlite",
31312
31700
  remote_enabled: remoteEnabled,
31313
31701
  database: {
31314
31702
  configured: Boolean(config.database),
@@ -31390,8 +31778,12 @@ function storageEnvStatus(env) {
31390
31778
  ]));
31391
31779
  }
31392
31780
  function fallbackConfig(env) {
31393
- const modeValue = clean2(env[TODOS_STORAGE_ENV.mode]);
31394
- 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
+ }
31395
31787
  return {
31396
31788
  service: "todos",
31397
31789
  mode,
@@ -41733,6 +42125,8 @@ init_database();
41733
42125
  init_tasks();
41734
42126
  init_task_graph();
41735
42127
  init_task_lifecycle();
42128
+ init_types();
42129
+ init_types();
41736
42130
  var DEPENDENCY_GRAPH_SCHEMA = "todos.dependency_graph.v1";
41737
42131
  var TASK_DEPENDENCY_EDGES_SCHEMA = "todos.task_dependency_edges.v1";
41738
42132
  var PROJECT_DEPENDENCY_GRAPH_SCHEMA = "todos.project_dependency_graph.v1";
@@ -41988,13 +42382,14 @@ function getBlockers(taskId, db) {
41988
42382
  missing_dependencies: missing
41989
42383
  };
41990
42384
  }
41991
- function buildTaskDependencyEdges(task2, dependencies, blocked_by) {
42385
+ function buildTaskDependencyEdges(task2, dependencies, blocks) {
41992
42386
  return {
41993
42387
  schema_version: TASK_DEPENDENCY_EDGES_SCHEMA,
41994
42388
  task_id: task2.id,
41995
42389
  short_id: task2.short_id,
41996
42390
  dependencies: dependencies.map(toNode),
41997
- blocked_by: blocked_by.map(toNode)
42391
+ blocked_by: dependencies.filter((dep) => isBlockingDependencyStatus(dep.status)).map(toNode),
42392
+ blocks: blocks.map(toNode)
41998
42393
  };
41999
42394
  }
42000
42395
  function getTaskDependencyEdges(taskId, db) {
@@ -42003,8 +42398,8 @@ function getTaskDependencyEdges(taskId, db) {
42003
42398
  if (!task2)
42004
42399
  return null;
42005
42400
  const dependencies = getTaskDependencies(taskId, d).map((edge) => getTask(edge.depends_on, d)).filter((t) => Boolean(t));
42006
- const blocked_by = getTaskDependents(taskId, d).map((edge) => getTask(edge.task_id, d)).filter((t) => Boolean(t));
42007
- return buildTaskDependencyEdges(task2, dependencies, blocked_by);
42401
+ const blocks = getTaskDependents(taskId, d).map((edge) => getTask(edge.task_id, d)).filter((t) => Boolean(t));
42402
+ return buildTaskDependencyEdges(task2, dependencies, blocks);
42008
42403
  }
42009
42404
  function buildProjectDependencyGraph(projectId, tasks, edges, generatedAt = new Date().toISOString()) {
42010
42405
  const nodeIds = new Set(tasks.map((t) => t.id));
@@ -50567,7 +50962,7 @@ function parseObject4(value) {
50567
50962
  function normalizeKey(value) {
50568
50963
  return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
50569
50964
  }
50570
- function normalizeFingerprint(value) {
50965
+ function normalizeTaskFindingFingerprint(value) {
50571
50966
  const normalized = normalizeKey(value);
50572
50967
  if (!normalized)
50573
50968
  throw new Error("finding fingerprint is required");
@@ -50676,7 +51071,7 @@ function assertTask(taskId, db) {
50676
51071
  throw new TaskNotFoundError(taskId);
50677
51072
  }
50678
51073
  function nextFinding(input, db) {
50679
- const fingerprint4 = normalizeFingerprint(input.fingerprint);
51074
+ const fingerprint4 = normalizeTaskFindingFingerprint(input.fingerprint);
50680
51075
  const title = redactOptional(input.title, 300);
50681
51076
  if (!title)
50682
51077
  throw new Error("finding title is required");
@@ -50824,7 +51219,7 @@ function resolveMissingTaskFindings(input, db) {
50824
51219
  const timestamp3 = input.resolved_at || now();
50825
51220
  const status = normalizeResolutionStatus(input.status);
50826
51221
  const runId = resolveRunForTask(input.run_id, input.task_id, d);
50827
- const present = new Set(input.fingerprints.map(normalizeFingerprint));
51222
+ const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
50828
51223
  const warnings = [];
50829
51224
  const conditions = ["task_id = ?", "status = 'open'"];
50830
51225
  const params = [input.task_id];
@@ -51351,12 +51746,12 @@ init_artifact_store();
51351
51746
 
51352
51747
  // src/lib/doctor.ts
51353
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";
51354
51751
  init_migrations();
51355
51752
  init_schema();
51356
51753
  init_recurrence();
51357
- import { chmodSync, copyFileSync as copyFileSync2, existsSync as existsSync30, mkdirSync as mkdirSync22, statSync as statSync12 } from "fs";
51358
- import { basename as basename7, dirname as dirname18, join as join27 } from "path";
51359
- var REQUIRED_TABLES2 = [
51754
+ var REQUIRED_TABLES3 = [
51360
51755
  "_migrations",
51361
51756
  "projects",
51362
51757
  "tasks",
@@ -51369,7 +51764,7 @@ var REQUIRED_TABLES2 = [
51369
51764
  "task_run_commands",
51370
51765
  "task_run_artifacts"
51371
51766
  ];
51372
- function tableExists(db, table) {
51767
+ function tableExists2(db, table) {
51373
51768
  return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
51374
51769
  }
51375
51770
  function quoteIdent(identifier) {
@@ -51453,7 +51848,7 @@ function findDuplicateIndexes(db) {
51453
51848
  return duplicates;
51454
51849
  }
51455
51850
  function findMissingProjectRoots(db) {
51456
- if (!tableExists(db, "projects"))
51851
+ if (!tableExists2(db, "projects"))
51457
51852
  return 0;
51458
51853
  let missing = 0;
51459
51854
  const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
@@ -51468,7 +51863,7 @@ function findMissingProjectRoots(db) {
51468
51863
  return missing;
51469
51864
  }
51470
51865
  function addTaskStateChecks(db, checks) {
51471
- if (!tableExists(db, "tasks"))
51866
+ if (!tableExists2(db, "tasks"))
51472
51867
  return;
51473
51868
  const columns = getTableColumns(db, "tasks");
51474
51869
  const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
@@ -51540,15 +51935,42 @@ function createBackup(dbPath) {
51540
51935
  function pushRepair(repairs, type, message, applied, count3) {
51541
51936
  repairs.push({ type, message, applied, count: count3 });
51542
51937
  }
51543
- function summarize3(checks, repairs) {
51938
+ function summarize3(checks, repairs, integrity) {
51544
51939
  return {
51545
51940
  errors: checks.filter((check) => check.severity === "error").length,
51546
51941
  warnings: checks.filter((check) => check.severity === "warn").length,
51547
51942
  infos: checks.filter((check) => check.severity === "info").length,
51548
51943
  repairable: checks.filter((check) => check.repairable).length,
51549
- 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
51550
51948
  };
51551
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
+ }
51552
51974
  function deleteOrphans(db, table, where) {
51553
51975
  const before = countQuery2(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
51554
51976
  if (before > 0)
@@ -51577,7 +51999,7 @@ function runTodosDoctor(options = {}) {
51577
51999
  message: `Schema at migration ${migrationCurrent}`
51578
52000
  });
51579
52001
  }
51580
- const missingTables = REQUIRED_TABLES2.filter((table) => !tableExists(db, table));
52002
+ const missingTables = REQUIRED_TABLES3.filter((table) => !tableExists2(db, table));
51581
52003
  if (missingTables.length > 0) {
51582
52004
  addCheck(checks, {
51583
52005
  severity: "error",
@@ -51587,7 +52009,7 @@ function runTodosDoctor(options = {}) {
51587
52009
  repairable: true
51588
52010
  });
51589
52011
  }
51590
- 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;
51591
52013
  if (orphanedParents > 0) {
51592
52014
  addCheck(checks, {
51593
52015
  severity: "error",
@@ -51597,7 +52019,7 @@ function runTodosDoctor(options = {}) {
51597
52019
  repairable: true
51598
52020
  });
51599
52021
  }
51600
- 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;
51601
52023
  if (orphanedDependencies > 0) {
51602
52024
  addCheck(checks, {
51603
52025
  severity: "error",
@@ -51616,7 +52038,7 @@ function runTodosDoctor(options = {}) {
51616
52038
  ];
51617
52039
  let orphanedRows2 = 0;
51618
52040
  for (const [table, where] of orphanTables) {
51619
- if (!tableExists(db, table))
52041
+ if (!tableExists2(db, table))
51620
52042
  continue;
51621
52043
  orphanedRows2 += countQuery2(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
51622
52044
  }
@@ -51630,6 +52052,8 @@ function runTodosDoctor(options = {}) {
51630
52052
  });
51631
52053
  }
51632
52054
  addTaskStateChecks(db, checks);
52055
+ const integrity = scanSqliteIntegrity(db);
52056
+ addIntegrityChecks(integrity, checks);
51633
52057
  const corruptJson = findCorruptJsonMetadata(db);
51634
52058
  if (corruptJson.length > 0) {
51635
52059
  addCheck(checks, {
@@ -51688,12 +52112,12 @@ function runTodosDoctor(options = {}) {
51688
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)");
51689
52113
  pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
51690
52114
  }
51691
- if (orphanedDependencies > 0 && tableExists(db, "task_dependencies")) {
52115
+ if (orphanedDependencies > 0 && tableExists2(db, "task_dependencies")) {
51692
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)");
51693
52117
  pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count3);
51694
52118
  }
51695
52119
  for (const [table, where] of orphanTables) {
51696
- if (!tableExists(db, table))
52120
+ if (!tableExists2(db, table))
51697
52121
  continue;
51698
52122
  const count3 = deleteOrphans(db, table, where);
51699
52123
  if (count3 > 0)
@@ -51722,16 +52146,19 @@ function runTodosDoctor(options = {}) {
51722
52146
  }
51723
52147
  }
51724
52148
  }
51725
- const finalChecks = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }).checks : checks;
51726
- 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);
51727
52153
  return {
51728
- ok: !finalChecks.some((check) => check.severity === "error"),
52154
+ ok: !finalChecks.some((check) => check.severity === "error") && finalIntegrity.summary.ok,
51729
52155
  dry_run: !apply,
51730
52156
  database_path: dbPath,
51731
52157
  migration: { current: getMigrationLevel(db), expected: migrationExpected },
51732
52158
  backup,
51733
52159
  checks: finalChecks,
51734
52160
  repairs,
52161
+ integrity: finalIntegrity,
51735
52162
  summary
51736
52163
  };
51737
52164
  }
@@ -52277,6 +52704,7 @@ export {
52277
52704
  updateArtifactRedaction,
52278
52705
  updateAgentActivity,
52279
52706
  updateAgent,
52707
+ unverifiedCondition,
52280
52708
  unsubscribeResource,
52281
52709
  unlockTask,
52282
52710
  unlinkTaskCommit,
@@ -52297,6 +52725,7 @@ export {
52297
52725
  summarizeTaskRun,
52298
52726
  summarizeRoadmap,
52299
52727
  summarizeMilestone,
52728
+ summarizeIntegrity,
52300
52729
  suggestAgentNames,
52301
52730
  subscribeResource,
52302
52731
  storeArtifactContent,
@@ -52337,6 +52766,7 @@ export {
52337
52766
  scorePlanHealth,
52338
52767
  scheduleTask,
52339
52768
  scanTextForSecrets,
52769
+ scanSqliteIntegrity,
52340
52770
  scanSourceForForbiddenWebPatterns,
52341
52771
  scanReminders,
52342
52772
  scanPreWriteText,
@@ -52384,6 +52814,7 @@ export {
52384
52814
  resolvePartialId,
52385
52815
  resolveMissingTaskFindings,
52386
52816
  resolveMentions,
52817
+ resolveIntegritySeverity,
52387
52818
  resolveGitRoot,
52388
52819
  resolveCommandQuery,
52389
52820
  resolveAccessProfile,
@@ -52498,6 +52929,7 @@ export {
52498
52929
  patrolTasks,
52499
52930
  parseTmuxTarget,
52500
52931
  parseStorageMode,
52932
+ parseStorageBackend,
52501
52933
  parseRecurrenceRule,
52502
52934
  parseQuietHours,
52503
52935
  parsePlanArtifactMarkdown,
@@ -52526,6 +52958,8 @@ export {
52526
52958
  migrationDryRun,
52527
52959
  migrateWorkflowStates,
52528
52960
  mergeDuplicateTask,
52961
+ measuredCondition,
52962
+ measureIntegrityRows,
52529
52963
  materializePlanSteps,
52530
52964
  matchCapabilities,
52531
52965
  looksSensitiveKey,
@@ -52644,12 +53078,14 @@ export {
52644
53078
  isWorktreePath,
52645
53079
  isValidRecurrenceRule,
52646
53080
  isTodosRemoteStorageEnabled,
53081
+ isTodosPostgresBackend,
52647
53082
  isSnapshotStale,
52648
53083
  isEncryptedValue,
52649
53084
  isEncryptedBridgeBundle,
52650
53085
  isArtifactExpired,
52651
53086
  isAllowedLocalApiUrl,
52652
53087
  isAgentConflict,
53088
+ integrityCondition,
52653
53089
  installTemplateLibrary,
52654
53090
  installLocalExtension,
52655
53091
  inspectPlanArtifact,
@@ -52692,6 +53128,7 @@ export {
52692
53128
  getTodosStorageEnvName,
52693
53129
  getTodosStorageDatabaseUrl,
52694
53130
  getTodosStorageDatabaseEnv,
53131
+ getTodosStorageBackend,
52695
53132
  getTimeReport,
52696
53133
  getTimeLogs,
52697
53134
  getTerminalNotificationRule,
@@ -52899,6 +53336,7 @@ export {
52899
53336
  formatLockConflict,
52900
53337
  formatKnowledgeSnapshotMarkdown,
52901
53338
  formatIssueImportPreviewText,
53339
+ formatIntegrityMessage,
52902
53340
  formatIntakePreviewText,
52903
53341
  formatHandoffPacket,
52904
53342
  formatGoalHandoff,
@@ -53129,16 +53567,19 @@ export {
53129
53567
  bulkAddTaskFiles,
53130
53568
  buildTaskDependencyEdges,
53131
53569
  buildTaskBoardSnapshot,
53570
+ buildSqliteIntegritySql,
53132
53571
  buildS3ObjectUrl,
53133
53572
  buildS3ObjectKey,
53134
53573
  buildRunReplayBundle,
53135
53574
  buildResourceSnapshot,
53136
53575
  buildReportExportData,
53137
53576
  buildProjectDependencyGraph,
53577
+ buildPostgresIntegritySql,
53138
53578
  buildPlanArtifactSnapshot,
53139
53579
  buildMcpToolGroups,
53140
53580
  buildMachineTopologyReport,
53141
53581
  buildKnowledgeSnapshotPayload,
53582
+ buildIntegrityReport,
53142
53583
  buildHandoffPacket,
53143
53584
  buildFeatureManifest,
53144
53585
  buildFailureTriageReport,
@@ -53173,6 +53614,7 @@ export {
53173
53614
  appendRunCommand,
53174
53615
  analyzeDependencyGraph,
53175
53616
  agentClaimNextSafe,
53617
+ adoptRemoteIntegrityReport,
53176
53618
  addTemplateTasks,
53177
53619
  addTaskRunFile,
53178
53620
  addTaskRunEvent,
@@ -53219,6 +53661,7 @@ export {
53219
53661
  TODOS_LOCAL_BACKUP_KIND,
53220
53662
  TODOS_JSON_CONTRACTS_MANIFEST,
53221
53663
  TODOS_JSON_CONTRACTS,
53664
+ TODOS_INTEGRITY_SCHEMA_VERSION,
53222
53665
  TODOS_ERROR_CODES,
53223
53666
  TODOS_ENCRYPTION_SCHEMA_VERSION,
53224
53667
  TODOS_ENCRYPTED_VALUE_KIND,
@@ -53261,7 +53704,7 @@ export {
53261
53704
  REMINDER_STATUSES,
53262
53705
  RELEASE_CHECK_SCHEMA,
53263
53706
  RELATIONSHIP_TYPES,
53264
- REDACTION_PLACEHOLDER,
53707
+ REDACTION_PLACEHOLDER2 as REDACTION_PLACEHOLDER,
53265
53708
  ProjectNotFoundError,
53266
53709
  PreWriteSecretError,
53267
53710
  PrGroupLedgerError,
@@ -53275,6 +53718,7 @@ export {
53275
53718
  PLAN_EXECUTION_SCHEMA,
53276
53719
  PLAN_EXECUTION_MODES,
53277
53720
  PARITY_SCHEMA_VERSION,
53721
+ OPEN_TASK_STATUSES,
53278
53722
  NOTIFICATION_REMINDERS_SCHEMA,
53279
53723
  NL_INTAKE_SCHEMA,
53280
53724
  NESTED_SUBCOMMANDS,
@@ -53303,6 +53747,7 @@ export {
53303
53747
  JSON_OUTPUT_CONTRACT,
53304
53748
  ISSUE_SOURCES,
53305
53749
  ISSUE_IMPORT_SCHEMA,
53750
+ INTEGRITY_CONDITIONS,
53306
53751
  INTAKE_TRIAGE_STATUSES,
53307
53752
  INTAKE_SOURCE_TYPES,
53308
53753
  INBOX_INTAKE_SCHEMA,