@hasna/todos 0.13.2 → 0.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +78 -14
  2. package/dist/cli/cloud-router.d.ts +57 -7
  3. package/dist/cli/cloud-router.d.ts.map +1 -1
  4. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  5. package/dist/cli/doctor-integrity.d.ts +106 -0
  6. package/dist/cli/doctor-integrity.d.ts.map +1 -0
  7. package/dist/cli/helpers.d.ts +2 -2
  8. package/dist/cli/index.js +1461 -778
  9. package/dist/cli/stage-a.d.ts.map +1 -1
  10. package/dist/contracts.js +118 -30
  11. package/dist/db/database.d.ts.map +1 -1
  12. package/dist/db/findings.d.ts +1 -0
  13. package/dist/db/findings.d.ts.map +1 -1
  14. package/dist/db/integrity.d.ts +20 -0
  15. package/dist/db/integrity.d.ts.map +1 -0
  16. package/dist/index.d.ts +5 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +562 -132
  19. package/dist/lib/context-packs.d.ts +1 -0
  20. package/dist/lib/context-packs.d.ts.map +1 -1
  21. package/dist/lib/dependency-graph.d.ts +1 -1
  22. package/dist/lib/doctor.d.ts +20 -0
  23. package/dist/lib/doctor.d.ts.map +1 -1
  24. package/dist/lib/integrity.d.ts +215 -0
  25. package/dist/lib/integrity.d.ts.map +1 -0
  26. package/dist/lib/native-storage-status.d.ts +2 -2
  27. package/dist/lib/native-storage-status.d.ts.map +1 -1
  28. package/dist/lib/release-compatibility.d.ts.map +1 -1
  29. package/dist/lib/task-dedupe.d.ts +15 -0
  30. package/dist/lib/task-dedupe.d.ts.map +1 -1
  31. package/dist/mcp/index.d.ts.map +1 -1
  32. package/dist/mcp/index.js +517 -85
  33. package/dist/mcp.js +7 -3
  34. package/dist/registry.d.ts +1 -1
  35. package/dist/registry.d.ts.map +1 -1
  36. package/dist/registry.js +125 -30
  37. package/dist/release-provenance.json +5 -5
  38. package/dist/sdk/types.d.ts +42 -0
  39. package/dist/sdk/types.d.ts.map +1 -1
  40. package/dist/sdk/v1.generated.d.ts +1 -0
  41. package/dist/sdk/v1.generated.d.ts.map +1 -1
  42. package/dist/server/cloud.d.ts +8 -1
  43. package/dist/server/cloud.d.ts.map +1 -1
  44. package/dist/server/index.js +521 -89
  45. package/dist/server/openapi.d.ts +12 -0
  46. package/dist/server/openapi.d.ts.map +1 -1
  47. package/dist/server/serve.d.ts.map +1 -1
  48. package/dist/server/v1.d.ts.map +1 -1
  49. package/dist/storage/config.d.ts +23 -5
  50. package/dist/storage/config.d.ts.map +1 -1
  51. package/dist/storage/factory.d.ts +9 -3
  52. package/dist/storage/factory.d.ts.map +1 -1
  53. package/dist/storage/hybrid.d.ts +7 -0
  54. package/dist/storage/hybrid.d.ts.map +1 -1
  55. package/dist/storage/index.d.ts +3 -3
  56. package/dist/storage/index.d.ts.map +1 -1
  57. package/dist/storage/interfaces.d.ts +29 -8
  58. package/dist/storage/interfaces.d.ts.map +1 -1
  59. package/dist/storage/local-sqlite.d.ts.map +1 -1
  60. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  61. package/dist/storage.d.ts +3 -3
  62. package/dist/storage.d.ts.map +1 -1
  63. package/dist/storage.js +601 -250
  64. package/dist/testing.d.ts +79 -0
  65. package/dist/testing.d.ts.map +1 -0
  66. package/dist/testing.js +107 -0
  67. package/dist/types/index.d.ts +12 -1
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/package.json +7 -3
package/dist/mcp/index.js CHANGED
@@ -44,7 +44,7 @@ var __require = import.meta.require;
44
44
  function isBlockingDependencyStatus(status) {
45
45
  return status !== "completed" && status !== "cancelled";
46
46
  }
