@hasna/todos 0.15.41 → 0.15.47

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 (45) hide show
  1. package/dashboard/dist/assets/index-BNQ4gJua.js +342 -0
  2. package/dashboard/dist/assets/index-DjzvHUWt.css +1 -0
  3. package/dashboard/dist/index.html +2 -2
  4. package/dist/cli/cloud-router.d.ts.map +1 -1
  5. package/dist/cli/components/Dashboard.d.ts +1 -1
  6. package/dist/cli/components/Dashboard.d.ts.map +1 -1
  7. package/dist/cli/components/Header.d.ts +1 -1
  8. package/dist/cli/components/Header.d.ts.map +1 -1
  9. package/dist/cli/components/ProjectList.d.ts +1 -1
  10. package/dist/cli/components/ProjectList.d.ts.map +1 -1
  11. package/dist/cli/components/SearchView.d.ts +1 -1
  12. package/dist/cli/components/SearchView.d.ts.map +1 -1
  13. package/dist/cli/components/TaskDetail.d.ts +1 -1
  14. package/dist/cli/components/TaskDetail.d.ts.map +1 -1
  15. package/dist/cli/components/TaskForm.d.ts +1 -1
  16. package/dist/cli/components/TaskForm.d.ts.map +1 -1
  17. package/dist/cli/components/TaskList.d.ts +1 -1
  18. package/dist/cli/components/TaskList.d.ts.map +1 -1
  19. package/dist/cli/index.js +521 -120
  20. package/dist/contracts.js +346 -83
  21. package/dist/db/task-graph.d.ts.map +1 -1
  22. package/dist/db/task-lifecycle.d.ts.map +1 -1
  23. package/dist/db/webhooks.d.ts.map +1 -1
  24. package/dist/index.js +499 -113
  25. package/dist/lib/instant-compare.d.ts +35 -0
  26. package/dist/lib/instant-compare.d.ts.map +1 -0
  27. package/dist/mcp/index.js +520 -119
  28. package/dist/mcp.js +5 -4
  29. package/dist/pr-groups/postgres.d.ts.map +1 -1
  30. package/dist/project-registration/postgres.d.ts.map +1 -1
  31. package/dist/project-registration.js +466 -100
  32. package/dist/registry.js +346 -83
  33. package/dist/release-provenance.json +5 -5
  34. package/dist/server/cloud.d.ts.map +1 -1
  35. package/dist/server/index.js +1179 -309
  36. package/dist/storage/local-sqlite.d.ts.map +1 -1
  37. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  38. package/dist/storage.js +447 -89
  39. package/dist/task-manifest/postgres.d.ts.map +1 -1
  40. package/dist/task-manifest.js +15 -6
  41. package/dist/task-subtree-transfer/postgres.d.ts.map +1 -1
  42. package/dist/task-subtree-transfer.js +15 -6
  43. package/package.json +5 -4
  44. package/dashboard/dist/assets/index-DJm6m6Yy.css +0 -1
  45. package/dashboard/dist/assets/index-DVotjwab.js +0 -346
package/dist/cli/index.js CHANGED
@@ -2123,7 +2123,7 @@ var package_default;
2123
2123
  var init_package = __esm(() => {
2124
2124
  package_default = {
2125
2125
  name: "@hasna/todos",
2126
- version: "0.15.41",
2126
+ version: "0.15.47",
2127
2127
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
2128
2128
  type: "module",
2129
2129
  main: "dist/index.js",
@@ -2241,18 +2241,19 @@ var init_package = __esm(() => {
2241
2241
  author: "Andrei Hasna <andrei@hasna.com>",
2242
2242
  license: "Apache-2.0",
2243
2243
  dependencies: {
2244
- "@hasna/contracts": "0.13.3",
2244
+ "@hasna/contracts": "0.13.4",
2245
2245
  "@hasna/events": "^0.1.11",
2246
2246
  "@modelcontextprotocol/sdk": "^1.12.1",
2247
2247
  chalk: "^5.4.1",
2248
2248
  commander: "^13.1.0",
2249
2249
  ink: "^5.2.0",
2250
2250
  react: "^18.3.1",
2251
- zod: "^3.24.2"
2251
+ zod: "3.25.76"
2252
2252
  },
2253
2253
  overrides: {
2254
2254
  ajv: "8.20.0",
2255
- "fast-uri": "3.1.2"
2255
+ "fast-uri": "3.1.2",
2256
+ zod: "3.25.76"
2256
2257
  },
2257
2258
  devDependencies: {
2258
2259
  "@types/bun": "^1.2.4",
@@ -2877,6 +2878,60 @@ var init_redaction = __esm(() => {
2877
2878
  REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
2878
2879
  });
2879
2880
 
2881
+ // src/lib/instant-compare.ts
2882
+ function isLeapYear(year) {
2883
+ return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
2884
+ }
2885
+ function sqliteJulianDay(value) {
2886
+ const m = SQLITE_STAMP.exec(value);
2887
+ if (!m)
2888
+ return null;
2889
+ const year = Number(m[1]);
2890
+ const month = Number(m[2]);
2891
+ const day = Number(m[3]);
2892
+ const hour = m[4] === undefined ? 0 : Number(m[4]);
2893
+ const minute = m[5] === undefined ? 0 : Number(m[5]);
2894
+ const second = m[6] === undefined ? 0 : Number(m[6]);
2895
+ const frac = m[7];
2896
+ const sign = m[8];
2897
+ const offsetHour = m[9];
2898
+ const offsetMinute = m[10];
2899
+ if (month < 1 || month > 12)
2900
+ return null;
2901
+ if (hour > 23 || minute > 59 || second > 59)
2902
+ return null;
2903
+ const maxDay = (DAYS_IN_MONTH[month - 1] ?? 0) + (month === 2 && isLeapYear(year) ? 1 : 0);
2904
+ if (day < 1 || day > maxDay)
2905
+ return null;
2906
+ let offsetMinutes = 0;
2907
+ if (sign !== undefined) {
2908
+ const oh = Number(offsetHour ?? "0");
2909
+ const om = Number(offsetMinute ?? "0");
2910
+ if (oh > 23 || om > 59)
2911
+ return null;
2912
+ offsetMinutes = oh * 60 + om;
2913
+ if (sign === "-")
2914
+ offsetMinutes = -offsetMinutes;
2915
+ }
2916
+ const micros = frac === undefined ? 0 : Number((frac + "000000").slice(0, 6));
2917
+ const ms = Date.UTC(year, month - 1, day, hour, minute, second);
2918
+ return (ms + micros / 1000) / 86400000 + 2440587.5 - offsetMinutes / 1440;
2919
+ }
2920
+ function changedSinceStampNewer(stamp, since) {
2921
+ const stampJd = sqliteJulianDay(stamp);
2922
+ if (stampJd === null)
2923
+ return true;
2924
+ const sinceJd = sqliteJulianDay(since);
2925
+ if (sinceJd === null)
2926
+ return false;
2927
+ return stampJd > sinceJd;
2928
+ }
2929
+ var SQLITE_STAMP, DAYS_IN_MONTH;
2930
+ var init_instant_compare = __esm(() => {
2931
+ SQLITE_STAMP = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(?:Z|([+-])(\d{2}):(\d{2}))?)?$/;
2932
+ DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
2933
+ });
2934
+
2880
2935
  // src/lib/plan-project-link-contract.ts
2881
2936
  import { createHash as createHash2 } from "crypto";
2882
2937
  function canonicalPlanProjectLinkJson(value) {
@@ -7387,7 +7442,7 @@ async function cloudEscalatedTasks(client, opts = {}, at = new Date) {
7387
7442
  }
7388
7443
  async function cloudChangedSince(client, since, filter = {}) {
7389
7444
  const tasks = await cloudListTasks(client, filter);
7390
- return tasks.filter((t) => (t.updated_at ?? "") > since).sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? ""));
7445
+ return tasks.filter((t) => changedSinceStampNewer(t.updated_at ?? "", since)).sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? ""));
7391
7446
  }
7392
7447
  async function cloudTaskStats(client, filter = {}) {
7393
7448
  const tasks = await cloudListTasks(client, filter);
@@ -7672,6 +7727,7 @@ var UUID_RE, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, retryCapabil
7672
7727
  var init_cloud_router = __esm(() => {
7673
7728
  init_types();
7674
7729
  init_redaction();
7730
+ init_instant_compare();
7675
7731
  init_plan_project_link_contract();
7676
7732
  init_http_client();
7677
7733
  init_adoption_validation();
@@ -14523,8 +14579,9 @@ var init_event_hooks = __esm(() => {
14523
14579
  VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
14524
14580
  });
14525
14581
 
14526
- // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
14582
+ // node_modules/.bun/@hasna+events@0.1.16/node_modules/@hasna/events/dist/index.js
14527
14583
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
14584
+ import { Buffer as Buffer2 } from "buffer";
14528
14585
  import { existsSync as existsSync6 } from "fs";
14529
14586
  import { homedir as homedir2 } from "os";
14530
14587
  import { join as join5 } from "path";
@@ -14633,11 +14690,13 @@ function getEventsDataDir(override) {
14633
14690
 
14634
14691
  class JsonEventsStore {
14635
14692
  dataDir;
14693
+ runtime;
14636
14694
  channelsPath;
14637
14695
  eventsPath;
14638
14696
  deliveriesPath;
14639
14697
  constructor(dataDir = getEventsDataDir()) {
14640
14698
  this.dataDir = dataDir;
14699
+ this.runtime = localJsonRuntime(dataDir);
14641
14700
  this.channelsPath = join5(dataDir, "channels.json");
14642
14701
  this.eventsPath = join5(dataDir, "events.json");
14643
14702
  this.deliveriesPath = join5(dataDir, "deliveries.json");
@@ -14685,13 +14744,58 @@ class JsonEventsStore {
14685
14744
  await this.writeJson(this.eventsPath, events);
14686
14745
  return event;
14687
14746
  }
14688
- async listEvents() {
14747
+ async appendEventOnce(event, options = {}) {
14748
+ await this.init();
14749
+ const events = await this.readJson(this.eventsPath, []);
14750
+ const dedupe2 = options.dedupe !== false;
14751
+ if (dedupe2) {
14752
+ const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
14753
+ if (existing) {
14754
+ return {
14755
+ event: existing,
14756
+ stored: false,
14757
+ deduped: true,
14758
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
14759
+ };
14760
+ }
14761
+ }
14762
+ events.push(event);
14763
+ await this.writeJson(this.eventsPath, events);
14764
+ return {
14765
+ event,
14766
+ stored: true,
14767
+ deduped: false,
14768
+ identity: { id: event.id, dedupeKey: event.dedupeKey }
14769
+ };
14770
+ }
14771
+ async listEvents(options = {}) {
14689
14772
  await this.init();
14690
- return this.readJson(this.eventsPath, []);
14773
+ const events = await this.readJson(this.eventsPath, []);
14774
+ return queryEvents(events, options);
14775
+ }
14776
+ async listEventsPage(options = {}) {
14777
+ await this.init();
14778
+ const events = await this.readJson(this.eventsPath, []);
14779
+ const queried = queryEvents(events, {
14780
+ eventId: options.eventId,
14781
+ source: options.source,
14782
+ type: options.type
14783
+ });
14784
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
14785
+ const limit = normalizeEventPageLimit(options.limit);
14786
+ const pageEvents = queried.slice(offset, offset + limit);
14787
+ const nextOffset = offset + pageEvents.length;
14788
+ const hasMore = nextOffset < queried.length;
14789
+ return {
14790
+ events: pageEvents,
14791
+ cursor: options.cursor,
14792
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
14793
+ hasMore
14794
+ };
14691
14795
  }
14692
14796
  async findEventByIdentity(identity) {
14693
14797
  const events = await this.listEvents();
14694
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
14798
+ return findEventByIdentity(events, identity);
14695
14799
  }
14696
14800
  async appendDelivery(result) {
14697
14801
  await this.init();
@@ -14742,6 +14846,83 @@ class JsonEventsStore {
14742
14846
  });
14743
14847
  }
14744
14848
  }
14849
+ function localJsonRuntime(dataDir = getEventsDataDir()) {
14850
+ return {
14851
+ mode: "local-files",
14852
+ name: "json-events-store",
14853
+ remote: false,
14854
+ localFiles: true,
14855
+ localSqlite: false,
14856
+ postgres: false,
14857
+ s3: false,
14858
+ aws: false,
14859
+ durable: true,
14860
+ idempotency: "best-effort-local",
14861
+ replayCursors: true,
14862
+ description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
14863
+ };
14864
+ }
14865
+ function encodeLocalJsonEventCursor(offset, options = {}) {
14866
+ if (!Number.isInteger(offset) || offset < 0)
14867
+ throw new Error(`Invalid event cursor offset: ${offset}`);
14868
+ const payload = {
14869
+ offset,
14870
+ eventId: options.eventId,
14871
+ source: options.source,
14872
+ type: options.type
14873
+ };
14874
+ return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
14875
+ }
14876
+ function decodeLocalJsonEventCursor(cursor, options = {}) {
14877
+ if (!cursor)
14878
+ return 0;
14879
+ if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
14880
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
14881
+ const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
14882
+ let payload;
14883
+ try {
14884
+ payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
14885
+ } catch {
14886
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
14887
+ }
14888
+ const offset = payload.offset;
14889
+ if (!Number.isInteger(offset) || offset < 0)
14890
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
14891
+ assertCursorFilter("eventId", payload.eventId, options.eventId);
14892
+ assertCursorFilter("source", payload.source, options.source);
14893
+ assertCursorFilter("type", payload.type, options.type);
14894
+ return offset;
14895
+ }
14896
+ function normalizeEventPageLimit(limit) {
14897
+ if (limit === undefined)
14898
+ return DEFAULT_EVENT_PAGE_LIMIT;
14899
+ if (!Number.isInteger(limit) || limit < 1)
14900
+ throw new Error(`Event page limit must be a positive integer, got ${limit}`);
14901
+ return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
14902
+ }
14903
+ function queryEvents(events, options) {
14904
+ let rows = events;
14905
+ if (options.eventId)
14906
+ rows = rows.filter((event) => event.id === options.eventId);
14907
+ if (options.source)
14908
+ rows = rows.filter((event) => event.source === options.source);
14909
+ if (options.type)
14910
+ rows = rows.filter((event) => event.type === options.type);
14911
+ if (options.cursor) {
14912
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
14913
+ rows = rows.slice(offset);
14914
+ }
14915
+ if (options.limit !== undefined)
14916
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
14917
+ return rows;
14918
+ }
14919
+ function assertCursorFilter(name, cursorValue, optionValue) {
14920
+ if (cursorValue !== optionValue)
14921
+ throw new Error(`Local JSON event cursor ${name} filter mismatch`);
14922
+ }
14923
+ function findEventByIdentity(events, identity) {
14924
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
14925
+ }
14745
14926
  function buildSignatureBase(timestamp2, body) {
14746
14927
  return `${timestamp2}.${body}`;
14747
14928
  }
@@ -14755,21 +14936,27 @@ function now2() {
14755
14936
  function truncate(value, max = 4096) {
14756
14937
  return value.length > max ? `${value.slice(0, max)}...` : value;
14757
14938
  }
14758
- function buildWebhookRequest(event, channel) {
14939
+ function buildWebhookRequest(event, channel, options = {}) {
14759
14940
  if (!channel.webhook)
14760
14941
  throw new Error(`Channel ${channel.id} has no webhook config`);
14942
+ for (const name of Object.keys(channel.webhook.headers ?? {})) {
14943
+ if (/^x-hasna-/i.test(name)) {
14944
+ throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
14945
+ }
14946
+ }
14761
14947
  const body = JSON.stringify(event);
14762
- const timestamp2 = event.time;
14948
+ const timestamp2 = options.timestamp ?? new Date().toISOString();
14763
14949
  const headers = {
14764
14950
  "Content-Type": "application/json",
14765
14951
  "User-Agent": "@hasna/events",
14766
14952
  "X-Hasna-Event-Id": event.id,
14767
14953
  "X-Hasna-Event-Type": event.type,
14768
- "X-Hasna-Timestamp": timestamp2,
14769
- ...channel.webhook.headers
14954
+ ...channel.webhook.headers,
14955
+ "X-Hasna-Timestamp": timestamp2
14770
14956
  };
14771
- if (channel.webhook.secret) {
14772
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp2, body);
14957
+ const secret = options.secret ?? channel.webhook.secret;
14958
+ if (secret) {
14959
+ headers["X-Hasna-Signature"] = signPayload(secret, timestamp2, body);
14773
14960
  }
14774
14961
  return { body, headers };
14775
14962
  }
@@ -14777,7 +14964,21 @@ async function dispatchWebhook(event, channel, options = {}) {
14777
14964
  if (!channel.webhook)
14778
14965
  throw new Error(`Channel ${channel.id} has no webhook config`);
14779
14966
  const startedAt = now2();
14780
- const { body, headers } = buildWebhookRequest(event, channel);
14967
+ let secret = channel.webhook.secret;
14968
+ if (channel.webhook.secretRef) {
14969
+ if (!options.secretResolver) {
14970
+ return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
14971
+ }
14972
+ try {
14973
+ secret = await options.secretResolver(channel.webhook.secretRef);
14974
+ } catch {
14975
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
14976
+ }
14977
+ if (!secret)
14978
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
14979
+ }
14980
+ const timestamp2 = (options.now?.() ?? new Date).toISOString();
14981
+ const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
14781
14982
  const controller = new AbortController;
14782
14983
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
14783
14984
  try {
@@ -14809,6 +15010,15 @@ async function dispatchWebhook(event, channel, options = {}) {
14809
15010
  clearTimeout(timeout);
14810
15011
  }
14811
15012
  }
15013
+ function failedAttempt(startedAt, error) {
15014
+ return {
15015
+ attempt: 1,
15016
+ status: "failed",
15017
+ startedAt,
15018
+ completedAt: now2(),
15019
+ error
15020
+ };
15021
+ }
14812
15022
  async function dispatchCommand(event, channel) {
14813
15023
  if (!channel.command)
14814
15024
  throw new Error(`Channel ${channel.id} has no command config`);
@@ -14897,6 +15107,76 @@ function createDeliveryResult(event, channel, attempts) {
14897
15107
  completedAt: attempts.at(-1)?.completedAt ?? now2()
14898
15108
  };
14899
15109
  }
15110
+
15111
+ class EventTypeCatalog {
15112
+ definitions = new Map;
15113
+ register(definition) {
15114
+ this.definitions.set(definition.type, definition);
15115
+ return this;
15116
+ }
15117
+ unregister(type) {
15118
+ return this.definitions.delete(type);
15119
+ }
15120
+ has(type) {
15121
+ return this.definitions.has(type);
15122
+ }
15123
+ get(type) {
15124
+ return this.definitions.get(type);
15125
+ }
15126
+ list() {
15127
+ return [...this.definitions.values()];
15128
+ }
15129
+ validateEvent(event) {
15130
+ const definition = this.definitions.get(event.type);
15131
+ if (!definition)
15132
+ return { ok: true };
15133
+ return definition.validate(event.data, event);
15134
+ }
15135
+ assertEventValid(event) {
15136
+ const result = this.validateEvent(event);
15137
+ if (!result.ok) {
15138
+ throw new EventValidationError(event.type, result.issues);
15139
+ }
15140
+ }
15141
+ }
15142
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
15143
+ if (paths.length === 0)
15144
+ return event;
15145
+ const copy = structuredClone(event);
15146
+ for (const path of paths) {
15147
+ setPath(copy, path, replacement);
15148
+ }
15149
+ return copy;
15150
+ }
15151
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
15152
+ return redactValue2(event, replacement);
15153
+ }
15154
+ function shouldRedactKey(key) {
15155
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
15156
+ }
15157
+ function redactValue2(value, replacement) {
15158
+ if (Array.isArray(value))
15159
+ return value.map((item) => redactValue2(item, replacement));
15160
+ if (!value || typeof value !== "object")
15161
+ return value;
15162
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
15163
+ key,
15164
+ shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
15165
+ ]));
15166
+ }
15167
+ function setPath(input, path, replacement) {
15168
+ const parts = path.split(".");
15169
+ let cursor = input;
15170
+ for (const part of parts.slice(0, -1)) {
15171
+ const next = cursor[part];
15172
+ if (!next || typeof next !== "object")
15173
+ return;
15174
+ cursor = next;
15175
+ }
15176
+ const last = parts.at(-1);
15177
+ if (last && last in cursor)
15178
+ cursor[last] = replacement;
15179
+ }
14900
15180
  function createEvent(input) {
14901
15181
  return {
14902
15182
  id: input.id ?? randomUUID22(),
@@ -14917,10 +15197,18 @@ class EventsClient {
14917
15197
  store;
14918
15198
  redactors;
14919
15199
  transportOptions;
15200
+ catalog;
15201
+ validateCatalogTypes;
14920
15202
  constructor(options = {}) {
14921
15203
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
14922
15204
  this.redactors = options.redactors ?? [];
14923
- this.transportOptions = { fetchImpl: options.fetchImpl };
15205
+ this.transportOptions = {
15206
+ fetchImpl: options.fetchImpl,
15207
+ secretResolver: options.secretResolver,
15208
+ now: options.now
15209
+ };
15210
+ this.catalog = options.catalog ?? defaultEventTypeCatalog;
15211
+ this.validateCatalogTypes = options.validateCatalogTypes ?? false;
14924
15212
  }
14925
15213
  async addChannel(input) {
14926
15214
  const timestamp2 = new Date().toISOString();
@@ -14938,18 +15226,40 @@ class EventsClient {
14938
15226
  }
14939
15227
  async emit(input, options = {}) {
14940
15228
  const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
14941
- if (options.dedupe !== false) {
14942
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
14943
- if (existing) {
14944
- return { event: existing, deliveries: [], deduped: true };
14945
- }
14946
- }
14947
- await this.store.appendEvent(event);
14948
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
14949
- return { event, deliveries, deduped: false };
14950
- }
14951
- async listEvents() {
14952
- return this.store.listEvents();
15229
+ if (options.validate ?? this.validateCatalogTypes) {
15230
+ this.catalog.assertEventValid(event);
15231
+ }
15232
+ const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
15233
+ if (append.deduped) {
15234
+ return { event: append.event, deliveries: [], deduped: true };
15235
+ }
15236
+ const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
15237
+ return { event: append.event, deliveries, deduped: false };
15238
+ }
15239
+ async listEvents(options = {}) {
15240
+ if (Object.keys(options).length === 0)
15241
+ return this.store.listEvents();
15242
+ return queryClientEvents(await this.store.listEvents(), options);
15243
+ }
15244
+ async listEventsPage(options = {}) {
15245
+ if (this.store.listEventsPage)
15246
+ return this.store.listEventsPage(options);
15247
+ const events = queryClientEvents(await this.store.listEvents(), {
15248
+ eventId: options.eventId,
15249
+ source: options.source,
15250
+ type: options.type
15251
+ });
15252
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
15253
+ const limit = normalizeEventPageLimit(options.limit);
15254
+ const pageEvents = events.slice(offset, offset + limit);
15255
+ const nextOffset = offset + pageEvents.length;
15256
+ const hasMore = nextOffset < events.length;
15257
+ return {
15258
+ events: pageEvents,
15259
+ cursor: options.cursor,
15260
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
15261
+ hasMore
15262
+ };
14953
15263
  }
14954
15264
  async listDeliveries() {
14955
15265
  return this.store.listDeliveries();
@@ -15017,22 +15327,37 @@ class EventsClient {
15017
15327
  return result;
15018
15328
  }
15019
15329
  async replay(options = {}) {
15020
- const events = (await this.store.listEvents()).filter((event) => {
15021
- if (options.eventId && event.id !== options.eventId)
15022
- return false;
15023
- if (options.source && event.source !== options.source)
15024
- return false;
15025
- if (options.type && event.type !== options.type)
15026
- return false;
15027
- return true;
15028
- });
15330
+ const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
15029
15331
  if (options.dryRun)
15030
- return { events, deliveries: [] };
15332
+ return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
15031
15333
  const deliveries = [];
15032
- for (const event of events) {
15334
+ for (const event of page.events) {
15033
15335
  deliveries.push(...await this.deliver(event));
15034
15336
  }
15035
- return { events, deliveries };
15337
+ return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
15338
+ }
15339
+ async appendEvent(event, options) {
15340
+ if (this.store.appendEventOnce) {
15341
+ return this.store.appendEventOnce(event, { dedupe: options.dedupe });
15342
+ }
15343
+ if (options.dedupe) {
15344
+ const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
15345
+ if (existing) {
15346
+ return {
15347
+ event: existing,
15348
+ stored: false,
15349
+ deduped: true,
15350
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
15351
+ };
15352
+ }
15353
+ }
15354
+ const stored = await this.store.appendEvent(event);
15355
+ return {
15356
+ event: stored,
15357
+ stored: true,
15358
+ deduped: false,
15359
+ identity: { id: stored.id, dedupeKey: stored.dedupeKey }
15360
+ };
15036
15361
  }
15037
15362
  async applyRedaction(event, channel) {
15038
15363
  let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
@@ -15059,43 +15384,19 @@ class EventsClient {
15059
15384
  return createDeliveryResult(event, channel, attempts);
15060
15385
  }
15061
15386
  }
15062
- function redactPaths(event, paths, replacement = "[REDACTED]") {
15063
- if (paths.length === 0)
15064
- return event;
15065
- const copy = structuredClone(event);
15066
- for (const path of paths) {
15067
- setPath(copy, path, replacement);
15068
- }
15069
- return copy;
15070
- }
15071
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
15072
- return redactValue2(event, replacement);
15073
- }
15074
- function shouldRedactKey(key) {
15075
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
15076
- }
15077
- function redactValue2(value, replacement) {
15078
- if (Array.isArray(value))
15079
- return value.map((item) => redactValue2(item, replacement));
15080
- if (!value || typeof value !== "object")
15081
- return value;
15082
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
15083
- key,
15084
- shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
15085
- ]));
15086
- }
15087
- function setPath(input, path, replacement) {
15088
- const parts = path.split(".");
15089
- let cursor = input;
15090
- for (const part of parts.slice(0, -1)) {
15091
- const next = cursor[part];
15092
- if (!next || typeof next !== "object")
15093
- return;
15094
- cursor = next;
15095
- }
15096
- const last = parts.at(-1);
15097
- if (last && last in cursor)
15098
- cursor[last] = replacement;
15387
+ function queryClientEvents(events, options) {
15388
+ let rows = events;
15389
+ if (options.eventId)
15390
+ rows = rows.filter((event) => event.id === options.eventId);
15391
+ if (options.source)
15392
+ rows = rows.filter((event) => event.source === options.source);
15393
+ if (options.type)
15394
+ rows = rows.filter((event) => event.type === options.type);
15395
+ if (options.cursor)
15396
+ rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
15397
+ if (options.limit !== undefined)
15398
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
15399
+ return rows;
15099
15400
  }
15100
15401
  function normalizeTime(value) {
15101
15402
  if (!value)
@@ -15109,9 +15410,22 @@ function normalizeRetryPolicy(policy) {
15109
15410
  multiplier: Math.max(1, policy?.multiplier ?? 2)
15110
15411
  };
15111
15412
  }
15112
- var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
15413
+ var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:", DEFAULT_EVENT_PAGE_LIMIT = 100, MAX_EVENT_PAGE_LIMIT = 1000, DEFAULT_SIGNATURE_TOLERANCE_MS, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES;
15113
15414
  var init_dist = __esm(() => {
15114
15415
  DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
15416
+ EventValidationError = class EventValidationError extends Error {
15417
+ eventType;
15418
+ issues;
15419
+ constructor(eventType, issues) {
15420
+ const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
15421
+ super(`Event validation failed for type "${eventType}": ${detail}`);
15422
+ this.name = "EventValidationError";
15423
+ this.eventType = eventType;
15424
+ this.issues = issues;
15425
+ }
15426
+ };
15427
+ defaultEventTypeCatalog = new EventTypeCatalog;
15428
+ APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
15115
15429
  });
15116
15430
 
15117
15431
  // src/db/task-lists.ts
@@ -16166,8 +16480,12 @@ async function deliverWebhook(wh, event, body, attempt, db) {
16166
16480
  const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
16167
16481
  headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
16168
16482
  }
16169
- const resp = await fetch(wh.url, { method: "POST", headers, body });
16483
+ const resp = await fetch(wh.url, { method: "POST", headers, body, redirect: "manual" });
16170
16484
  const respText = await resp.text().catch(() => "");
16485
+ if (resp.status >= 300 && resp.status < 400) {
16486
+ logDelivery(db, wh.id, event, body, resp.status, "Blocked: webhook URL returned a 3xx redirect; redirects are never followed (SSRF prevention)", attempt);
16487
+ return;
16488
+ }
16171
16489
  logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
16172
16490
  if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
16173
16491
  const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
@@ -16976,7 +17294,7 @@ function getTaskGraph(taskId, direction = "both", db) {
16976
17294
  const deps = getTaskDependencies(t.id, d);
16977
17295
  const hasUnfinishedDeps = deps.some((dep) => {
16978
17296
  const depTask = getTask(dep.depends_on, d);
16979
- return depTask && depTask.status !== "completed";
17297
+ return depTask && isBlockingDependencyStatus(depTask.status);
16980
17298
  });
16981
17299
  return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
16982
17300
  }
@@ -17236,7 +17554,7 @@ function getBlockingDeps(id, db) {
17236
17554
  const blocking = [];
17237
17555
  for (const dep of deps) {
17238
17556
  const task = getTask(dep.depends_on, d);
17239
- if (task && task.status !== "completed")
17557
+ if (task && isBlockingDependencyStatus(task.status))
17240
17558
  blocking.push(task);
17241
17559
  }
17242
17560
  return blocking;
@@ -17560,7 +17878,7 @@ function getNextTask(agentId, filters, db) {
17560
17878
  conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
17561
17879
  params.push(...filters.tags);
17562
17880
  }
17563
- conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')");
17881
+ conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status NOT IN ('completed', 'cancelled'))");
17564
17882
  const where = conditions.join(" AND ");
17565
17883
  let recentProjectIds = [];
17566
17884
  const assignedAliasParams = [];
@@ -17604,7 +17922,7 @@ function getActiveWork(filters, db) {
17604
17922
  }
17605
17923
  function getTasksChangedSince(since, filters, db) {
17606
17924
  const d = db || getDatabase();
17607
- const conditions = ["updated_at > ?"];
17925
+ const conditions = ["(julianday(updated_at) IS NULL OR julianday(updated_at) > julianday(?))"];
17608
17926
  const params = [since];
17609
17927
  if (filters?.project_id) {
17610
17928
  conditions.push("project_id = ?");
@@ -26031,7 +26349,7 @@ function matchesExtraFilters(task, filter) {
26031
26349
  }
26032
26350
  if (filter.tags?.length) {
26033
26351
  const taskTags = new Set(task.tags ?? []);
26034
- if (!filter.tags.every((tag) => taskTags.has(tag)))
26352
+ if (!filter.tags.some((tag) => taskTags.has(tag)))
26035
26353
  return false;
26036
26354
  }
26037
26355
  if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
@@ -32542,11 +32860,18 @@ class PostgresJsonRecordStore {
32542
32860
  return context?.requestId ?? this.sourceMachineId ?? null;
32543
32861
  }
32544
32862
  async ensureSchema() {
32545
- this.schemaReady ??= (async () => {
32546
- for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
32547
- await this.options.client.query(sql);
32548
- }
32549
- })();
32863
+ if (!this.schemaReady) {
32864
+ this.schemaReady = (async () => {
32865
+ await retryOnTransientPostgresError(async () => {
32866
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
32867
+ await this.options.client.query(sql);
32868
+ }
32869
+ });
32870
+ })().catch((error) => {
32871
+ this.schemaReady = null;
32872
+ throw error;
32873
+ });
32874
+ }
32550
32875
  await this.schemaReady;
32551
32876
  }
32552
32877
  async get(type, id) {
@@ -32955,13 +33280,14 @@ class PostgresJsonRecordStore {
32955
33280
  throw new Error(divergentAuditHistoryReplayError(value.id));
32956
33281
  }
32957
33282
  async withTaskParentIntegrityTransaction(fn) {
32958
- if (typeof this.options.client.transaction !== "function") {
33283
+ const transaction = this.options.client.transaction;
33284
+ if (typeof transaction !== "function") {
32959
33285
  throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
32960
33286
  }
32961
- return this.options.client.transaction(async (client) => {
33287
+ return retryOnTransientPostgresError(() => transaction(async (client) => {
32962
33288
  await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
32963
33289
  return fn(client);
32964
- });
33290
+ }));
32965
33291
  }
32966
33292
  async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
32967
33293
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
@@ -34308,7 +34634,7 @@ async function getActiveWork2(filters, store) {
34308
34634
  }));
34309
34635
  }
34310
34636
  async function getChangedSince(since, filters, store) {
34311
- return (await listTasks2(filters ?? {}, store)).filter((task) => task.updated_at > since);
34637
+ return (await listTasks2(filters ?? {}, store)).filter((task) => changedSinceStampNewer(task.updated_at ?? "", since));
34312
34638
  }
34313
34639
  async function createProject2(input, store, context) {
34314
34640
  const timestamp2 = new Date().toISOString();
@@ -34844,6 +35170,35 @@ function compareClock(left, right) {
34844
35170
  function numberValue2(value) {
34845
35171
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
34846
35172
  }
35173
+ function isTransientPostgresError(error) {
35174
+ if (typeof error !== "object" || error === null)
35175
+ return false;
35176
+ const candidate = error;
35177
+ const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
35178
+ if (typeof candidate.cause === "object" && candidate.cause !== null) {
35179
+ const cause = candidate.cause;
35180
+ states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
35181
+ }
35182
+ if (states.some((state) => typeof state === "string" && TRANSIENT_POSTGRES_SQLSTATES.has(state))) {
35183
+ return true;
35184
+ }
35185
+ const message = typeof candidate.message === "string" ? candidate.message : candidate.cause?.message;
35186
+ return typeof message === "string" && TRANSIENT_POSTGRES_MESSAGE_MARKERS.some((marker) => message.includes(marker));
35187
+ }
35188
+ async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
35189
+ let lastError;
35190
+ for (let attempt = 1;attempt <= attempts; attempt += 1) {
35191
+ try {
35192
+ return await fn();
35193
+ } catch (error) {
35194
+ lastError = error;
35195
+ if (!isTransientPostgresError(error) || attempt === attempts)
35196
+ throw error;
35197
+ await new Promise((resolve14) => setTimeout(resolve14, delayMs * attempt));
35198
+ }
35199
+ }
35200
+ throw lastError;
35201
+ }
34847
35202
  function isPostgresUniqueViolation(error) {
34848
35203
  if (typeof error !== "object" || error === null)
34849
35204
  return false;
@@ -34863,10 +35218,11 @@ function postgresConstraintName(error) {
34863
35218
  const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
34864
35219
  return typeof constraint === "string" ? constraint : "";
34865
35220
  }
34866
- var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC", TASK_ORDER_BY;
35221
+ var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC", TASK_ORDER_BY, TRANSIENT_POSTGRES_SQLSTATES, TRANSIENT_POSTGRES_MESSAGE_MARKERS;
34867
35222
  var init_postgres_adapter = __esm(() => {
34868
35223
  init_types();
34869
35224
  init_creator_identity();
35225
+ init_instant_compare();
34870
35226
  init_plan_project_link_contract();
34871
35227
  init_stale_lock_handoff();
34872
35228
  init_postgres_sync();
@@ -34876,6 +35232,12 @@ var init_postgres_adapter = __esm(() => {
34876
35232
  init_audit_history_import();
34877
35233
  init_canonical();
34878
35234
  TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
35235
+ TRANSIENT_POSTGRES_SQLSTATES = new Set(["55P03", "40P01", "40001"]);
35236
+ TRANSIENT_POSTGRES_MESSAGE_MARKERS = [
35237
+ "canceling statement due to lock timeout",
35238
+ "deadlock detected",
35239
+ "could not serialize access"
35240
+ ];
34879
35241
  });
34880
35242
 
34881
35243
  // src/project-registration/postgres.ts
@@ -35238,14 +35600,23 @@ class PostgresTodosProjectRegistrationBackend {
35238
35600
  this.cursorTableName = safeIdentifier(options.cursorTableName ?? "todos_sync_cursors", "cursorTableName");
35239
35601
  }
35240
35602
  async ensureSchema() {
35241
- this.schemaReady ??= (async () => {
35242
- for (const statement of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
35243
- await this.client.query(statement);
35244
- }
35245
- for (const statement of postgresTodosProjectRegistrationSchemaSql()) {
35246
- await this.client.query(statement);
35603
+ if (this.schemaReady === null) {
35604
+ const attempt = (async () => {
35605
+ for (const statement of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
35606
+ await this.client.query(statement);
35607
+ }
35608
+ for (const statement of postgresTodosProjectRegistrationSchemaSql()) {
35609
+ await this.client.query(statement);
35610
+ }
35611
+ })();
35612
+ this.schemaReady = attempt;
35613
+ try {
35614
+ await attempt;
35615
+ } catch (error) {
35616
+ this.schemaReady = null;
35617
+ throw error;
35247
35618
  }
35248
- })();
35619
+ }
35249
35620
  await this.schemaReady;
35250
35621
  }
35251
35622
  async transaction(fn) {
@@ -43754,10 +44125,19 @@ class PostgresPrGroupLedgerPersistence {
43754
44125
  this.client = client;
43755
44126
  }
43756
44127
  async ensureSchema() {
43757
- this.schemaReady ??= (async () => {
43758
- for (const statement of postgresPrGroupSchemaSql())
43759
- await this.client.query(statement);
43760
- })();
44128
+ if (this.schemaReady === null) {
44129
+ const attempt = (async () => {
44130
+ for (const statement of postgresPrGroupSchemaSql())
44131
+ await this.client.query(statement);
44132
+ })();
44133
+ this.schemaReady = attempt;
44134
+ try {
44135
+ await attempt;
44136
+ } catch (error) {
44137
+ this.schemaReady = null;
44138
+ throw error;
44139
+ }
44140
+ }
43761
44141
  return this.schemaReady;
43762
44142
  }
43763
44143
  async transaction(fn) {
@@ -49131,12 +49511,21 @@ class PostgresTodosTaskManifestBackend {
49131
49511
  this.tenantId = options.tenantId ?? "default";
49132
49512
  }
49133
49513
  async ensureSchema() {
49134
- this.schemaReady ??= (async () => {
49135
- for (const sql of postgresTodosSyncSchemaSql(this.tableName))
49136
- await this.client.query(sql);
49137
- for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
49138
- await this.client.query(sql);
49139
- })();
49514
+ if (this.schemaReady === null) {
49515
+ const attempt = (async () => {
49516
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName))
49517
+ await this.client.query(sql);
49518
+ for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
49519
+ await this.client.query(sql);
49520
+ })();
49521
+ this.schemaReady = attempt;
49522
+ try {
49523
+ await attempt;
49524
+ } catch (error) {
49525
+ this.schemaReady = null;
49526
+ throw error;
49527
+ }
49528
+ }
49140
49529
  await this.schemaReady;
49141
49530
  }
49142
49531
  async insertSync(tx, objectType2, objectId, payload, now4) {
@@ -50475,12 +50864,21 @@ class PostgresTodosTaskSubtreeTransferBackend {
50475
50864
  this.tenantId = options.tenantId ?? "default";
50476
50865
  }
50477
50866
  async ensureSchema() {
50478
- this.schemaReady ??= (async () => {
50479
- for (const sql of postgresTodosSyncSchemaSql(this.tableName))
50480
- await this.client.query(sql);
50481
- for (const sql of postgresTodosTaskSubtreeTransferSchemaSql())
50482
- await this.client.query(sql);
50483
- })();
50867
+ if (this.schemaReady === null) {
50868
+ const attempt = (async () => {
50869
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName))
50870
+ await this.client.query(sql);
50871
+ for (const sql of postgresTodosTaskSubtreeTransferSchemaSql())
50872
+ await this.client.query(sql);
50873
+ })();
50874
+ this.schemaReady = attempt;
50875
+ try {
50876
+ await attempt;
50877
+ } catch (error) {
50878
+ this.schemaReady = null;
50879
+ throw error;
50880
+ }
50881
+ }
50484
50882
  await this.schemaReady;
50485
50883
  }
50486
50884
  async snapshot(client, input, forUpdate = false) {
@@ -51909,7 +52307,10 @@ async function ensureCloudSchema() {
51909
52307
  await client.query(sql);
51910
52308
  }
51911
52309
  await getApiKeyStore().ensureSchema();
51912
- })();
52310
+ })().catch((error) => {
52311
+ schemaEnsured = null;
52312
+ throw error;
52313
+ });
51913
52314
  return schemaEnsured;
51914
52315
  }
51915
52316
  async function ensureCloudCommentCursorIndex() {
@@ -70451,7 +70852,7 @@ function scoreHealth(scope, scopeId, db) {
70451
70852
  const blockers = d.query(`SELECT dep.id, dep.short_id, dep.title, dep.status
70452
70853
  FROM task_dependencies td
70453
70854
  JOIN tasks dep ON dep.id = td.depends_on
70454
- WHERE td.task_id = ? AND dep.status != 'completed'`).all(task2.id);
70855
+ WHERE td.task_id = ? AND dep.status NOT IN ('completed', 'cancelled')`).all(task2.id);
70455
70856
  return { id: task2.id, short_id: task2.short_id, title: redactEvidenceText(task2.title), blockers };
70456
70857
  }).filter((entry2) => entry2.blockers.length > 0);
70457
70858
  const overdue = tasks.filter((task2) => activeTaskIds.has(task2.id) && Boolean(task2.due_at && task2.due_at < generatedAt)).map((task2) => ({ id: task2.id, short_id: task2.short_id, title: redactEvidenceText(task2.title), due_at: task2.due_at }));