47
- var TASK_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
47
+ var TASK_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
48
48
  var init_types = __esm(() => {
49
49
  TASK_STATUSES = [
50
50
  "pending",
@@ -77,6 +77,22 @@ var init_types = __esm(() => {
77
77
  this.name = "TaskNotFoundError";
78
78
  }
79
79
  };
80
+ TaskReferenceAmbiguousError = class TaskReferenceAmbiguousError extends Error {
81
+ reference;
82
+ static code = "TASK_REFERENCE_AMBIGUOUS";
83
+ candidateTaskIds;
84
+ candidateProjectIds;
85
+ constructor(reference, candidates) {
86
+ const candidateTaskIds = [...new Set(candidates.map((candidate) => candidate.task_id))].sort();
87
+ const candidateProjectIds = [...new Set(candidates.map((candidate) => candidate.project_id).filter((projectId) => typeof projectId === "string" && projectId.length > 0))].sort();
88
+ const projectList = candidateProjectIds.length > 0 ? candidateProjectIds.join(", ") : "(unscoped)";
89
+ super(`Task reference is ambiguous: "${reference}". Candidate project IDs: ${projectList}. ` + "Use a full task UUID.");
90
+ this.reference = reference;
91
+ this.name = "TaskReferenceAmbiguousError";
92
+ this.candidateTaskIds = candidateTaskIds;
93
+ this.candidateProjectIds = candidateProjectIds;
94
+ }
95
+ };
80
96
  ProjectNotFoundError = class ProjectNotFoundError extends Error {
81
97
  projectId;
82
98
  static code = "PROJECT_NOT_FOUND";
@@ -3766,15 +3782,18 @@ var init_identity_mapping = __esm(() => {
3766
3782
  var exports_config = {};
3767
3783
  __export(exports_config, {
3768
3784
  parseStorageMode: () => parseStorageMode,
3785
+ parseStorageBackend: () => parseStorageBackend,
3769
3786
  loadTodosStorageConfig: () => loadTodosStorageConfig,
3770
3787
  loadStorageConfig: () => loadStorageConfig,
3771
3788
  isTodosShadowEnabled: () => isTodosShadowEnabled,
3772
3789
  isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
3790
+ isTodosPostgresBackend: () => isTodosPostgresBackend,
3773
3791
  getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
3774
3792
  getTodosStorageMode: () => getTodosStorageMode,
3775
3793
  getTodosStorageEnvName: () => getTodosStorageEnvName,
3776
3794
  getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
3777
3795
  getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
3796
+ getTodosStorageBackend: () => getTodosStorageBackend,
3778
3797
  getStorageMode: () => getStorageMode,
3779
3798
  getStorageDatabaseUrl: () => getStorageDatabaseUrl,
3780
3799
  getStorageDatabaseEnv: () => getStorageDatabaseEnv,
@@ -3799,7 +3818,7 @@ function getCanonicalTodosRdsConfig(env = process.env) {
3799
3818
  };
3800
3819
  }
3801
3820
  function loadTodosStorageConfig(env = process.env) {
3802
- const mode = getTodosStorageMode(env);
3821
+ const mode = getTodosStorageBackend(env);
3803
3822
  const databaseUrl = getTodosStorageDatabaseUrl(env);
3804
3823
  const bucket = readStorageEnv(env, "s3Bucket").value;
3805
3824
  const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
@@ -3836,29 +3855,41 @@ function loadTodosStorageConfig(env = process.env) {
3836
3855
  function loadStorageConfig(env = process.env) {
3837
3856
  return loadTodosStorageConfig(env);
3838
3857
  }
3858
+ function isTodosPostgresBackend(config) {
3859
+ return config.mode === "postgres";
3860
+ }
3839
3861
  function isTodosRemoteStorageEnabled(config) {
3840
- return config.mode === "remote" || config.mode === "hybrid";
3862
+ return isTodosPostgresBackend(config);
3841
3863
  }
3842
3864
  function assertTodosRemoteStorageConfig(config) {
3843
- if (!isTodosRemoteStorageEnabled(config))
3865
+ if (!isTodosPostgresBackend(config))
3844
3866
  return;
3845
3867
  if (!config.database?.url) {
3846
3868
  throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
3847
3869
  }
3848
3870
  }
3849
- function parseStorageMode(value) {
3871
+ function parseStorageBackend(value) {
3850
3872
  const normalized = clean(value)?.toLowerCase();
3851
3873
  if (!normalized)
3852
- return "local";
3853
- if (normalized === "local" || normalized === "remote" || normalized === "hybrid")
3874
+ return "sqlite";
3875
+ if (normalized === "sqlite" || normalized === "postgres")
3854
3876
  return normalized;
3855
- throw new Error(`${TODOS_STORAGE_ENV.mode} must be local, remote, or hybrid`);
3877
+ const legacy = LEGACY_BACKEND_TOKENS[normalized];
3878
+ if (legacy)
3879
+ return legacy;
3880
+ throw new Error(`${TODOS_STORAGE_ENV.mode} must be sqlite or postgres (legacy values local and remote are accepted)`);
3881
+ }
3882
+ function parseStorageMode(value) {
3883
+ return parseStorageBackend(value);
3884
+ }
3885
+ function getTodosStorageBackend(env = process.env) {
3886
+ return parseStorageBackend(readStorageEnv(env, "mode").value);
3856
3887
  }
3857
3888
  function getTodosStorageMode(env = process.env) {
3858
- return parseStorageMode(readStorageEnv(env, "mode").value);
3889
+ return getTodosStorageBackend(env);
3859
3890
  }
3860
3891
  function getStorageMode(env = process.env) {
3861
- return getTodosStorageMode(env);
3892
+ return getTodosStorageBackend(env);
3862
3893
  }
3863
3894
  function isTodosShadowEnabled(env = process.env) {
3864
3895
  return parseBoolean(readStorageEnv(env, "shadow").value, false);
@@ -3869,8 +3900,8 @@ function getTodosStorageShadowEnvName(env = process.env) {
3869
3900
  function assertTodosShadowConfig(config, env = process.env) {
3870
3901
  if (!isTodosShadowEnabled(env))
3871
3902
  return;
3872
- if (config.mode !== "local") {
3873
- throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires ${TODOS_STORAGE_ENV.mode}=local (got ${config.mode})`);
3903
+ if (config.mode !== "sqlite") {
3904
+ throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
3874
3905
  }
3875
3906
  if (!config.database?.url) {
3876
3907
  throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
@@ -3926,7 +3957,7 @@ function parsePositiveInteger(value, fallback) {
3926
3957
  }
3927
3958
  return parsed;
3928
3959
  }
3929
- 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";
3960
+ 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;
3930
3961
  var init_config = __esm(() => {
3931
3962
  TODOS_STORAGE_TABLES = [
3932
3963
  "todos_sync_records",
@@ -3967,6 +3998,14 @@ var init_config = __esm(() => {
3967
3998
  syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
3968
3999
  syncDryRun: "TODOS_SYNC_DRY_RUN"
3969
4000
  };
4001
+ LEGACY_BACKEND_TOKENS = {
4002
+ local: "sqlite",
4003
+ remote: "postgres",
4004
+ postgresql: "postgres",
4005
+ hybrid: "postgres",
4006
+ self_hosted: "postgres",
4007
+ cloud: "postgres"
4008
+ };
3970
4009
  });
3971
4010
 
3972
4011
  // src/storage/shadow-outbox-schema.ts
@@ -4100,6 +4139,30 @@ function findGitRoot(startDir) {
4100
4139
  }
4101
4140
  return null;
4102
4141
  }
4142
+ function getGlobalDbPath() {
4143
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4144
+ return join(home, ".hasna", "todos", "todos.db");
4145
+ }
4146
+ function hasExplicitProjectArg(args = process.argv.slice(2)) {
4147
+ return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
4148
+ }
4149
+ function getCliCommand(args = process.argv.slice(2)) {
4150
+ const globalOptionsWithValues = new Set(["--project", "--agent", "--session"]);
4151
+ for (let index = 0;index < args.length; index += 1) {
4152
+ const arg = args[index];
4153
+ if (globalOptionsWithValues.has(arg)) {
4154
+ index += 1;
4155
+ continue;
4156
+ }
4157
+ if (arg.startsWith("-"))
4158
+ continue;
4159
+ return arg;
4160
+ }
4161
+ return null;
4162
+ }
4163
+ function canCreateScopedProjectDb(args = process.argv.slice(2)) {
4164
+ return getCliCommand(args) === "project-bootstrap" && !args.some((arg) => arg === "--dry-run" || arg.startsWith("--dry-run="));
4165
+ }
4103
4166
  function getDbPath() {
4104
4167
  if (process.env["HASNA_TODOS_DB_PATH"]) {
4105
4168
  return process.env["HASNA_TODOS_DB_PATH"];
@@ -4107,18 +4170,19 @@ function getDbPath() {
4107
4170
  if (process.env["TODOS_DB_PATH"]) {
4108
4171
  return process.env["TODOS_DB_PATH"];
4109
4172
  }
4173
+ if (hasExplicitProjectArg())
4174
+ return getGlobalDbPath();
4110
4175
  const cwd = process.cwd();
4111
4176
  const nearest = findNearestProjectDb(cwd);
4112
4177
  if (nearest)
4113
4178
  return nearest;
4114
4179
  if (process.env["TODOS_DB_SCOPE"] === "project") {
4115
4180
  const gitRoot = findGitRoot(cwd);
4116
- if (gitRoot) {
4181
+ if (gitRoot && canCreateScopedProjectDb()) {
4117
4182
  return join(gitRoot, ".hasna", "todos", "todos.db");
4118
4183
  }
4119
4184
  }
4120
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4121
- return join(home, ".hasna", "todos", "todos.db");
4185
+ return getGlobalDbPath();
4122
4186
  }
4123
4187
  function getDatabasePath() {
4124
4188
  return getDbPath();
@@ -4221,10 +4285,13 @@ function resolvePartialId(db, table, partialId) {
4221
4285
  return null;
4222
4286
  }
4223
4287
  if (table === "tasks") {
4224
- const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
4288
+ const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = LOWER(?) ORDER BY project_id, id").all(partialId);
4225
4289
  if (shortIdRows.length === 1) {
4226
4290
  return shortIdRows[0].id;
4227
4291
  }
4292
+ if (shortIdRows.length > 1) {
4293
+ throw new TaskReferenceAmbiguousError(partialId, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
4294
+ }
4228
4295
  }
4229
4296
  if (table === "task_lists") {
4230
4297
  const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
@@ -20022,9 +20089,6 @@ function cleanMode(value) {
20022
20089
  const normalized = value?.trim().toLowerCase();
20023
20090
  return normalized || null;
20024
20091
  }
20025
- function modeRole(mode) {
20026
- return mode === "local" ? "local" : "remote";
20027
- }
20028
20092
  function resolveTodosCliStorageMode(env = process.env) {
20029
20093
  for (const source of ["HASNA_TODOS_STORAGE_MODE", "TODOS_STORAGE_MODE"]) {
20030
20094
  if (env[source] !== undefined && env[source].trim() === "") {
@@ -20037,22 +20101,25 @@ function resolveTodosCliStorageMode(env = process.env) {
20037
20101
  ["HASNA_TODOS_STORAGE_MODE", canonical],
20038
20102
  ["TODOS_STORAGE_MODE", fallback]
20039
20103
  ]) {
20040
- if (value && !VALID_STORAGE_MODES.has(value)) {
20041
- throw new Error(`REMOTE_STORAGE_MODE_INVALID: ${source}=${value} must be local, remote, self_hosted, cloud, or hybrid; ` + "local SQLite fallback is disabled for invalid routing state");
20104
+ if (value && !(value in TRANSPORT_TOKENS)) {
20105
+ throw new Error(`REMOTE_STORAGE_MODE_INVALID: ${source}=${value} must be sqlite (local file) or http (hosted /v1 authority); ` + "legacy values local and remote are accepted; " + "local SQLite fallback is disabled for invalid routing state");
20042
20106
  }
20043
20107
  }
20044
- if (canonical && fallback && modeRole(canonical) !== modeRole(fallback)) {
20108
+ const canonicalTransport = canonical ? TRANSPORT_TOKENS[canonical] : null;
20109
+ const fallbackTransport = fallback ? TRANSPORT_TOKENS[fallback] : null;
20110
+ if (canonicalTransport && fallbackTransport && canonicalTransport !== fallbackTransport) {
20045
20111
  throw new Error(`REMOTE_STORAGE_MODE_CONFLICT: HASNA_TODOS_STORAGE_MODE=${canonical} conflicts with ` + `TODOS_STORAGE_MODE=${fallback}; local SQLite fallback is disabled`);
20046
20112
  }
20047
- const mode = canonical ?? fallback ?? "local";
20113
+ const transport = canonicalTransport ?? fallbackTransport ?? "sqlite";
20048
20114
  return {
20049
- mode,
20050
- selected: CLOUD_MODES.has(mode),
20115
+ mode: transport,
20116
+ transport,
20117
+ selected: transport === "http",
20051
20118
  source: canonical ? "HASNA_TODOS_STORAGE_MODE" : fallback ? "TODOS_STORAGE_MODE" : "default"
20052
20119
  };
20053
20120
  }
20054
- function requestedStorageMode(env) {
20055
- return resolveTodosCliStorageMode(env).mode;
20121
+ function requestedTransport(env) {
20122
+ return resolveTodosCliStorageMode(env).transport;
20056
20123
  }
20057
20124
  function normalizeRemoteAuthorityUrl(value) {
20058
20125
  const trimmed = value?.trim();
@@ -20105,7 +20172,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
20105
20172
  return {
20106
20173
  selected: false,
20107
20174
  ok: true,
20108
- mode: mode || "local",
20175
+ mode,
20109
20176
  api_url_configured: false,
20110
20177
  api_key_configured: false,
20111
20178
  v1_base_url: null,
@@ -20217,8 +20284,7 @@ async function requiredRemoteRoute(client, route, request) {
20217
20284
  }
20218
20285
  }
20219
20286
  function getTodosCloudClient(env = process.env) {
20220
- const mode = requestedStorageMode(env);
20221
- if (!CLOUD_MODES.has(mode))
20287
+ if (requestedTransport(env) !== "http")
20222
20288
  return null;
20223
20289
  const resolved = resolveStorageClient("todos", requireTodosRemoteAuthorityEnv(env), {
20224
20290
  fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" })
@@ -20253,13 +20319,38 @@ function toListQuery(filter = {}) {
20253
20319
  query["assigned_to"] = filter.assigned_to;
20254
20320
  if (filter.agent_id)
20255
20321
  query["agent_id"] = filter.agent_id;
20322
+ if (filter.tags?.length)
20323
+ query["tags"] = filter.tags.join(",");
20256
20324
  if (typeof filter.limit === "number")
20257
20325
  query["limit"] = filter.limit;
20258
20326
  if (typeof filter.offset === "number")
20259
20327
  query["offset"] = filter.offset;
20260
20328
  return query;
20261
20329
  }
20330
+ async function fetchListTagsCapability(client) {
20331
+ const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
20332
+ if (!document || typeof document !== "object" || Array.isArray(document))
20333
+ return false;
20334
+ const paths = document["paths"];
20335
+ const tasksPath = paths && typeof paths === "object" && !Array.isArray(paths) ? paths["/v1/tasks"] : undefined;
20336
+ const get = tasksPath && typeof tasksPath === "object" && !Array.isArray(tasksPath) ? tasksPath["get"] : undefined;
20337
+ const parameters = get && typeof get === "object" && !Array.isArray(get) ? get["parameters"] : undefined;
20338
+ return Array.isArray(parameters) && parameters.some((parameter) => parameter && typeof parameter === "object" && parameter["name"] === "tags");
20339
+ }
20340
+ async function requireTagsFilterCapability(client) {
20341
+ const authority = remoteAuthorityBase(client);
20342
+ let capability = listTagsCapabilityCache.get(authority);
20343
+ if (!capability) {
20344
+ capability = fetchListTagsCapability(client);
20345
+ listTagsCapabilityCache.set(authority, capability);
20346
+ }
20347
+ if (!await capability) {
20348
+ throw new Error(`REMOTE_TAGS_FILTER_UNSUPPORTED: configured Todos authority ${authority} does not advertise the tags ` + "query param on GET /v1/tasks; deploy the current @hasna/todos /v1 server to filter by tag; " + "no unfiltered task read was issued");
20349
+ }
20350
+ }
20262
20351
  async function cloudListTasks(client, filter = {}) {
20352
+ if (filter.tags?.length)
20353
+ await requireTagsFilterCapability(client);
20263
20354
  const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: toListQuery(filter) }));
20264
20355
  const envelope = res.raw;
20265
20356
  return Array.isArray(envelope?.tasks) ? envelope.tasks : res.items;
@@ -20367,6 +20458,8 @@ function redactComment(comment) {
20367
20458
  }
20368
20459
  async function cloudCountTasks(client, filter = {}) {
20369
20460
  const { limit: _drop, offset: _o, ...rest } = filter;
20461
+ if (rest.tags?.length)
20462
+ await requireTagsFilterCapability(client);
20370
20463
  const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: { ...toListQuery(rest), limit: 1 } }));
20371
20464
  const envelope = res.raw;
20372
20465
  if (typeof envelope?.total === "number")
@@ -20429,15 +20522,23 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
20429
20522
  }
20430
20523
  throw new Error(`Task list not found: "${input}"`);
20431
20524
  }
20432
- var UUID_RE, CLOUD_MODES, VALID_STORAGE_MODES, completionCapabilityCache;
20525
+ var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, listTagsCapabilityCache;
20433
20526
  var init_cloud_router = __esm(() => {
20434
20527
  init_types();
20435
20528
  init_redaction();
20436
20529
  init_http_client();
20437
20530
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
20438
- CLOUD_MODES = new Set(["self_hosted", "cloud", "remote", "hybrid"]);
20439
- VALID_STORAGE_MODES = new Set(["local", ...CLOUD_MODES]);
20531
+ TRANSPORT_TOKENS = {
20532
+ sqlite: "sqlite",
20533
+ http: "http",
20534
+ local: "sqlite",
20535
+ remote: "http",
20536
+ self_hosted: "http",
20537
+ cloud: "http",
20538
+ hybrid: "http"
20539
+ };
20440
20540
  completionCapabilityCache = new Map;
20541
+ listTagsCapabilityCache = new Map;
20441
20542
  });
20442
20543
 
20443
20544
  // src/mcp/tools/task-crud.ts
@@ -24023,12 +24124,11 @@ ${task.description || ""}`.split(/\r?\n/);
24023
24124
  function normalizeStackLocation(value) {
24024
24125
  return value.replace(/:\d+(?::\d+)?/g, "").replace(/^file:\/\//, "").toLowerCase().trim();
24025
24126
  }
24026
- function fingerprint(task) {
24127
+ function fingerprintContent(task) {
24027
24128
  const body = task.description || "";
24028
24129
  const text = `${task.title}
24029
24130
  ${body}`;
24030
24131
  return {
24031
- task,
24032
24132
  title: normalizeText(task.title),
24033
24133
  body: normalizeText(body),
24034
24134
  text: normalizeText(text),
@@ -24037,6 +24137,9 @@ ${body}`;
24037
24137
  stackKeys: stackKeysFor(task)
24038
24138
  };
24039
24139
  }
24140
+ function fingerprint(task) {
24141
+ return { task, ...fingerprintContent(task) };
24142
+ }
24040
24143
  function intersects(left, right) {
24041
24144
  for (const value of left)
24042
24145
  if (right.has(value))
@@ -25373,7 +25476,7 @@ function createReleaseCompatibilityReport(options = {}) {
25373
25476
  "todos --help",
25374
25477
  "todos-mcp --help",
25375
25478
  "todos-serve --help",
25376
- "todos doctor"
25479
+ "todos doctor --no-fail-on-findings"
25377
25480
  ],
25378
25481
  smoke_tests: [
25379
25482
  "command -v todos",
@@ -25381,7 +25484,7 @@ function createReleaseCompatibilityReport(options = {}) {
25381
25484
  "command -v todos-serve",
25382
25485
  "todos --version",
25383
25486
  "todos --help",
25384
- "todos doctor"
25487
+ "todos doctor --no-fail-on-findings"
25385
25488
  ],
25386
25489
  rollback: [
25387
25490
  "npm view @hasna/todos versions --json",
@@ -29027,6 +29130,211 @@ var init_auto_assign = __esm(() => {
29027
29130
  init_agents();
29028
29131
  });
29029
29132
 
29133
+ // src/lib/integrity.ts
29134
+ function resolveIntegritySeverity(spec, measurement) {
29135
+ if (spec.kind === "dangling")
29136
+ return "error";
29137
+ if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
29138
+ return "error";
29139
+ return spec.base_severity;
29140
+ }
29141
+ function plural(entity, count) {
29142
+ const [one, many] = ENTITY_LABEL[entity];
29143
+ return count === 1 ? one : many;
29144
+ }
29145
+ function formatIntegrityMessage(spec, measurement) {
29146
+ const noun = plural(spec.entity, measurement.count);
29147
+ const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
29148
+ const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
29149
+ 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}`;
29150
+ }
29151
+ function measuredCondition(spec, measurement, source) {
29152
+ return {
29153
+ id: spec.id,
29154
+ entity: spec.entity,
29155
+ field: spec.field,
29156
+ kind: spec.kind,
29157
+ count: measurement.count,
29158
+ open_count: measurement.open_count,
29159
+ severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
29160
+ verified: true,
29161
+ source,
29162
+ message: formatIntegrityMessage(spec, measurement),
29163
+ impact: spec.impact
29164
+ };
29165
+ }
29166
+ function unverifiedCondition(spec, reason) {
29167
+ return {
29168
+ id: spec.id,
29169
+ entity: spec.entity,
29170
+ field: spec.field,
29171
+ kind: spec.kind,
29172
+ count: null,
29173
+ open_count: null,
29174
+ severity: null,
29175
+ verified: false,
29176
+ source: "unverified",
29177
+ unverified_reason: reason,
29178
+ message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
29179
+ impact: spec.impact
29180
+ };
29181
+ }
29182
+ function summarizeIntegrity(conditions) {
29183
+ const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
29184
+ const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
29185
+ const unverified = conditions.length - measured.length;
29186
+ return {
29187
+ ok: unverified === 0 && findings.length === 0,
29188
+ findings: findings.length,
29189
+ rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
29190
+ errors: findings.filter((condition) => condition.severity === "error").length,
29191
+ warnings: findings.filter((condition) => condition.severity === "warn").length,
29192
+ unverified,
29193
+ complete: unverified === 0
29194
+ };
29195
+ }
29196
+ function buildIntegrityReport(conditions, generatedAt) {
29197
+ const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source) => source !== "unverified");
29198
+ return {
29199
+ schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
29200
+ generated_at: generatedAt,
29201
+ source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
29202
+ conditions,
29203
+ summary: summarizeIntegrity(conditions)
29204
+ };
29205
+ }
29206
+ function sqliteOpenStatusList() {
29207
+ return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
29208
+ }
29209
+ function buildSqliteIntegritySql(spec) {
29210
+ const table = SQLITE_TABLE[spec.entity];
29211
+ const target = SQLITE_TABLE[spec.target];
29212
+ const column = `t."${spec.field}"`;
29213
+ 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})`;
29214
+ const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
29215
+ return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
29216
+ }
29217
+ function buildPostgresIntegritySql(spec, options) {
29218
+ const params = [options.service, RECORD_TYPE[spec.entity]];
29219
+ const p = (value) => {
29220
+ params.push(value);
29221
+ return `$${params.length}`;
29222
+ };
29223
+ const column = `t.payload->>'${spec.field}'`;
29224
+ 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})`;
29225
+ const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
29226
+ 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}`;
29227
+ return { sql, params };
29228
+ }
29229
+ var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1", TERMINAL_TASK_STATUSES, OPEN_TASK_STATUSES, INTEGRITY_CONDITIONS, ENTITY_LABEL, SQLITE_TABLE, RECORD_TYPE;
29230
+ var init_integrity = __esm(() => {
29231
+ init_types();
29232
+ TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
29233
+ OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
29234
+ INTEGRITY_CONDITIONS = [
29235
+ {
29236
+ id: "tasks_without_project",
29237
+ entity: "task",
29238
+ field: "project_id",
29239
+ target: "project",
29240
+ kind: "missing",
29241
+ base_severity: "warn",
29242
+ escalate_when_open: true,
29243
+ impact: "invisible to every project-scoped read (list, status, next, claim)"
29244
+ },
29245
+ {
29246
+ id: "tasks_without_task_list",
29247
+ entity: "task",
29248
+ field: "task_list_id",
29249
+ target: "task_list",
29250
+ kind: "missing",
29251
+ base_severity: "warn",
29252
+ escalate_when_open: true,
29253
+ impact: "invisible to every task-list read \u2014 the list reports zero open work"
29254
+ },
29255
+ {
29256
+ id: "tasks_with_unregistered_project",
29257
+ entity: "task",
29258
+ field: "project_id",
29259
+ target: "project",
29260
+ kind: "dangling",
29261
+ base_severity: "error",
29262
+ escalate_when_open: false,
29263
+ impact: "points at a project id that does not exist; the reference can never resolve"
29264
+ },
29265
+ {
29266
+ id: "tasks_with_unregistered_task_list",
29267
+ entity: "task",
29268
+ field: "task_list_id",
29269
+ target: "task_list",
29270
+ kind: "dangling",
29271
+ base_severity: "error",
29272
+ escalate_when_open: false,
29273
+ impact: "points at a task-list id that does not exist; the reference can never resolve"
29274
+ },
29275
+ {
29276
+ id: "task_lists_without_project",
29277
+ entity: "task_list",
29278
+ field: "project_id",
29279
+ target: "project",
29280
+ kind: "missing",
29281
+ base_severity: "warn",
29282
+ escalate_when_open: false,
29283
+ impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
29284
+ },
29285
+ {
29286
+ id: "task_lists_with_unregistered_project",
29287
+ entity: "task_list",
29288
+ field: "project_id",
29289
+ target: "project",
29290
+ kind: "dangling",
29291
+ base_severity: "error",
29292
+ escalate_when_open: false,
29293
+ impact: "points at a project id that does not exist; the list can never be reached"
29294
+ }
29295
+ ];
29296
+ ENTITY_LABEL = {
29297
+ task: ["task", "tasks"],
29298
+ task_list: ["task list", "task lists"]
29299
+ };
29300
+ SQLITE_TABLE = {
29301
+ task: "tasks",
29302
+ task_list: "task_lists",
29303
+ project: "projects"
29304
+ };
29305
+ RECORD_TYPE = {
29306
+ task: "tasks",
29307
+ task_list: "task_lists",
29308
+ project: "projects"
29309
+ };
29310
+ });
29311
+
29312
+ // src/db/integrity.ts
29313
+ function tableExists(db, table) {
29314
+ return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
29315
+ }
29316
+ function scanSqliteIntegrity(db = getDatabase()) {
29317
+ const missing = Object.keys(REQUIRED_TABLES2).filter((table) => !tableExists(db, table));
29318
+ const conditions = INTEGRITY_CONDITIONS.map((spec) => {
29319
+ if (missing.length > 0) {
29320
+ return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
29321
+ }
29322
+ try {
29323
+ const row = db.query(buildSqliteIntegritySql(spec)).get();
29324
+ return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
29325
+ } catch (error) {
29326
+ return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
29327
+ }
29328
+ });
29329
+ return buildIntegrityReport(conditions, now());
29330
+ }
29331
+ var REQUIRED_TABLES2;
29332
+ var init_integrity2 = __esm(() => {
29333
+ init_database();
29334
+ init_integrity();
29335
+ REQUIRED_TABLES2 = { tasks: true, task_lists: true, projects: true };
29336
+ });
29337
+
29030
29338
  // src/lib/doctor.ts
29031
29339
  var exports_doctor = {};
29032
29340
  __export(exports_doctor, {
@@ -29034,7 +29342,7 @@ __export(exports_doctor, {
29034
29342
  });
29035
29343
  import { chmodSync, copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync5, statSync as statSync5 } from "fs";
29036
29344
  import { basename as basename4, dirname as dirname6, join as join8 } from "path";
29037
- function tableExists(db, table) {
29345
+ function tableExists2(db, table) {
29038
29346
  return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
29039
29347
  }
29040
29348
  function quoteIdent(identifier) {
@@ -29118,7 +29426,7 @@ function findDuplicateIndexes(db) {
29118
29426
  return duplicates;
29119
29427
  }
29120
29428
  function findMissingProjectRoots(db) {
29121
- if (!tableExists(db, "projects"))
29429
+ if (!tableExists2(db, "projects"))
29122
29430
  return 0;
29123
29431
  let missing = 0;
29124
29432
  const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
@@ -29133,7 +29441,7 @@ function findMissingProjectRoots(db) {
29133
29441
  return missing;
29134
29442
  }
29135
29443
  function addTaskStateChecks(db, checks) {
29136
- if (!tableExists(db, "tasks"))
29444
+ if (!tableExists2(db, "tasks"))
29137
29445
  return;
29138
29446
  const columns = getTableColumns(db, "tasks");
29139
29447
  const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
@@ -29205,15 +29513,42 @@ function createBackup(dbPath) {
29205
29513
  function pushRepair(repairs, type, message, applied, count) {
29206
29514
  repairs.push({ type, message, applied, count });
29207
29515
  }
29208
- function summarize2(checks, repairs) {
29516
+ function summarize2(checks, repairs, integrity) {
29209
29517
  return {
29210
29518
  errors: checks.filter((check) => check.severity === "error").length,
29211
29519
  warnings: checks.filter((check) => check.severity === "warn").length,
29212
29520
  infos: checks.filter((check) => check.severity === "info").length,
29213
29521
  repairable: checks.filter((check) => check.repairable).length,
29214
- applied: repairs.filter((repair) => repair.applied).length
29522
+ applied: repairs.filter((repair) => repair.applied).length,
29523
+ integrity_findings: integrity.summary.findings,
29524
+ integrity_rows: integrity.summary.rows,
29525
+ integrity_unverified: integrity.summary.unverified
29215
29526
  };
29216
29527
  }
29528
+ function addIntegrityChecks(integrity, checks) {
29529
+ for (const condition of integrity.conditions) {
29530
+ if (!condition.verified) {
29531
+ addCheck(checks, {
29532
+ severity: "warn",
29533
+ type: `${condition.id}_unverified`,
29534
+ message: condition.message,
29535
+ repairable: false,
29536
+ integrity: true
29537
+ });
29538
+ continue;
29539
+ }
29540
+ if ((condition.count ?? 0) === 0)
29541
+ continue;
29542
+ addCheck(checks, {
29543
+ severity: condition.severity ?? "warn",
29544
+ type: condition.id,
29545
+ message: `${condition.message} \u2014 ${condition.impact}`,
29546
+ count: condition.count ?? undefined,
29547
+ repairable: false,
29548
+ integrity: true
29549
+ });
29550
+ }
29551
+ }
29217
29552
  function deleteOrphans(db, table, where) {
29218
29553
  const before = countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
29219
29554
  if (before > 0)
@@ -29242,7 +29577,7 @@ function runTodosDoctor(options = {}) {
29242
29577
  message: `Schema at migration ${migrationCurrent}`
29243
29578
  });
29244
29579
  }
29245
- const missingTables = REQUIRED_TABLES2.filter((table) => !tableExists(db, table));
29580
+ const missingTables = REQUIRED_TABLES3.filter((table) => !tableExists2(db, table));
29246
29581
  if (missingTables.length > 0) {
29247
29582
  addCheck(checks, {
29248
29583
  severity: "error",
@@ -29252,7 +29587,7 @@ function runTodosDoctor(options = {}) {
29252
29587
  repairable: true
29253
29588
  });
29254
29589
  }
29255
- const orphanedParents = tableExists(db, "tasks") ? countQuery(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;
29590
+ const orphanedParents = tableExists2(db, "tasks") ? countQuery(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;
29256
29591
  if (orphanedParents > 0) {
29257
29592
  addCheck(checks, {
29258
29593
  severity: "error",
@@ -29262,7 +29597,7 @@ function runTodosDoctor(options = {}) {
29262
29597
  repairable: true
29263
29598
  });
29264
29599
  }
29265
- const orphanedDependencies = tableExists(db, "task_dependencies") && tableExists(db, "tasks") ? countQuery(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;
29600
+ const orphanedDependencies = tableExists2(db, "task_dependencies") && tableExists2(db, "tasks") ? countQuery(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;
29266
29601
  if (orphanedDependencies > 0) {
29267
29602
  addCheck(checks, {
29268
29603
  severity: "error",
@@ -29281,7 +29616,7 @@ function runTodosDoctor(options = {}) {
29281
29616
  ];
29282
29617
  let orphanedRows = 0;
29283
29618
  for (const [table, where] of orphanTables) {
29284
- if (!tableExists(db, table))
29619
+ if (!tableExists2(db, table))
29285
29620
  continue;
29286
29621
  orphanedRows += countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
29287
29622
  }
@@ -29295,6 +29630,8 @@ function runTodosDoctor(options = {}) {
29295
29630
  });
29296
29631
  }
29297
29632
  addTaskStateChecks(db, checks);
29633
+ const integrity = scanSqliteIntegrity(db);
29634
+ addIntegrityChecks(integrity, checks);
29298
29635
  const corruptJson = findCorruptJsonMetadata(db);
29299
29636
  if (corruptJson.length > 0) {
29300
29637
  addCheck(checks, {
@@ -29353,12 +29690,12 @@ function runTodosDoctor(options = {}) {
29353
29690
  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)");
29354
29691
  pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
29355
29692
  }
29356
- if (orphanedDependencies > 0 && tableExists(db, "task_dependencies")) {
29693
+ if (orphanedDependencies > 0 && tableExists2(db, "task_dependencies")) {
29357
29694
  const count = 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)");
29358
29695
  pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count);
29359
29696
  }
29360
29697
  for (const [table, where] of orphanTables) {
29361
- if (!tableExists(db, table))
29698
+ if (!tableExists2(db, table))
29362
29699
  continue;
29363
29700
  const count = deleteOrphans(db, table, where);
29364
29701
  if (count > 0)
@@ -29387,26 +29724,30 @@ function runTodosDoctor(options = {}) {
29387
29724
  }
29388
29725
  }
29389
29726
  }
29390
- const finalChecks = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }).checks : checks;
29391
- const summary = summarize2(finalChecks, repairs);
29727
+ const reread = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }) : undefined;
29728
+ const finalChecks = reread?.checks ?? checks;
29729
+ const finalIntegrity = reread?.integrity ?? integrity;
29730
+ const summary = summarize2(finalChecks, repairs, finalIntegrity);
29392
29731
  return {
29393
- ok: !finalChecks.some((check) => check.severity === "error"),
29732
+ ok: !finalChecks.some((check) => check.severity === "error") && finalIntegrity.summary.ok,
29394
29733
  dry_run: !apply,
29395
29734
  database_path: dbPath,
29396
29735
  migration: { current: getMigrationLevel(db), expected: migrationExpected },
29397
29736
  backup,
29398
29737
  checks: finalChecks,
29399
29738
  repairs,
29739
+ integrity: finalIntegrity,
29400
29740
  summary
29401
29741
  };
29402
29742
  }
29403
- var REQUIRED_TABLES2;
29743
+ var REQUIRED_TABLES3;
29404
29744
  var init_doctor = __esm(() => {
29405
29745
  init_database();
29746
+ init_integrity2();
29406
29747
  init_migrations();
29407
29748
  init_schema();
29408
29749
  init_recurrence();
29409
- REQUIRED_TABLES2 = [
29750
+ REQUIRED_TABLES3 = [
29410
29751
  "_migrations",
29411
29752
  "projects",
29412
29753
  "tasks",
@@ -29750,7 +30091,7 @@ function limits(input) {
29750
30091
  verification_limit: clamp(input.verification_limit, DEFAULT_LIMITS.verification_limit, 100),
29751
30092
  run_limit: clamp(input.run_limit, DEFAULT_LIMITS.run_limit, 20),
29752
30093
  dependency_limit: clamp(input.dependency_limit, DEFAULT_LIMITS.dependency_limit, 100),
29753
- plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit, 100),
30094
+ plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit, MAX_CONTEXT_TASKS),
29754
30095
  max_text_chars: clamp(input.max_text_chars, DEFAULT_LIMITS.max_text_chars, 50000),
29755
30096
  summary_char_limit: clamp(input.summary_char_limit, DEFAULT_LIMITS.summary_char_limit, 4000),
29756
30097
  stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
@@ -29773,6 +30114,13 @@ function taskSummary(task) {
29773
30114
  priority: task.priority
29774
30115
  };
29775
30116
  }
30117
+ function latestPlanTasks(planId, limit, db) {
30118
+ const rows = db.query(`SELECT * FROM tasks
30119
+ WHERE plan_id = ? AND archived_at IS NULL
30120
+ ORDER BY updated_at DESC, created_at DESC, id ASC
30121
+ LIMIT ?`).all(planId, limit);
30122
+ return rows.map(rowToTask);
30123
+ }
29776
30124
  function acceptanceCriteria(task, maxText) {
29777
30125
  const metadata = task.metadata || {};
29778
30126
  const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
@@ -29870,7 +30218,7 @@ function summarizeSection(pack, section, maxChars) {
29870
30218
  return "No plan was attached.";
29871
30219
  return summarizeStrings([
29872
30220
  `Plan ${pack.plan.name} is ${pack.plan.status}`,
29873
- `${pack.plan.tasks.length} listed plan tasks`,
30221
+ `${pack.plan.tasks.length} of ${pack.plan.total_tasks} plan tasks listed; ${pack.plan.omitted_tasks} omitted`,
29874
30222
  ...pack.plan.tasks.slice(0, 4).map((task) => `${task.status} ${task.short_id || task.id.slice(0, 8)} ${task.title}`)
29875
30223
  ], maxChars);
29876
30224
  }
@@ -30020,7 +30368,8 @@ function createAgentContextPack(input, db) {
30020
30368
  const limit = limits(input);
30021
30369
  const project = task.project_id ? getProject(task.project_id, d) : null;
30022
30370
  const plan = task.plan_id ? getPlan(task.plan_id, d) : null;
30023
- const planTasks = task.plan_id ? listTasks({ plan_id: task.plan_id, limit: limit.plan_task_limit + 1 }, d) : [];
30371
+ const planTasks = task.plan_id ? latestPlanTasks(task.plan_id, limit.plan_task_limit, d) : [];
30372
+ const totalPlanTasks = task.plan_id ? countTasks({ plan_id: task.plan_id }, d) : 0;
30024
30373
  const upstream = getTaskDependencies(task.id, d).map((dep) => taskSummary(getTask(dep.depends_on, d))).filter((item) => Boolean(item));
30025
30374
  const downstream = getTaskDependents(task.id, d).map((dep) => taskSummary(getTask(dep.task_id, d))).filter((item) => Boolean(item));
30026
30375
  const comments = listComments(task.id, d);
@@ -30063,6 +30412,9 @@ function createAgentContextPack(input, db) {
30063
30412
  warnings.push(`${runs.length - selectedRuns.length} older runs omitted`);
30064
30413
  if (fileMap.size > relevantFiles.length)
30065
30414
  warnings.push(`${fileMap.size - relevantFiles.length} relevant files omitted`);
30415
+ if (totalPlanTasks > planTasks.length && plan) {
30416
+ 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>"`);
30417
+ }
30066
30418
  const contextTask = {
30067
30419
  id: task.id,
30068
30420
  short_id: task.short_id,
@@ -30094,8 +30446,9 @@ function createAgentContextPack(input, db) {
30094
30446
  description: truncate2(plan.description, limit.max_text_chars),
30095
30447
  status: plan.status,
30096
30448
  agent_id: plan.agent_id,
30097
- tasks: planTasks.slice(0, limit.plan_task_limit).map(taskSummary).filter((item) => Boolean(item)),
30098
- omitted_tasks: Math.max(0, planTasks.length - limit.plan_task_limit)
30449
+ tasks: planTasks.map(taskSummary).filter((item) => Boolean(item)),
30450
+ total_tasks: totalPlanTasks,
30451
+ omitted_tasks: Math.max(0, totalPlanTasks - planTasks.length)
30099
30452
  } : null,
30100
30453
  acceptance_criteria: acceptanceCriteria(task, limit.max_text_chars),
30101
30454
  dependencies: {
@@ -30205,6 +30558,8 @@ ${pack.task.description}` : null,
30205
30558
  "## Project And Plan",
30206
30559
  pack.project ? `- Project: ${pack.project.name} (${pack.project.path})` : "- Project: none",
30207
30560
  pack.plan ? `- Plan: ${pack.plan.name} (${pack.plan.status})` : "- Plan: none",
30561
+ pack.plan ? `- Plan tasks: ${pack.plan.tasks.length} shown of ${pack.plan.total_tasks} total (${pack.plan.omitted_tasks} omitted)` : null,
30562
+ 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,
30208
30563
  pack.plan && pack.plan.tasks.length > 0 ? bullet(pack.plan.tasks.map((task) => `${task.status} ${task.short_id || task.id.slice(0, 8)} ${task.title}`)) : null,
30209
30564
  "",
30210
30565
  "## Acceptance Criteria",
@@ -30285,7 +30640,7 @@ function renderAgentContextPack(pack, format, compact2 = false) {
30285
30640
  return compact2 ? renderAgentContextPackCompactMarkdown(pack) : renderAgentContextPackMarkdown(pack);
30286
30641
  return compact2 ? JSON.stringify(pack) : JSON.stringify(pack, null, 2);
30287
30642
  }
30288
- var DEFAULT_LIMITS, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
30643
+ var DEFAULT_LIMITS, MAX_CONTEXT_TASKS = 24, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
30289
30644
  var init_context_packs = __esm(() => {
30290
30645
  init_comments();
30291
30646
  init_database();
@@ -30304,7 +30659,7 @@ var init_context_packs = __esm(() => {
30304
30659
  verification_limit: 10,
30305
30660
  run_limit: 3,
30306
30661
  dependency_limit: 12,
30307
- plan_task_limit: 20,
30662
+ plan_task_limit: 24,
30308
30663
  max_text_chars: 6000,
30309
30664
  summary_char_limit: 480,
30310
30665
  stale_after_hours: 72
@@ -31353,7 +31708,7 @@ function parseObject2(value) {
31353
31708
  function normalizeKey(value) {
31354
31709
  return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
31355
31710
  }
31356
- function normalizeFingerprint(value) {
31711
+ function normalizeTaskFindingFingerprint(value) {
31357
31712
  const normalized = normalizeKey(value);
31358
31713
  if (!normalized)
31359
31714
  throw new Error("finding fingerprint is required");
@@ -31462,7 +31817,7 @@ function assertTask(taskId, db) {
31462
31817
  throw new TaskNotFoundError(taskId);
31463
31818
  }
31464
31819
  function nextFinding(input, db) {
31465
- const fingerprint2 = normalizeFingerprint(input.fingerprint);
31820
+ const fingerprint2 = normalizeTaskFindingFingerprint(input.fingerprint);
31466
31821
  const title = redactOptional(input.title, 300);
31467
31822
  if (!title)
31468
31823
  throw new Error("finding title is required");
@@ -31610,7 +31965,7 @@ function resolveMissingTaskFindings(input, db) {
31610
31965
  const timestamp4 = input.resolved_at || now();
31611
31966
  const status = normalizeResolutionStatus(input.status);
31612
31967
  const runId = resolveRunForTask(input.run_id, input.task_id, d);
31613
- const present = new Set(input.fingerprints.map(normalizeFingerprint));
31968
+ const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
31614
31969
  const warnings = [];
31615
31970
  const conditions = ["task_id = ?", "status = 'open'"];
31616
31971
  const params = [input.task_id];
@@ -34198,7 +34553,7 @@ var package_default;
34198
34553
  var init_package = __esm(() => {
34199
34554
  package_default = {
34200
34555
  name: "@hasna/todos",
34201
- version: "0.13.2",
34556
+ version: "0.13.3",
34202
34557
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
34203
34558
  type: "module",
34204
34559
  main: "dist/index.js",
@@ -34232,6 +34587,10 @@ var init_package = __esm(() => {
34232
34587
  "./storage": {
34233
34588
  types: "./dist/storage.d.ts",
34234
34589
  import: "./dist/storage.js"
34590
+ },
34591
+ "./testing": {
34592
+ types: "./dist/testing.d.ts",
34593
+ import: "./dist/testing.js"
34235
34594
  }
34236
34595
  },
34237
34596
  workspaces: [
@@ -34244,8 +34603,8 @@ var init_package = __esm(() => {
34244
34603
  "README.md"
34245
34604
  ],
34246
34605
  scripts: {
34247
- 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",
34248
- "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/*'",
34606
+ 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",
34607
+ "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/*'",
34249
34608
  migrate: "bun run src/server/index.ts migrate",
34250
34609
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
34251
34610
  "generate:sdk": "bun run scripts/generate-sdk.ts",
@@ -43951,15 +44310,15 @@ function resolveTaskRefLocal(db, ref) {
43951
44310
  return null;
43952
44311
  if (TASK_UUID_RE.test(raw))
43953
44312
  return getTask(raw, db);
43954
- const prefixRows = db.query("SELECT id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
44313
+ 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}`)}%`);
43955
44314
  if (prefixRows.length > 1) {
43956
- throw new Error(`Task reference is ambiguous: "${ref}"`);
44315
+ throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
43957
44316
  }
43958
44317
  if (prefixRows.length === 1)
43959
44318
  return getTask(prefixRows[0].id, db);
43960
- const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
44319
+ const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
43961
44320
  if (shortIdRows.length > 1) {
43962
- throw new Error(`Task reference is ambiguous: "${ref}"`);
44321
+ throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
43963
44322
  }
43964
44323
  if (shortIdRows.length === 1)
43965
44324
  return getTask(shortIdRows[0].id, db);
@@ -44108,6 +44467,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
44108
44467
  exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
44109
44468
  importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
44110
44469
  },
44470
+ integrity: {
44471
+ report: () => scanSqliteIntegrity(database())
44472
+ },
44111
44473
  transaction: (fn) => {
44112
44474
  const tx = database().transaction(() => fn(adapter));
44113
44475
  return tx();
@@ -44117,6 +44479,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
44117
44479
  }
44118
44480
  var TASK_UUID_RE;
44119
44481
  var init_local_sqlite = __esm(() => {
44482
+ init_types();
44120
44483
  init_search();
44121
44484
  init_tasks();
44122
44485
  init_projects();
@@ -44127,6 +44490,7 @@ var init_local_sqlite = __esm(() => {
44127
44490
  init_audit();
44128
44491
  init_comments();
44129
44492
  init_database();
44493
+ init_integrity2();
44130
44494
  init_sqlite_snapshot();
44131
44495
  TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
44132
44496
  });
@@ -45051,6 +45415,9 @@ function createPostgresTodosStorageAdapter(options) {
45051
45415
  exportSnapshot: () => exportSnapshot(store),
45052
45416
  importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
45053
45417
  },
45418
+ integrity: {
45419
+ report: () => store.integrityReport()
45420
+ },
45054
45421
  transaction: (fn) => fn(adapter)
45055
45422
  };
45056
45423
  return adapter;
@@ -45157,8 +45524,9 @@ class PostgresJsonRecordStore {
45157
45524
  conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
45158
45525
  if (filter.session_id !== undefined)
45159
45526
  conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
45160
- if (filter.tags?.length)
45161
- conds.push(`payload->'tags' @> ${p(filter.tags)}::jsonb`);
45527
+ if (filter.tags?.length) {
45528
+ 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)})`);
45529
+ }
45162
45530
  if (filter.has_recurrence !== undefined) {
45163
45531
  conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
45164
45532
  }
@@ -45219,7 +45587,8 @@ class PostgresJsonRecordStore {
45219
45587
  AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
45220
45588
  LIMIT 2`, [this.service, raw, upper]);
45221
45589
  if (prefixResult.rows.length > 1) {
45222
- throw new Error(`Task reference is ambiguous: "${ref}"`);
45590
+ const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
45591
+ throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
45223
45592
  }
45224
45593
  if (prefixResult.rows.length === 1) {
45225
45594
  return payloadRecord2(prefixResult.rows[0].payload);
@@ -45230,7 +45599,8 @@ class PostgresJsonRecordStore {
45230
45599
  AND LOWER(payload->>'short_id') = $2
45231
45600
  LIMIT 2`, [this.service, raw]);
45232
45601
  if (shortIdResult.rows.length > 1) {
45233
- throw new Error(`Task reference is ambiguous: "${ref}"`);
45602
+ const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
45603
+ throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
45234
45604
  }
45235
45605
  if (shortIdResult.rows.length === 1) {
45236
45606
  return payloadRecord2(shortIdResult.rows[0].payload);
@@ -45244,6 +45614,24 @@ class PostgresJsonRecordStore {
45244
45614
  const result = await this.options.client.query(sql, params);
45245
45615
  return Number(result.rows[0]?.count ?? 0);
45246
45616
  }
45617
+ async integrityReport() {
45618
+ await this.ensureSchema();
45619
+ const conditions = [];
45620
+ for (const spec of INTEGRITY_CONDITIONS) {
45621
+ const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
45622
+ try {
45623
+ const result = await this.options.client.query(sql, params);
45624
+ const row = result.rows[0];
45625
+ conditions.push(measuredCondition(spec, {
45626
+ count: Number(row?.count ?? 0),
45627
+ open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
45628
+ }, "postgres"));
45629
+ } catch (error) {
45630
+ conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
45631
+ }
45632
+ }
45633
+ return buildIntegrityReport(conditions, new Date().toISOString());
45634
+ }
45247
45635
  async listTombstones() {
45248
45636
  await this.ensureSchema();
45249
45637
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
@@ -45719,7 +46107,7 @@ async function startTask2(id, agentId, store) {
45719
46107
  if (task2.status !== "pending" && task2.status !== "in_progress") {
45720
46108
  throw new Error(`Task is ${task2.status} and cannot be started by ${agentId}`);
45721
46109
  }
45722
- return patchTask(task2, {
46110
+ const started = await patchTask(task2, {
45723
46111
  status: "in_progress",
45724
46112
  assigned_to: task2.assigned_to ?? agentId,
45725
46113
  agent_id: task2.agent_id ?? agentId,
@@ -45727,6 +46115,8 @@ async function startTask2(id, agentId, store) {
45727
46115
  locked_at: new Date().toISOString(),
45728
46116
  started_at: task2.started_at ?? new Date().toISOString()
45729
46117
  }, store);
46118
+ await logTaskChange2(task2.id, "start", "status", task2.status, "in_progress", agentId, store);
46119
+ return started;
45730
46120
  }
45731
46121
  async function completeTask2(id, agentId, options, store) {
45732
46122
  const task2 = await store.completeTask(id, agentId, options);
@@ -46393,6 +46783,7 @@ var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'prior
46393
46783
  var init_postgres_adapter = __esm(() => {
46394
46784
  init_types();
46395
46785
  init_postgres_sync();
46786
+ init_integrity();
46396
46787
  init_redaction();
46397
46788
  TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
46398
46789
  });
@@ -46981,6 +47372,7 @@ __export(exports_cloud, {
46981
47372
  resolveCloudDatabaseUrl: () => resolveCloudDatabaseUrl,
46982
47373
  pingCloud: () => pingCloud,
46983
47374
  normalizeCloudPayloads: () => normalizeCloudPayloads,
47375
+ isPostgresBackendConfigured: () => isPostgresBackendConfigured,
46984
47376
  isCloudModeEnabled: () => isCloudModeEnabled,
46985
47377
  getCloudVerifier: () => getCloudVerifier,
46986
47378
  getCloudStorageAdapter: () => getCloudStorageAdapter,
@@ -47003,9 +47395,12 @@ function resolveCloudDatabaseUrl(env = process.env) {
47003
47395
  function resolveSigningSecret(env = process.env) {
47004
47396
  return env.HASNA_TODOS_API_SIGNING_KEY || env.HASNA_API_SIGNING_KEY || env.API_KEY_SIGNING_SECRET || undefined;
47005
47397
  }
47006
- function isCloudModeEnabled(env = process.env) {
47398
+ function isPostgresBackendConfigured(env = process.env) {
47007
47399
  return Boolean(resolveCloudDatabaseUrl(env));
47008
47400
  }
47401
+ function isCloudModeEnabled(env = process.env) {
47402
+ return isPostgresBackendConfigured(env);
47403
+ }
47009
47404
  function getClient() {
47010
47405
  if (cachedClient)
47011
47406
  return cachedClient;
@@ -47185,7 +47580,7 @@ function authNotConfiguredMessage(host) {
47185
47580
  `);
47186
47581
  }
47187
47582
  function resolveAuthPosture(input) {
47188
- const hosted = input.hosted ?? isCloudModeEnabled();
47583
+ const hosted = input.hosted ?? isPostgresBackendConfigured();
47189
47584
  const hasCredentialSource = Boolean(input.apiKey) || input.hasGeneratedKeys;
47190
47585
  if (hasCredentialSource) {
47191
47586
  return {
@@ -49146,6 +49541,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
49146
49541
  { name: "task_list_id", in: "query", schema: { type: "string" } },
49147
49542
  { name: "assigned_to", in: "query", schema: { type: "string" } },
49148
49543
  { name: "agent_id", in: "query", schema: { type: "string" } },
49544
+ { name: "tags", in: "query", schema: { type: "string" }, description: "Comma-separated tags; matches tasks carrying any of them" },
49149
49545
  { name: "limit", in: "query", schema: { type: "integer", minimum: 1 } },
49150
49546
  { name: "offset", in: "query", schema: { type: "integer", minimum: 0 } }
49151
49547
  ],
@@ -50350,6 +50746,9 @@ async function handleV1Request(req, url, dependencies = {}) {
50350
50746
  ...url.searchParams.get("task_list_id") ? { task_list_id: url.searchParams.get("task_list_id") } : {},
50351
50747
  ...url.searchParams.get("assigned_to") ? { assigned_to: url.searchParams.get("assigned_to") } : {},
50352
50748
  ...url.searchParams.get("agent_id") ? { agent_id: url.searchParams.get("agent_id") } : {},
50749
+ ...url.searchParams.get("tags") ? {
50750
+ tags: url.searchParams.get("tags").split(",").map((tag) => tag.trim()).filter(Boolean)
50751
+ } : {},
50353
50752
  ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
50354
50753
  ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
50355
50754
  };
@@ -50631,6 +51030,13 @@ async function handleV1Request(req, url, dependencies = {}) {
50631
51030
  try {
50632
51031
  task2 = await store.tasks.resolveRef(id);
50633
51032
  } catch (e) {
51033
+ if (e instanceof TaskReferenceAmbiguousError) {
51034
+ return error(409, e.message, {
51035
+ code: TaskReferenceAmbiguousError.code,
51036
+ candidate_project_ids: e.candidateProjectIds,
51037
+ candidate_task_ids: e.candidateTaskIds
51038
+ });
51039
+ }
50634
51040
  const msg = e.message || "";
50635
51041
  if (/ambiguous/i.test(msg))
50636
51042
  return error(409, msg);
@@ -50988,6 +51394,15 @@ async function handleV1Request(req, url, dependencies = {}) {
50988
51394
  ]);
50989
51395
  return json3({ tasks, tasks_all: tasksAll, subtasks: tasksAll - tasks, projects: projects.length });
50990
51396
  }
51397
+ if (resource === "integrity" && !id) {
51398
+ if (method !== "GET")
51399
+ return error(405, `method ${method} not allowed on /v1/integrity`);
51400
+ if (typeof store.integrity?.report !== "function") {
51401
+ return error(501, "referential-integrity reporting is not supported by this storage backend");
51402
+ }
51403
+ const integrity = await store.integrity.report();
51404
+ return json3({ integrity });
51405
+ }
50991
51406
  if (resource === "import") {
50992
51407
  if (method !== "POST")
50993
51408
  return error(405, `method ${method} not allowed on /v1/import`);
@@ -51007,6 +51422,13 @@ async function handleV1Request(req, url, dependencies = {}) {
51007
51422
  }
51008
51423
  return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
51009
51424
  } catch (e) {
51425
+ if (e instanceof TaskReferenceAmbiguousError) {
51426
+ return error(409, e.message, {
51427
+ code: TaskReferenceAmbiguousError.code,
51428
+ candidate_project_ids: e.candidateProjectIds,
51429
+ candidate_task_ids: e.candidateTaskIds
51430
+ });
51431
+ }
51010
51432
  if (e instanceof LockError)
51011
51433
  return error(409, e.message, { code: LockError.code });
51012
51434
  if (e instanceof ResourceConflictError)
@@ -51527,23 +51949,24 @@ Dashboard not found at: ${dashboardDir}`);
51527
51949
  }
51528
51950
  if ((path === "/health" || path === "/ready" || path === "/version") && method === "GET") {
51529
51951
  const { getPackageVersion: getPackageVersion2 } = await Promise.resolve().then(() => (init_package_version(), exports_package_version));
51530
- const { isCloudModeEnabled: isCloudModeEnabled2, pingCloud: pingCloud2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
51531
- const mode = isCloudModeEnabled2() ? "remote" : "local";
51952
+ const { isPostgresBackendConfigured: isPostgresBackendConfigured2, pingCloud: pingCloud2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
51953
+ const backend = isPostgresBackendConfigured2() ? "postgresql" : "sqlite";
51954
+ const mode = backend === "postgresql" ? "remote" : "local";
51532
51955
  const version = getPackageVersion2();
51533
51956
  if (path === "/version") {
51534
- return Response.json({ status: "ok", version, mode, name: "todos" });
51957
+ return Response.json({ status: "ok", version, mode, backend, name: "todos" });
51535
51958
  }
51536
51959
  if (path === "/ready") {
51537
- if (mode === "remote") {
51960
+ if (backend === "postgresql") {
51538
51961
  try {
51539
51962
  await pingCloud2();
51540
51963
  } catch (e) {
51541
- return Response.json({ status: "unavailable", version, mode, error: e.message }, { status: 503 });
51964
+ return Response.json({ status: "unavailable", version, mode, backend, error: e.message }, { status: 503 });
51542
51965
  }
51543
51966
  }
51544
- return Response.json({ status: "ready", version, mode });
51967
+ return Response.json({ status: "ready", version, mode, backend });
51545
51968
  }
51546
- return Response.json({ status: "ok", version, mode, name: "todos" });
51969
+ return Response.json({ status: "ok", version, mode, backend, name: "todos" });
51547
51970
  }
51548
51971
  if ((path === "/openapi.json" || path === "/v1/openapi.json") && method === "GET") {
51549
51972
  const { buildV1OpenApiDocument: buildV1OpenApiDocument2 } = await Promise.resolve().then(() => (init_openapi(), exports_openapi));
@@ -51899,6 +52322,15 @@ function formatError(error2) {
51899
52322
  if (error2 instanceof TaskNotFoundError) {
51900
52323
  return JSON.stringify({ code: TaskNotFoundError.code, message: error2.message, suggestion: TaskNotFoundError.suggestion });
51901
52324
  }
52325
+ if (error2 instanceof TaskReferenceAmbiguousError) {
52326
+ return JSON.stringify({
52327
+ code: TaskReferenceAmbiguousError.code,
52328
+ message: error2.message,
52329
+ candidate_project_ids: error2.candidateProjectIds,
52330
+ candidate_task_ids: error2.candidateTaskIds,
52331
+ suggestion: "Use a full task UUID."
52332
+ });
52333
+ }
51902
52334
  if (error2 instanceof ProjectNotFoundError) {
51903
52335
  return JSON.stringify({ code: ProjectNotFoundError.code, message: error2.message, suggestion: ProjectNotFoundError.suggestion });
51904
52336
  }