@hasna/todos 0.15.52 → 0.16.0

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 (183) hide show
  1. package/CHANGELOG.md +1884 -0
  2. package/README.md +314 -37
  3. package/dist/ai-tools.d.ts.map +1 -1
  4. package/dist/ai.d.ts.map +1 -1
  5. package/dist/cli/cloud-router.d.ts +99 -51
  6. package/dist/cli/cloud-router.d.ts.map +1 -1
  7. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  8. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  9. package/dist/cli/commands/delegate.d.ts.map +1 -1
  10. package/dist/cli/commands/environment-snapshots.d.ts.map +1 -1
  11. package/dist/cli/commands/machines-api.d.ts +4 -0
  12. package/dist/cli/commands/machines-api.d.ts.map +1 -0
  13. package/dist/cli/commands/machines.d.ts.map +1 -1
  14. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  15. package/dist/cli/commands/plan-commands.d.ts +3 -0
  16. package/dist/cli/commands/plan-commands.d.ts.map +1 -0
  17. package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
  18. package/dist/cli/commands/pr-group-commands.d.ts.map +1 -1
  19. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  20. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  21. package/dist/cli/commands/storage-commands.d.ts.map +1 -1
  22. package/dist/cli/commands/task-list-commands.d.ts +3 -0
  23. package/dist/cli/commands/task-list-commands.d.ts.map +1 -0
  24. package/dist/cli/commands/template-commands.d.ts +3 -0
  25. package/dist/cli/commands/template-commands.d.ts.map +1 -0
  26. package/dist/cli/helpers.d.ts.map +1 -1
  27. package/dist/cli/index.js +15128 -11093
  28. package/dist/cli/machine-api.d.ts +7 -0
  29. package/dist/cli/machine-api.d.ts.map +1 -0
  30. package/dist/cli/plan-read-api.d.ts +6 -0
  31. package/dist/cli/plan-read-api.d.ts.map +1 -0
  32. package/dist/cli/project-api.d.ts +119 -0
  33. package/dist/cli/project-api.d.ts.map +1 -0
  34. package/dist/cli/stage-a.d.ts +34 -4
  35. package/dist/cli/stage-a.d.ts.map +1 -1
  36. package/dist/cli/task-query-api.d.ts +21 -0
  37. package/dist/cli/task-query-api.d.ts.map +1 -0
  38. package/dist/cli/template-api.d.ts +7 -0
  39. package/dist/cli/template-api.d.ts.map +1 -0
  40. package/dist/contracts.js +835 -265
  41. package/dist/db/builtin-templates.d.ts +2 -33
  42. package/dist/db/builtin-templates.d.ts.map +1 -1
  43. package/dist/db/database.d.ts.map +1 -1
  44. package/dist/db/handoffs.d.ts.map +1 -1
  45. package/dist/db/machines.d.ts.map +1 -1
  46. package/dist/db/migrations.d.ts.map +1 -1
  47. package/dist/db/plans.d.ts.map +1 -1
  48. package/dist/db/retrospectives.d.ts.map +1 -1
  49. package/dist/db/schema.d.ts.map +1 -1
  50. package/dist/db/task-lists.d.ts.map +1 -1
  51. package/dist/db/task-runs.d.ts +2 -0
  52. package/dist/db/task-runs.d.ts.map +1 -1
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3862 -1715
  56. package/dist/lib/assignee-validation.d.ts.map +1 -1
  57. package/dist/lib/builtin-template-library.d.ts +32 -0
  58. package/dist/lib/builtin-template-library.d.ts.map +1 -0
  59. package/dist/lib/cli-reference.d.ts.map +1 -1
  60. package/dist/lib/config.d.ts +10 -13
  61. package/dist/lib/config.d.ts.map +1 -1
  62. package/dist/lib/db-backup.d.ts +1 -1
  63. package/dist/lib/db-backup.d.ts.map +1 -1
  64. package/dist/lib/env.d.ts +18 -0
  65. package/dist/lib/env.d.ts.map +1 -0
  66. package/dist/lib/environment-snapshots.d.ts.map +1 -1
  67. package/dist/lib/gatherer.d.ts.map +1 -1
  68. package/dist/lib/headless-boundaries.d.ts +4 -3
  69. package/dist/lib/headless-boundaries.d.ts.map +1 -1
  70. package/dist/lib/local-encryption.d.ts +12 -0
  71. package/dist/lib/local-encryption.d.ts.map +1 -1
  72. package/dist/lib/local-opt-in.d.ts +90 -0
  73. package/dist/lib/local-opt-in.d.ts.map +1 -0
  74. package/dist/lib/paths.d.ts +9 -0
  75. package/dist/lib/paths.d.ts.map +1 -1
  76. package/dist/lib/plan-artifacts.d.ts +1 -0
  77. package/dist/lib/plan-artifacts.d.ts.map +1 -1
  78. package/dist/lib/plan-client-boundary.d.ts +2 -0
  79. package/dist/lib/plan-client-boundary.d.ts.map +1 -0
  80. package/dist/lib/plan-schedule.d.ts +6 -0
  81. package/dist/lib/plan-schedule.d.ts.map +1 -0
  82. package/dist/lib/project-panel.d.ts +6 -0
  83. package/dist/lib/project-panel.d.ts.map +1 -1
  84. package/dist/lib/project-receipt.d.ts +3 -0
  85. package/dist/lib/project-receipt.d.ts.map +1 -0
  86. package/dist/lib/sandbox-profiles.d.ts.map +1 -1
  87. package/dist/lib/shared-plan-artifacts.d.ts +14 -0
  88. package/dist/lib/shared-plan-artifacts.d.ts.map +1 -0
  89. package/dist/lib/sync.d.ts.map +1 -1
  90. package/dist/lib/task-list-client-boundary.d.ts +3 -0
  91. package/dist/lib/task-list-client-boundary.d.ts.map +1 -0
  92. package/dist/lib/task-routing.d.ts.map +1 -1
  93. package/dist/lib/template-client-boundary.d.ts +2 -0
  94. package/dist/lib/template-client-boundary.d.ts.map +1 -0
  95. package/dist/mcp/index.d.ts +1 -0
  96. package/dist/mcp/index.d.ts.map +1 -1
  97. package/dist/mcp/index.js +45271 -40452
  98. package/dist/mcp/plan-api.d.ts +6 -0
  99. package/dist/mcp/plan-api.d.ts.map +1 -0
  100. package/dist/mcp/remote-authority.d.ts +49 -0
  101. package/dist/mcp/remote-authority.d.ts.map +1 -0
  102. package/dist/mcp/task-coordination-api.d.ts +15 -0
  103. package/dist/mcp/task-coordination-api.d.ts.map +1 -0
  104. package/dist/mcp/task-list-api.d.ts +8 -0
  105. package/dist/mcp/task-list-api.d.ts.map +1 -0
  106. package/dist/mcp/token-utils.d.ts.map +1 -1
  107. package/dist/mcp/tools/environment-snapshots.d.ts.map +1 -1
  108. package/dist/mcp/tools/machines.d.ts.map +1 -1
  109. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  110. package/dist/mcp/tools/task-resources.d.ts.map +1 -1
  111. package/dist/mcp.d.ts +2 -2
  112. package/dist/mcp.d.ts.map +1 -1
  113. package/dist/mcp.js +44 -18
  114. package/dist/project-registration.js +2829 -556
  115. package/dist/registry.js +836 -266
  116. package/dist/release-provenance.json +6 -6
  117. package/dist/sdk/client.d.ts +51 -4
  118. package/dist/sdk/client.d.ts.map +1 -1
  119. package/dist/sdk/index.d.ts +2 -0
  120. package/dist/sdk/index.d.ts.map +1 -1
  121. package/dist/sdk/index.js +1209 -1080
  122. package/dist/sdk/resolve.d.ts +91 -0
  123. package/dist/sdk/resolve.d.ts.map +1 -0
  124. package/dist/sdk/v1.generated.d.ts +219 -3
  125. package/dist/sdk/v1.generated.d.ts.map +1 -1
  126. package/dist/server/auth-posture.d.ts +66 -7
  127. package/dist/server/auth-posture.d.ts.map +1 -1
  128. package/dist/server/client-ip.d.ts +39 -0
  129. package/dist/server/client-ip.d.ts.map +1 -0
  130. package/dist/server/cloud.d.ts +2 -1
  131. package/dist/server/cloud.d.ts.map +1 -1
  132. package/dist/server/index.d.ts +1 -1
  133. package/dist/server/index.js +20556 -15541
  134. package/dist/server/openapi.d.ts +1341 -83
  135. package/dist/server/openapi.d.ts.map +1 -1
  136. package/dist/server/port.d.ts +2 -2
  137. package/dist/server/port.d.ts.map +1 -1
  138. package/dist/server/routes.d.ts +3 -6
  139. package/dist/server/routes.d.ts.map +1 -1
  140. package/dist/server/serve.d.ts +2 -6
  141. package/dist/server/serve.d.ts.map +1 -1
  142. package/dist/server/v1.d.ts +3 -1
  143. package/dist/server/v1.d.ts.map +1 -1
  144. package/dist/storage/atomic-project-migration.d.ts +53 -0
  145. package/dist/storage/atomic-project-migration.d.ts.map +1 -0
  146. package/dist/storage/index.d.ts +3 -1
  147. package/dist/storage/index.d.ts.map +1 -1
  148. package/dist/storage/interfaces.d.ts +61 -0
  149. package/dist/storage/interfaces.d.ts.map +1 -1
  150. package/dist/storage/machine-registry.d.ts +36 -0
  151. package/dist/storage/machine-registry.d.ts.map +1 -0
  152. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  153. package/dist/storage/postgres-sync.d.ts +2 -2
  154. package/dist/storage/postgres-sync.d.ts.map +1 -1
  155. package/dist/storage/run-artifact-creation-upload.d.ts +50 -0
  156. package/dist/storage/run-artifact-creation-upload.d.ts.map +1 -0
  157. package/dist/storage/s3-artifact-sync.d.ts +18 -0
  158. package/dist/storage/s3-artifact-sync.d.ts.map +1 -1
  159. package/dist/storage/sqlite-snapshot.d.ts +2 -0
  160. package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
  161. package/dist/storage.d.ts +2 -2
  162. package/dist/storage.d.ts.map +1 -1
  163. package/dist/storage.js +2695 -322
  164. package/dist/task-manifest/postgres.d.ts +1 -1
  165. package/dist/task-manifest/sqlite.d.ts +1 -1
  166. package/dist/task-manifest.js +244 -73
  167. package/dist/task-subtree-transfer.js +198 -4
  168. package/dist/testing.d.ts +90 -29
  169. package/dist/testing.d.ts.map +1 -1
  170. package/dist/testing.js +34 -10
  171. package/dist/types/index.d.ts +40 -4
  172. package/dist/types/index.d.ts.map +1 -1
  173. package/docs/PLAN_API.md +93 -0
  174. package/docs/TASK_LIST_API.md +58 -0
  175. package/docs/TASK_QUERY_API.md +14 -0
  176. package/docs/TEMPLATE_API.md +18 -0
  177. package/docs/native-storage.md +405 -0
  178. package/package.json +18 -15
  179. package/postinstall.js +52 -1
  180. package/dashboard/dist/assets/index-BNQ4gJua.js +0 -342
  181. package/dashboard/dist/assets/index-DjzvHUWt.css +0 -1
  182. package/dashboard/dist/index.html +0 -13
  183. package/dashboard/dist/logo.jpg +0 -0
package/dist/registry.js CHANGED
@@ -39,6 +39,84 @@ var __export = (target, all) => {
39
39
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
40
40
  var __require = import.meta.require;
41
41
 
42
+ // src/lib/env.ts
43
+ var alias = (canonical, legacy) => process.env[canonical] ?? process.env[legacy], env;
44
+ var init_env = __esm(() => {
45
+ env = {
46
+ profile: () => alias("HASNA_TODOS_PROFILE", "TODOS_PROFILE"),
47
+ toolGroups: () => alias("HASNA_TODOS_TOOL_GROUPS", "TODOS_TOOL_GROUPS"),
48
+ machineId: () => alias("HASNA_TODOS_MACHINE_ID", "TODOS_MACHINE_ID"),
49
+ machineName: () => alias("HASNA_TODOS_MACHINE_NAME", "TODOS_MACHINE_NAME"),
50
+ rateLimitMax: () => alias("HASNA_TODOS_RATE_LIMIT_MAX", "TODOS_RATE_LIMIT_MAX"),
51
+ trustProxy: () => alias("HASNA_TODOS_TRUST_PROXY", "TODOS_TRUST_PROXY"),
52
+ autoProject: () => alias("HASNA_TODOS_AUTO_PROJECT", "TODOS_AUTO_PROJECT"),
53
+ aiFormat: () => alias("HASNA_TODOS_AI_FORMAT", "TODOS_AI_FORMAT"),
54
+ syncAgents: () => alias("HASNA_TODOS_SYNC_AGENTS", "TODOS_SYNC_AGENTS"),
55
+ taskListId: () => alias("HASNA_TODOS_TASK_LIST_ID", "TODOS_TASK_LIST_ID"),
56
+ claudeTaskList: () => alias("HASNA_TODOS_CLAUDE_TASK_LIST", "TODOS_CLAUDE_TASK_LIST"),
57
+ sandboxProfilesPath: () => alias("HASNA_TODOS_SANDBOX_PROFILES_PATH", "TODOS_SANDBOX_PROFILES_PATH"),
58
+ seatRosterPath: () => alias("HASNA_TODOS_SEAT_ROSTER_PATH", "TODOS_SEAT_ROSTER_PATH"),
59
+ delegateNoticeChannel: () => alias("HASNA_TODOS_DELEGATE_NOTICE_CHANNEL", "TODOS_DELEGATE_NOTICE_CHANNEL"),
60
+ delegateNotifyBin: () => alias("HASNA_TODOS_DELEGATE_NOTIFY_BIN", "TODOS_DELEGATE_NOTIFY_BIN")
61
+ };
62
+ });
63
+
64
+ // src/lib/local-opt-in.ts
65
+ import {
66
+ clientTransportEnvKeys,
67
+ credentialOverrideEnvKey,
68
+ credentialPointerEnvKey,
69
+ CREDENTIAL_PROFILE_ENV_KEY
70
+ } from "@hasna/contracts/client";
71
+ function isTodosLocalOptIn(env2 = process.env) {
72
+ return TODOS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env2[key] ?? "").trim() !== "");
73
+ }
74
+ function todosAuthorityEnvKeys() {
75
+ const keys = clientTransportEnvKeys("todos");
76
+ return [
77
+ ...keys.apiUrlKeys,
78
+ ...keys.apiKeyKeys,
79
+ credentialOverrideEnvKey("todos"),
80
+ credentialPointerEnvKey("todos"),
81
+ CREDENTIAL_PROFILE_ENV_KEY
82
+ ];
83
+ }
84
+ function hasTodosEnvAuthorityIntent(env2 = process.env) {
85
+ return todosAuthorityEnvKeys().some((key) => (env2[key] ?? "").trim() !== "");
86
+ }
87
+ function selectsTodosLocalStore(env2 = process.env) {
88
+ return !hasTodosEnvAuthorityIntent(env2) && isTodosLocalOptIn(env2);
89
+ }
90
+ function todosResolverEnv(env2) {
91
+ const blanks = todosAuthorityEnvKeys().filter((key) => (key in env2) && (env2[key] ?? "").trim() === "");
92
+ if (blanks.length === 0)
93
+ return env2;
94
+ const next = { ...env2 };
95
+ for (const key of blanks)
96
+ delete next[key];
97
+ return next;
98
+ }
99
+ function isAmbientTodosEnv(env2) {
100
+ if (typeof process !== "undefined" && env2 === process.env)
101
+ return true;
102
+ return env2[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
103
+ }
104
+ function todosResolverInputs(env2, credentials = {}) {
105
+ const normalised = todosResolverEnv(env2);
106
+ if (normalised === env2)
107
+ return { env: normalised, credentials };
108
+ const keychain = { ...credentials.keychain };
109
+ if (keychain.enabled === undefined && keychain.run === undefined) {
110
+ keychain.enabled = isAmbientTodosEnv(env2);
111
+ }
112
+ return { env: normalised, credentials: { ...credentials, keychain } };
113
+ }
114
+ var TODOS_LOCAL_OPT_IN_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
115
+ var init_local_opt_in = __esm(() => {
116
+ TODOS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_TODOS_LOCAL", "TODOS_LOCAL"];
117
+ CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
118
+ });
119
+
42
120
  // src/project-registration/schema.ts
43
121
  function sqliteTodosProjectRegistrationSchemaSql() {
44
122
  return `
@@ -1608,16 +1686,18 @@ var init_migrations = __esm(() => {
1608
1686
  SET leaf_task_id = COALESCE(leaf_task_id, (
1609
1687
  SELECT attempt.leaf_task_id
1610
1688
  FROM pr_group_attempts AS attempt
1689
+ JOIN pr_groups AS owner ON owner.id = attempt.group_id
1611
1690
  WHERE attempt.group_id = pr_groups.id
1612
- ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
1691
+ ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
1613
1692
  attempt.created_at ASC, attempt.id ASC
1614
1693
  LIMIT 1
1615
1694
  )),
1616
1695
  branch = COALESCE(branch, (
1617
1696
  SELECT attempt.branch
1618
1697
  FROM pr_group_attempts AS attempt
1698
+ JOIN pr_groups AS owner ON owner.id = attempt.group_id
1619
1699
  WHERE attempt.group_id = pr_groups.id
1620
- ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
1700
+ ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
1621
1701
  attempt.created_at ASC, attempt.id ASC
1622
1702
  LIMIT 1
1623
1703
  ));
@@ -1640,16 +1720,18 @@ var init_migrations = __esm(() => {
1640
1720
  SET pr_number = COALESCE(pr_number, (
1641
1721
  SELECT attempt.pr_number
1642
1722
  FROM pr_group_attempts AS attempt
1723
+ JOIN pr_groups AS owner ON owner.id = attempt.group_id
1643
1724
  WHERE attempt.group_id = pr_groups.id AND attempt.pr_number IS NOT NULL
1644
- ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
1725
+ ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
1645
1726
  attempt.created_at ASC, attempt.id ASC
1646
1727
  LIMIT 1
1647
1728
  )),
1648
1729
  base_sha = COALESCE(base_sha, (
1649
1730
  SELECT attempt.base_sha
1650
1731
  FROM pr_group_attempts AS attempt
1732
+ JOIN pr_groups AS owner ON owner.id = attempt.group_id
1651
1733
  WHERE attempt.group_id = pr_groups.id AND attempt.base_sha IS NOT NULL
1652
- ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
1734
+ ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
1653
1735
  attempt.created_at ASC, attempt.id ASC
1654
1736
  LIMIT 1
1655
1737
  ));
@@ -1688,7 +1770,7 @@ var init_migrations = __esm(() => {
1688
1770
  ));
1689
1771
 
1690
1772
  INSERT OR IGNORE INTO _migrations (id)
1691
- SELECT 66
1773
+ SELECT 67
1692
1774
  WHERE NOT EXISTS (
1693
1775
  SELECT 1 FROM pr_groups WHERE leaf_task_id IS NULL OR branch IS NULL
1694
1776
  )
@@ -1893,32 +1975,52 @@ var init_migrations = __esm(() => {
1893
1975
  });
1894
1976
 
1895
1977
  // src/db/schema.ts
1978
+ function runPrGroupLineageMigration(db, migration, rebuild) {
1979
+ if (db.inTransaction)
1980
+ throw new Error("PR-group migration requires its own transaction");
1981
+ const priorForeignKeys = db.query("PRAGMA foreign_keys").get().foreign_keys;
1982
+ try {
1983
+ if (rebuild)
1984
+ db.exec("PRAGMA foreign_keys = OFF");
1985
+ db.exec("BEGIN IMMEDIATE");
1986
+ for (const statement of migration.split(/;\s*(?:\n|$)/).map((value) => value.trim()).filter(Boolean)) {
1987
+ if (/^(?:PRAGMA foreign_keys\s*=|BEGIN IMMEDIATE$|COMMIT$)/i.test(statement))
1988
+ continue;
1989
+ db.run(statement);
1990
+ }
1991
+ const completed = db.query("SELECT id FROM _migrations WHERE id = ?").get(rebuild ? 68 : 67);
1992
+ if (!completed)
1993
+ throw new Error("PR-group lineage backfill is incomplete; migration rolled back");
1994
+ if (rebuild && db.query("PRAGMA foreign_key_check").all().length) {
1995
+ throw new Error("PR-group migration failed foreign-key integrity verification");
1996
+ }
1997
+ db.exec("COMMIT");
1998
+ } catch (error) {
1999
+ if (db.inTransaction)
2000
+ db.exec("ROLLBACK");
2001
+ throw error;
2002
+ } finally {
2003
+ db.exec(`PRAGMA foreign_keys = ${priorForeignKeys ? "ON" : "OFF"}`);
2004
+ }
2005
+ }
1896
2006
  function runMigrations(db) {
1897
- let strictMigrationFailure = null;
1898
- const executeMigration = (migration, index) => {
2007
+ let currentLevel = 0;
2008
+ try {
2009
+ currentLevel = db.query("SELECT MAX(id) as max_id FROM _migrations").get()?.max_id ?? 0;
2010
+ } catch {}
2011
+ for (let index = currentLevel;index < MIGRATIONS.length; index++) {
1899
2012
  try {
1900
- db.exec(migration);
2013
+ if (index === 66 || index === 67)
2014
+ runPrGroupLineageMigration(db, MIGRATIONS[index], index === 67);
2015
+ else
2016
+ db.exec(MIGRATIONS[index]);
1901
2017
  } catch (error) {
1902
- try {
2018
+ if (db.inTransaction)
1903
2019
  db.exec("ROLLBACK");
1904
- } catch {}
1905
- if (index + 1 >= 68)
1906
- strictMigrationFailure = error;
1907
- }
1908
- };
1909
- try {
1910
- const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
1911
- const currentLevel = result?.max_id ?? 0;
1912
- for (let i = currentLevel;i < MIGRATIONS.length; i++) {
1913
- executeMigration(MIGRATIONS[i], i);
1914
- }
1915
- } catch {
1916
- for (const [index, migration] of MIGRATIONS.entries()) {
1917
- executeMigration(migration, index);
2020
+ if (index + 1 >= 67)
2021
+ throw error;
1918
2022
  }
1919
2023
  }
1920
- if (strictMigrationFailure)
1921
- throw strictMigrationFailure;
1922
2024
  ensureSchema(db);
1923
2025
  }
1924
2026
  function planSlugBase(value) {
@@ -3230,16 +3332,18 @@ function ensureSchema(db) {
3230
3332
  SET leaf_task_id = COALESCE(leaf_task_id, (
3231
3333
  SELECT attempt.leaf_task_id
3232
3334
  FROM pr_group_attempts AS attempt
3335
+ JOIN pr_groups AS owner ON owner.id = attempt.group_id
3233
3336
  WHERE attempt.group_id = pr_groups.id
3234
- ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
3337
+ ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
3235
3338
  attempt.created_at ASC, attempt.id ASC
3236
3339
  LIMIT 1
3237
3340
  )),
3238
3341
  branch = COALESCE(branch, (
3239
3342
  SELECT attempt.branch
3240
3343
  FROM pr_group_attempts AS attempt
3344
+ JOIN pr_groups AS owner ON owner.id = attempt.group_id
3241
3345
  WHERE attempt.group_id = pr_groups.id
3242
- ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
3346
+ ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
3243
3347
  attempt.created_at ASC, attempt.id ASC
3244
3348
  LIMIT 1
3245
3349
  ));
@@ -3260,16 +3364,18 @@ function ensureSchema(db) {
3260
3364
  SET pr_number = COALESCE(pr_number, (
3261
3365
  SELECT attempt.pr_number
3262
3366
  FROM pr_group_attempts AS attempt
3367
+ JOIN pr_groups AS owner ON owner.id = attempt.group_id
3263
3368
  WHERE attempt.group_id = pr_groups.id AND attempt.pr_number IS NOT NULL
3264
- ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
3369
+ ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
3265
3370
  attempt.created_at ASC, attempt.id ASC
3266
3371
  LIMIT 1
3267
3372
  )),
3268
3373
  base_sha = COALESCE(base_sha, (
3269
3374
  SELECT attempt.base_sha
3270
3375
  FROM pr_group_attempts AS attempt
3376
+ JOIN pr_groups AS owner ON owner.id = attempt.group_id
3271
3377
  WHERE attempt.group_id = pr_groups.id AND attempt.base_sha IS NOT NULL
3272
- ORDER BY CASE WHEN attempt.id = pr_groups.active_attempt_id THEN 0 ELSE 1 END,
3378
+ ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
3273
3379
  attempt.created_at ASC, attempt.id ASC
3274
3380
  LIMIT 1
3275
3381
  ));
@@ -3443,7 +3549,7 @@ function extractTopology(machine) {
3443
3549
  }
3444
3550
  function getOrCreateLocalMachine(db) {
3445
3551
  const d = db || getDatabase();
3446
- const name = process.env["TODOS_MACHINE_NAME"] || osHostname();
3552
+ const name = env.machineName() || osHostname();
3447
3553
  const host = osHostname();
3448
3554
  const plat = osPlatform();
3449
3555
  const existing = d.query("SELECT * FROM machines WHERE name = ?").get(name);
@@ -3512,7 +3618,7 @@ function registerMachine(name, opts, db) {
3512
3618
  }
3513
3619
  function updateMachineHeartbeat(idOrName, opts = {}, db) {
3514
3620
  const d = db || getDatabase();
3515
- const key = idOrName || process.env["TODOS_MACHINE_NAME"] || osHostname();
3621
+ const key = idOrName || env.machineName() || osHostname();
3516
3622
  const row = d.query("SELECT * FROM machines WHERE id = ? OR name = ?").get(key, key);
3517
3623
  if (!row) {
3518
3624
  return registerMachine(key, {
@@ -3664,6 +3770,7 @@ function backfillMachineId(db, force = false) {
3664
3770
  var _machineId = null, TABLES_WITH_MACHINE_ID;
3665
3771
  var init_machines = __esm(() => {
3666
3772
  init_database();
3773
+ init_env();
3667
3774
  TABLES_WITH_MACHINE_ID = [
3668
3775
  "projects",
3669
3776
  "tasks",
@@ -3695,7 +3802,7 @@ function isBlockingDependencyStatus(status) {
3695
3802
  function isTerminalStatus(status) {
3696
3803
  return status === "completed" || status === "failed" || status === "cancelled";
3697
3804
  }
3698
- var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3805
+ var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError, InputValidationError;
3699
3806
  var init_types = __esm(() => {
3700
3807
  TASK_STATUSES = [
3701
3808
  "pending",
@@ -3710,7 +3817,7 @@ var init_types = __esm(() => {
3710
3817
  "high",
3711
3818
  "critical"
3712
3819
  ];
3713
- PLAN_STATUSES = ["active", "completed", "archived"];
3820
+ PLAN_STATUSES = ["active", "completed", "archived", "planning", "cancelled"];
3714
3821
  VersionConflictError = class VersionConflictError extends Error {
3715
3822
  taskId;
3716
3823
  expectedVersion;
@@ -3899,6 +4006,16 @@ var init_types = __esm(() => {
3899
4006
  this.name = "DispatchNotFoundError";
3900
4007
  }
3901
4008
  };
4009
+ InputValidationError = class InputValidationError extends Error {
4010
+ static code = "INVALID_INPUT";
4011
+ code = InputValidationError.code;
4012
+ suggestion;
4013
+ constructor(message, suggestion) {
4014
+ super(message);
4015
+ this.name = "InputValidationError";
4016
+ this.suggestion = suggestion;
4017
+ }
4018
+ };
3902
4019
  });
3903
4020
 
3904
4021
  // src/db/identity-mapping.ts
@@ -4075,108 +4192,100 @@ var init_identity_mapping = __esm(() => {
4075
4192
  });
4076
4193
  });
4077
4194
 
4078
- // node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
4195
+ // src/lib/paths.ts
4196
+ import { existsSync as existsSync2 } from "fs";
4079
4197
  import { homedir } from "os";
4080
- import { join } from "path";
4081
- function assertApp(app) {
4198
+ import { join, resolve as resolve2 } from "path";
4199
+ import { homedir as pathsResolverHomedir } from "os";
4200
+ import { join as pathsResolverJoin } from "path";
4201
+ function pathsResolverAssertApp(app) {
4082
4202
  if (typeof app !== "string" || app.length === 0) {
4083
4203
  throw new TypeError("paths: app must be a non-empty string");
4084
4204
  }
4085
- if (!APP_SLUG_RE.test(app)) {
4205
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
4086
4206
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
4087
4207
  }
4088
4208
  }
4089
- function envOf(options) {
4090
- return options.env ?? process.env;
4091
- }
4092
- function envValue(options, kind) {
4093
- const value = envOf(options)[KIND_ENV[kind]];
4094
- return typeof value === "string" && value.length > 0 ? value : undefined;
4095
- }
4096
- function isMacOS(platform) {
4097
- return platform === "darwin";
4209
+ function pathsResolverAssertKind(kind) {
4210
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
4211
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
4212
+ }
4098
4213
  }
4099
- function baseDir(kind, options) {
4100
- const override = envValue(options, kind);
4101
- if (override)
4214
+ function pathsResolverBaseDir(kind, options) {
4215
+ pathsResolverAssertKind(kind);
4216
+ const env2 = options.env ?? process.env;
4217
+ const override = env2[PATHS_RESOLVER_KIND_ENV[kind]];
4218
+ if (typeof override === "string" && override.length > 0)
4102
4219
  return override;
4103
- const home = options.home ?? homedir();
4220
+ const home = options.home ?? pathsResolverHomedir();
4104
4221
  const platform = options.platform ?? process.platform;
4105
- if (isMacOS(platform)) {
4222
+ if (platform === "darwin") {
4106
4223
  switch (kind) {
4107
4224
  case "config":
4108
4225
  case "data":
4109
- return join(home, "Library", "Application Support", "Hasna");
4226
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
4110
4227
  case "cache":
4111
- return join(home, "Library", "Caches", "Hasna");
4228
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
4112
4229
  case "state":
4113
- return join(home, "Library", "Logs", "Hasna");
4230
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
4114
4231
  }
4115
4232
  }
4116
4233
  switch (kind) {
4117
4234
  case "config":
4118
- return join(home, ".config", "hasna");
4235
+ return pathsResolverJoin(home, ".config", "hasna");
4119
4236
  case "data":
4120
- return join(home, ".local", "share", "hasna");
4237
+ return pathsResolverJoin(home, ".local", "share", "hasna");
4121
4238
  case "state":
4122
- return join(home, ".local", "state", "hasna");
4239
+ return pathsResolverJoin(home, ".local", "state", "hasna");
4123
4240
  case "cache":
4124
- return join(home, ".cache", "hasna");
4241
+ return pathsResolverJoin(home, ".cache", "hasna");
4125
4242
  }
4126
4243
  }
4127
- function resolvePath(kind, options) {
4128
- assertApp(options.app);
4129
- const appSegment = options.internal === true ? join("internal", options.app) : options.app;
4130
- return join(baseDir(kind, options), appSegment);
4244
+ function pathsResolverResolve(kind, options) {
4245
+ pathsResolverAssertApp(options.app);
4246
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
4247
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
4131
4248
  }
4132
4249
  function dataDir(options) {
4133
- return resolvePath("data", options);
4250
+ return pathsResolverResolve("data", options);
4134
4251
  }
4135
- var KIND_ENV, APP_SLUG_RE;
4136
- var init_dist = __esm(() => {
4137
- KIND_ENV = {
4138
- config: "HASNA_CONFIG_HOME",
4139
- data: "HASNA_DATA_HOME",
4140
- state: "HASNA_STATE_HOME",
4141
- cache: "HASNA_CACHE_HOME"
4142
- };
4143
- APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4144
- });
4145
-
4146
- // src/lib/paths.ts
4147
- import { existsSync as existsSync2 } from "fs";
4148
- import { homedir as homedir2 } from "os";
4149
- import { join as join2, resolve as resolve2 } from "path";
4150
- function effectiveHome(env = process.env) {
4151
- return env.HOME || env.USERPROFILE || homedir2();
4252
+ function effectiveHome(env2 = process.env) {
4253
+ return env2.HOME || env2.USERPROFILE || homedir();
4152
4254
  }
4153
- function legacyHomeDir(env = process.env) {
4154
- return join2(effectiveHome(env), ".hasna", "todos");
4255
+ function legacyHomeDir(env2 = process.env) {
4256
+ return join(effectiveHome(env2), ".hasna", "todos");
4155
4257
  }
4156
- function resolverHome(env = process.env) {
4157
- return dataDir({ app: "todos", home: effectiveHome(env), env });
4258
+ function resolverHome(env2 = process.env) {
4259
+ return dataDir({ app: "todos", home: effectiveHome(env2), env: env2 });
4158
4260
  }
4159
- function adoptResolverHome(resolved, env = process.env) {
4160
- const dataOverride = env.HASNA_DATA_HOME;
4261
+ function adoptResolverHome(resolved, env2 = process.env) {
4262
+ const dataOverride = env2.HASNA_DATA_HOME;
4161
4263
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
4162
4264
  return true;
4163
- return existsSync2(join2(resolved, "todos.db")) || existsSync2(join2(resolved, "config.json"));
4265
+ return existsSync2(join(resolved, "todos.db")) || existsSync2(join(resolved, "config.json"));
4164
4266
  }
4165
- function getTodosDir(env = process.env) {
4166
- const resolved = resolverHome(env);
4167
- return resolve2(adoptResolverHome(resolved, env) ? resolved : legacyHomeDir(env));
4267
+ function getTodosDir(env2 = process.env) {
4268
+ const resolved = resolverHome(env2);
4269
+ return resolve2(adoptResolverHome(resolved, env2) ? resolved : legacyHomeDir(env2));
4168
4270
  }
4271
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
4169
4272
  var init_paths = __esm(() => {
4170
- init_dist();
4273
+ PATHS_RESOLVER_KIND_ENV = {
4274
+ config: "HASNA_CONFIG_HOME",
4275
+ data: "HASNA_DATA_HOME",
4276
+ state: "HASNA_STATE_HOME",
4277
+ cache: "HASNA_CACHE_HOME"
4278
+ };
4279
+ PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4171
4280
  });
4172
4281
 
4173
4282
  // src/lib/sync-utils.ts
4174
4283
  import { existsSync as existsSync3, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
4175
4284
  import { createHash } from "crypto";
4176
- import { homedir as homedir3 } from "os";
4177
- import { join as join3 } from "path";
4285
+ import { homedir as homedir2 } from "os";
4286
+ import { join as join2 } from "path";
4178
4287
  function getHomeDir() {
4179
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir3();
4288
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
4180
4289
  }
4181
4290
  function getTodosGlobalDir() {
4182
4291
  return getTodosDir();
@@ -4202,14 +4311,14 @@ function writeJsonFile(path, data) {
4202
4311
  `);
4203
4312
  }
4204
4313
  function readHighWaterMark(dir) {
4205
- const path = join3(dir, ".highwatermark");
4314
+ const path = join2(dir, ".highwatermark");
4206
4315
  if (!existsSync3(path))
4207
4316
  return 1;
4208
4317
  const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
4209
4318
  return isNaN(val) ? 1 : val;
4210
4319
  }
4211
4320
  function writeHighWaterMark(dir, value) {
4212
- writeFileSync(join3(dir, ".highwatermark"), String(value));
4321
+ writeFileSync(join2(dir, ".highwatermark"), String(value));
4213
4322
  }
4214
4323
  function getFileMtimeMs(path) {
4215
4324
  try {
@@ -4294,23 +4403,23 @@ __export(exports_config, {
4294
4403
  CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
4295
4404
  CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
4296
4405
  });
4297
- function getCanonicalTodosRdsConfig(env = process.env) {
4406
+ function getCanonicalTodosRdsConfig(env2 = process.env) {
4298
4407
  return {
4299
- cluster: clean(env[CANONICAL_TODOS_RDS_CLUSTER_ENV]) ?? null,
4408
+ cluster: clean(env2[CANONICAL_TODOS_RDS_CLUSTER_ENV]) ?? null,
4300
4409
  database: CANONICAL_TODOS_RDS_DATABASE,
4301
- runtimeSecretPath: clean(env[CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV]) ?? null,
4410
+ runtimeSecretPath: clean(env2[CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV]) ?? null,
4302
4411
  primaryEnv: TODOS_STORAGE_ENV.databaseUrl,
4303
4412
  fallbackEnv: TODOS_STORAGE_FALLBACK_ENV.databaseUrl
4304
4413
  };
4305
4414
  }
4306
- function loadTodosStorageConfig(env = process.env) {
4307
- const mode = getTodosStorageBackend(env);
4308
- const databaseUrl = getTodosStorageDatabaseUrl(env);
4309
- const bucket = readStorageEnv(env, "s3Bucket").value;
4310
- const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
4311
- const region = readStorageEnv(env, "awsRegion").value;
4312
- const endpoint = readStorageEnv(env, "s3Endpoint").value;
4313
- const schema = readStorageEnv(env, "databaseSchema").value;
4415
+ function loadTodosStorageConfig(env2 = process.env) {
4416
+ const mode = getTodosStorageBackend(env2);
4417
+ const databaseUrl = getTodosStorageDatabaseUrl(env2);
4418
+ const bucket = readStorageEnv(env2, "s3Bucket").value;
4419
+ const prefix = readStorageEnv(env2, "s3Prefix").value ?? "todos/";
4420
+ const region = readStorageEnv(env2, "awsRegion").value;
4421
+ const endpoint = readStorageEnv(env2, "s3Endpoint").value;
4422
+ const schema = readStorageEnv(env2, "databaseSchema").value;
4314
4423
  return {
4315
4424
  service: "todos",
4316
4425
  mode,
@@ -4318,7 +4427,7 @@ function loadTodosStorageConfig(env = process.env) {
4318
4427
  database: {
4319
4428
  provider: "postgres",
4320
4429
  url: databaseUrl,
4321
- ssl: parseBoolean(readStorageEnv(env, "databaseSsl").value, true),
4430
+ ssl: parseBoolean(readStorageEnv(env2, "databaseSsl").value, true),
4322
4431
  ...schema ? { schema } : {}
4323
4432
  }
4324
4433
  } : {},
@@ -4329,17 +4438,17 @@ function loadTodosStorageConfig(env = process.env) {
4329
4438
  prefix,
4330
4439
  ...region ? { region } : {},
4331
4440
  ...endpoint ? { endpoint } : {},
4332
- forcePathStyle: parseBoolean(readStorageEnv(env, "s3ForcePathStyle").value, false)
4441
+ forcePathStyle: parseBoolean(readStorageEnv(env2, "s3ForcePathStyle").value, false)
4333
4442
  }
4334
4443
  } : {},
4335
4444
  sync: {
4336
- batchSize: parsePositiveInteger(readStorageEnv(env, "syncBatchSize").value, 500),
4337
- dryRun: parseBoolean(readStorageEnv(env, "syncDryRun").value, false)
4445
+ batchSize: parsePositiveInteger(readStorageEnv(env2, "syncBatchSize").value, 500),
4446
+ dryRun: parseBoolean(readStorageEnv(env2, "syncDryRun").value, false)
4338
4447
  }
4339
4448
  };
4340
4449
  }
4341
- function loadStorageConfig(env = process.env) {
4342
- return loadTodosStorageConfig(env);
4450
+ function loadStorageConfig(env2 = process.env) {
4451
+ return loadTodosStorageConfig(env2);
4343
4452
  }
4344
4453
  function isTodosPostgresBackend(config) {
4345
4454
  return config.mode === "postgres";
@@ -4361,51 +4470,51 @@ function parseStorageBackend(value) {
4361
4470
  return "postgres";
4362
4471
  throw new Error(`Storage backend must be sqlite or postgres`);
4363
4472
  }
4364
- function getTodosStorageBackend(env = process.env) {
4365
- if (!getTodosStorageDatabaseUrl(env))
4473
+ function getTodosStorageBackend(env2 = process.env) {
4474
+ if (!getTodosStorageDatabaseUrl(env2))
4366
4475
  return "sqlite";
4367
- if (isTodosShadowEnabled(env))
4476
+ if (isTodosShadowEnabled(env2))
4368
4477
  return "sqlite";
4369
4478
  return "postgres";
4370
4479
  }
4371
- function isTodosShadowEnabled(env = process.env) {
4372
- return parseBoolean(readStorageEnv(env, "shadow").value, false);
4480
+ function isTodosShadowEnabled(env2 = process.env) {
4481
+ return parseBoolean(readStorageEnv(env2, "shadow").value, false);
4373
4482
  }
4374
- function getTodosStorageShadowEnvName(env = process.env) {
4375
- return readStorageEnv(env, "shadow").name;
4483
+ function getTodosStorageShadowEnvName(env2 = process.env) {
4484
+ return readStorageEnv(env2, "shadow").name;
4376
4485
  }
4377
- function assertTodosShadowConfig(config, env = process.env) {
4378
- if (!isTodosShadowEnabled(env))
4486
+ function assertTodosShadowConfig(config, env2 = process.env) {
4487
+ if (!isTodosShadowEnabled(env2))
4379
4488
  return;
4380
4489
  if (config.mode !== "sqlite") {
4381
- throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
4490
+ throw new Error(`${readStorageEnv(env2, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
4382
4491
  }
4383
4492
  if (!config.database?.url) {
4384
- throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
4493
+ throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env2, "shadow").name} is enabled`);
4385
4494
  }
4386
4495
  }
4387
- function getTodosStorageDatabaseEnv(env = process.env) {
4388
- return readStorageEnv(env, "databaseUrl").name;
4496
+ function getTodosStorageDatabaseEnv(env2 = process.env) {
4497
+ return readStorageEnv(env2, "databaseUrl").name;
4389
4498
  }
4390
- function getStorageDatabaseEnv(env = process.env) {
4391
- return getTodosStorageDatabaseEnv(env);
4499
+ function getStorageDatabaseEnv(env2 = process.env) {
4500
+ return getTodosStorageDatabaseEnv(env2);
4392
4501
  }
4393
- function getTodosStorageDatabaseUrl(env = process.env) {
4394
- return readStorageEnv(env, "databaseUrl").value;
4502
+ function getTodosStorageDatabaseUrl(env2 = process.env) {
4503
+ return readStorageEnv(env2, "databaseUrl").value;
4395
4504
  }
4396
- function getStorageDatabaseUrl(env = process.env) {
4397
- return getTodosStorageDatabaseUrl(env);
4505
+ function getStorageDatabaseUrl(env2 = process.env) {
4506
+ return getTodosStorageDatabaseUrl(env2);
4398
4507
  }
4399
- function getTodosStorageEnvName(env, key) {
4400
- return readStorageEnv(env, key).name;
4508
+ function getTodosStorageEnvName(env2, key) {
4509
+ return readStorageEnv(env2, key).name;
4401
4510
  }
4402
- function readStorageEnv(env, key) {
4511
+ function readStorageEnv(env2, key) {
4403
4512
  const primaryName = TODOS_STORAGE_ENV[key];
4404
- const primaryValue = clean(env[primaryName]);
4513
+ const primaryValue = clean(env2[primaryName]);
4405
4514
  if (primaryValue !== undefined)
4406
4515
  return { name: primaryName, value: primaryValue };
4407
4516
  const fallbackName = TODOS_STORAGE_FALLBACK_ENV[key];
4408
- const fallbackValue = clean(env[fallbackName]);
4517
+ const fallbackValue = clean(env2[fallbackName]);
4409
4518
  if (fallbackValue !== undefined)
4410
4519
  return { name: fallbackName, value: fallbackValue };
4411
4520
  return { name: primaryName };
@@ -4576,7 +4685,7 @@ __export(exports_database, {
4576
4685
  });
4577
4686
  import { Database } from "bun:sqlite";
4578
4687
  import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
4579
- import { dirname, join as join4, resolve as resolve3 } from "path";
4688
+ import { dirname, join as join3, resolve as resolve3 } from "path";
4580
4689
  function isInMemoryDb(path) {
4581
4690
  return path === ":memory:" || path.startsWith("file::memory:");
4582
4691
  }
@@ -4585,7 +4694,7 @@ function findNearestProjectDb(startDir) {
4585
4694
  const stopAt = gitRoot ? resolve3(gitRoot) : resolve3(startDir);
4586
4695
  let dir = resolve3(startDir);
4587
4696
  while (true) {
4588
- const candidate = join4(dir, ".hasna", "todos", "todos.db");
4697
+ const candidate = join3(dir, ".hasna", "todos", "todos.db");
4589
4698
  if (existsSync4(candidate))
4590
4699
  return candidate;
4591
4700
  if (dir === stopAt)
@@ -4600,7 +4709,7 @@ function findNearestProjectDb(startDir) {
4600
4709
  function findGitRoot(startDir) {
4601
4710
  let dir = resolve3(startDir);
4602
4711
  while (true) {
4603
- if (existsSync4(join4(dir, ".git")))
4712
+ if (existsSync4(join3(dir, ".git")))
4604
4713
  return dir;
4605
4714
  const parent = dirname(dir);
4606
4715
  if (parent === dir)
@@ -4610,7 +4719,7 @@ function findGitRoot(startDir) {
4610
4719
  return null;
4611
4720
  }
4612
4721
  function getGlobalDbPath() {
4613
- return join4(getTodosGlobalDir(), "todos.db");
4722
+ return join3(getTodosGlobalDir(), "todos.db");
4614
4723
  }
4615
4724
  function hasExplicitProjectArg(args = process.argv.slice(2)) {
4616
4725
  return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
@@ -4648,7 +4757,7 @@ function getDbPath() {
4648
4757
  if (process.env["TODOS_DB_SCOPE"] === "project") {
4649
4758
  const gitRoot = findGitRoot(cwd);
4650
4759
  if (gitRoot && canCreateScopedProjectDb()) {
4651
- return join4(gitRoot, ".hasna", "todos", "todos.db");
4760
+ return join3(gitRoot, ".hasna", "todos", "todos.db");
4652
4761
  }
4653
4762
  }
4654
4763
  return getGlobalDbPath();
@@ -4689,6 +4798,9 @@ function maybeInstallShadowCapture(db) {
4689
4798
  }
4690
4799
  }
4691
4800
  function getDatabase(dbPath) {
4801
+ if (dbPath === undefined && !selectsTodosLocalStore()) {
4802
+ throw new Error("API_DATABASE_FALLBACK_FORBIDDEN: this operation must use the shared Todos API; implicit SQLite access is unavailable");
4803
+ }
4692
4804
  const path = dbPath || getDbPath();
4693
4805
  if (_db && _dbPath === path)
4694
4806
  return _db;
@@ -4828,6 +4940,7 @@ function lowerInClause(column, values, params) {
4828
4940
  }
4829
4941
  var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
4830
4942
  var init_database = __esm(() => {
4943
+ init_local_opt_in();
4831
4944
  init_schema();
4832
4945
  init_machines();
4833
4946
  init_identity_mapping();
@@ -4838,9 +4951,9 @@ var init_database = __esm(() => {
4838
4951
 
4839
4952
  // src/lib/config.ts
4840
4953
  import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
4841
- import { dirname as dirname2, join as join5 } from "path";
4954
+ import { dirname as dirname2, join as join4 } from "path";
4842
4955
  function getConfigPath() {
4843
- return join5(getTodosGlobalDir(), "config.json");
4956
+ return join4(getTodosGlobalDir(), "config.json");
4844
4957
  }
4845
4958
  function normalizeAgent(agent) {
4846
4959
  return agent.trim().toLowerCase();
@@ -4891,21 +5004,6 @@ function normalizeApiUrl(value) {
4891
5004
  return null;
4892
5005
  return trimmed.replace(/\/+$/, "");
4893
5006
  }
4894
- function getLocalApiConfig(env = process.env) {
4895
- const config = loadConfig();
4896
- const envApiUrl = normalizeApiUrl(env["TODOS_URL"]);
4897
- const configApiUrl = normalizeApiUrl(config.apiUrl);
4898
- const apiUrl = envApiUrl ?? configApiUrl;
4899
- const apiKey = env["TODOS_API_KEY"] || config.apiKey || null;
4900
- return {
4901
- apiUrl,
4902
- apiKey,
4903
- source: {
4904
- apiUrl: envApiUrl ? "TODOS_URL" : configApiUrl ? "config" : "none",
4905
- apiKey: env["TODOS_API_KEY"] ? "TODOS_API_KEY" : config.apiKey ? "config" : "none"
4906
- }
4907
- };
4908
- }
4909
5007
  function getSyncAgentsFromConfig() {
4910
5008
  const config = loadConfig();
4911
5009
  const agents = config.sync_agents;
@@ -5756,11 +5854,11 @@ function writeAllowed(profile, root, writePath) {
5756
5854
  return isPathInside(scopeRoot, target);
5757
5855
  });
5758
5856
  }
5759
- function redactedEnvKeys(profile, env) {
5760
- if (!env)
5857
+ function redactedEnvKeys(profile, env2) {
5858
+ if (!env2)
5761
5859
  return [];
5762
5860
  const patterns = unique2([...DEFAULT_ENV_REDACTIONS, ...profile.env_redactions]).map((item) => item.toUpperCase());
5763
- return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
5861
+ return Object.keys(env2).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
5764
5862
  }
5765
5863
  function checkWorkspacePermission(input = {}) {
5766
5864
  const status = getWorkspaceTrustStatus(input.path || process.cwd());
@@ -5898,18 +5996,18 @@ function profileByName(name, path) {
5898
5996
  const resolved = normalizePath2(path);
5899
5997
  return profiles.find((profile) => isPathInside2(profile.root, resolved)) || defaultProfile2("default", resolved);
5900
5998
  }
5901
- function redactedEnvKeys2(profile, env) {
5902
- if (!env)
5999
+ function redactedEnvKeys2(profile, env2) {
6000
+ if (!env2)
5903
6001
  return [];
5904
6002
  const patterns = unique3([...DEFAULT_ENV_REDACTIONS2, ...profile.env_redactions]).map((item) => item.toUpperCase());
5905
- return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
6003
+ return Object.keys(env2).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
5906
6004
  }
5907
- function omittedEnvKeys(profile, env) {
5908
- if (!env)
6005
+ function omittedEnvKeys(profile, env2) {
6006
+ if (!env2)
5909
6007
  return [];
5910
6008
  if (profile.env_allowlist.includes("*"))
5911
6009
  return [];
5912
- return Object.keys(env).filter((key) => !profile.env_allowlist.some((pattern) => matchesPattern2(key, pattern)));
6010
+ return Object.keys(env2).filter((key) => !profile.env_allowlist.some((pattern) => matchesPattern2(key, pattern)));
5913
6011
  }
5914
6012
  function resolveFromCwd(cwd, path) {
5915
6013
  return normalizePath2(path.startsWith("/") ? path : `${cwd}/${path}`);
@@ -6292,15 +6390,23 @@ var init_event_hooks = __esm(() => {
6292
6390
  VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
6293
6391
  });
6294
6392
 
6295
- // node_modules/.bun/@hasna+events@0.1.16/node_modules/@hasna/events/dist/index.js
6393
+ // node_modules/.bun/@hasna+events@0.1.17/node_modules/@hasna/events/dist/index.js
6296
6394
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
6297
6395
  import { Buffer as Buffer2 } from "buffer";
6396
+ import { existsSync as existsSync22 } from "fs";
6397
+ import { join as join32 } from "path";
6298
6398
  import { existsSync as existsSync6 } from "fs";
6299
- import { homedir as homedir4 } from "os";
6300
- import { join as join6 } from "path";
6399
+ import { homedir as homedir22 } from "os";
6400
+ import { join as join22, resolve as resolve8 } from "path";
6401
+ import { homedir as homedir3 } from "os";
6402
+ import { join as join5 } from "path";
6301
6403
  import { createHmac, timingSafeEqual } from "crypto";
6404
+ import { lookup as dnsLookup } from "dns/promises";
6405
+ import { isIP } from "net";
6302
6406
  import { randomUUID as randomUUID2 } from "crypto";
6303
6407
  import { spawn } from "child_process";
6408
+ import { request as nodeHttpRequest } from "http";
6409
+ import { request as nodeHttpsRequest } from "https";
6304
6410
  import { randomUUID as randomUUID22 } from "crypto";
6305
6411
  function getPathValue(input, path) {
6306
6412
  return path.split(".").reduce((value, part) => {
@@ -6397,8 +6503,93 @@ function channelMatchesEvent(channel, event) {
6397
6503
  return true;
6398
6504
  return channel.filters.some((filter) => eventMatchesFilter(event, filter));
6399
6505
  }
6506
+ function assertApp(app) {
6507
+ if (typeof app !== "string" || app.length === 0) {
6508
+ throw new TypeError("paths: app must be a non-empty string");
6509
+ }
6510
+ if (!APP_SLUG_RE.test(app)) {
6511
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
6512
+ }
6513
+ }
6514
+ function envOf(options) {
6515
+ return options.env ?? process.env;
6516
+ }
6517
+ function envValue(options, kind) {
6518
+ const value = envOf(options)[KIND_ENV[kind]];
6519
+ return typeof value === "string" && value.length > 0 ? value : undefined;
6520
+ }
6521
+ function isMacOS(platform) {
6522
+ return platform === "darwin";
6523
+ }
6524
+ function baseDir(kind, options) {
6525
+ const override = envValue(options, kind);
6526
+ if (override)
6527
+ return override;
6528
+ const home = options.home ?? homedir3();
6529
+ const platform = options.platform ?? process.platform;
6530
+ if (isMacOS(platform)) {
6531
+ switch (kind) {
6532
+ case "config":
6533
+ case "data":
6534
+ return join5(home, "Library", "Application Support", "Hasna");
6535
+ case "cache":
6536
+ return join5(home, "Library", "Caches", "Hasna");
6537
+ case "state":
6538
+ return join5(home, "Library", "Logs", "Hasna");
6539
+ }
6540
+ }
6541
+ switch (kind) {
6542
+ case "config":
6543
+ return join5(home, ".config", "hasna");
6544
+ case "data":
6545
+ return join5(home, ".local", "share", "hasna");
6546
+ case "state":
6547
+ return join5(home, ".local", "state", "hasna");
6548
+ case "cache":
6549
+ return join5(home, ".cache", "hasna");
6550
+ }
6551
+ }
6552
+ function resolvePath(kind, options) {
6553
+ assertApp(options.app);
6554
+ const appSegment = options.internal === true ? join5("internal", options.app) : options.app;
6555
+ return join5(baseDir(kind, options), appSegment);
6556
+ }
6557
+ function dataDir2(options) {
6558
+ return resolvePath("data", options);
6559
+ }
6560
+ function effectiveHome2() {
6561
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir22();
6562
+ }
6563
+ function legacyHomeDir2() {
6564
+ return join22(effectiveHome2(), ".hasna", "events");
6565
+ }
6566
+ function resolverHome2() {
6567
+ return dataDir2({ app: "events", home: effectiveHome2() || undefined });
6568
+ }
6569
+ function adoptResolverHome2(resolved, env2 = process.env) {
6570
+ const dataOverride = env2.HASNA_DATA_HOME;
6571
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
6572
+ return true;
6573
+ return existsSync6(join22(resolved, EVENTS_STORE_SENTINEL_FILE));
6574
+ }
6575
+ function exactEventsHome() {
6576
+ const dir = process.env[HASNA_EVENTS_DIR_ENV];
6577
+ if (dir && dir.trim())
6578
+ return dir.trim();
6579
+ const home = process.env[HASNA_EVENTS_HOME_ENV];
6580
+ if (home && home.trim())
6581
+ return home.trim();
6582
+ return;
6583
+ }
6584
+ function getEventsHome() {
6585
+ const exact = exactEventsHome();
6586
+ if (exact)
6587
+ return resolve8(exact);
6588
+ const resolved = resolverHome2();
6589
+ return adoptResolverHome2(resolved) ? resolve8(resolved) : resolve8(legacyHomeDir2());
6590
+ }
6400
6591
  function getEventsDataDir(override) {
6401
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join6(homedir4(), ".hasna", "events");
6592
+ return override || getEventsHome();
6402
6593
  }
6403
6594
 
6404
6595
  class JsonEventsStore {
@@ -6407,12 +6598,12 @@ class JsonEventsStore {
6407
6598
  channelsPath;
6408
6599
  eventsPath;
6409
6600
  deliveriesPath;
6410
- constructor(dataDir2 = getEventsDataDir()) {
6411
- this.dataDir = dataDir2;
6412
- this.runtime = localJsonRuntime(dataDir2);
6413
- this.channelsPath = join6(dataDir2, "channels.json");
6414
- this.eventsPath = join6(dataDir2, "events.json");
6415
- this.deliveriesPath = join6(dataDir2, "deliveries.json");
6601
+ constructor(dataDir22 = getEventsDataDir()) {
6602
+ this.dataDir = dataDir22;
6603
+ this.runtime = localJsonRuntime(dataDir22);
6604
+ this.channelsPath = join32(dataDir22, "channels.json");
6605
+ this.eventsPath = join32(dataDir22, "events.json");
6606
+ this.deliveriesPath = join32(dataDir22, "deliveries.json");
6416
6607
  }
6417
6608
  async init() {
6418
6609
  await mkdir(this.dataDir, { recursive: true, mode: 448 });
@@ -6529,7 +6720,7 @@ class JsonEventsStore {
6529
6720
  };
6530
6721
  }
6531
6722
  async ensureArrayFile(path) {
6532
- if (!existsSync6(path)) {
6723
+ if (!existsSync22(path)) {
6533
6724
  await writeFile(path, `[]
6534
6725
  `, { encoding: "utf-8", mode: 384 });
6535
6726
  }
@@ -6559,7 +6750,7 @@ class JsonEventsStore {
6559
6750
  });
6560
6751
  }
6561
6752
  }
6562
- function localJsonRuntime(dataDir2 = getEventsDataDir()) {
6753
+ function localJsonRuntime(dataDir22 = getEventsDataDir()) {
6563
6754
  return {
6564
6755
  mode: "local-files",
6565
6756
  name: "json-events-store",
@@ -6572,7 +6763,7 @@ function localJsonRuntime(dataDir2 = getEventsDataDir()) {
6572
6763
  durable: true,
6573
6764
  idempotency: "best-effort-local",
6574
6765
  replayCursors: true,
6575
- description: `Local JSON files in ${dataDir2}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
6766
+ description: `Local JSON files in ${dataDir22}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
6576
6767
  };
6577
6768
  }
6578
6769
  function encodeLocalJsonEventCursor(offset, options = {}) {
@@ -6643,6 +6834,177 @@ function signPayload(secret, timestamp2, body) {
6643
6834
  const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp2, body)).digest("hex");
6644
6835
  return `sha256=${digest}`;
6645
6836
  }
6837
+ function isPrivateAddress(address) {
6838
+ const normalized = stripZoneId(address);
6839
+ const version = isIP(normalized);
6840
+ if (version === 4) {
6841
+ const integer = ipv4ToInt(normalized);
6842
+ if (integer === undefined)
6843
+ return true;
6844
+ return IPV4_PRIVATE_RANGES.some(([low, high]) => integer >= low && integer <= high);
6845
+ }
6846
+ if (version === 6) {
6847
+ const groups = ipv6Groups(normalized);
6848
+ if (!groups)
6849
+ return true;
6850
+ for (const prefix of IPV6_SPECIAL_PREFIXES) {
6851
+ if (!ipv6MatchesPrefix(groups, prefix.groups, prefix.bits))
6852
+ continue;
6853
+ if (prefix.bits === 96 && groups[5] === 65535) {
6854
+ return isPrivateAddress(ipv4IntToString(groups[6] << 16 | groups[7]));
6855
+ }
6856
+ if (prefix.bits === 16 && groups[0] === 8194) {
6857
+ return isPrivateAddress(ipv4IntToString(groups[1] << 16 | groups[2]));
6858
+ }
6859
+ return true;
6860
+ }
6861
+ return false;
6862
+ }
6863
+ return true;
6864
+ }
6865
+ async function resolveWebhookTarget(url, policy = {}) {
6866
+ const hostname = normalizeHostname(url.hostname);
6867
+ const allowlist = (policy.allowPrivateHosts ?? []).map((entry) => normalizeHostname(entry.toLowerCase()));
6868
+ if (allowlist.includes(hostname)) {
6869
+ const version2 = isIP(hostname);
6870
+ if (version2 === 4 || version2 === 6) {
6871
+ return { hostname, addresses: [hostname] };
6872
+ }
6873
+ const lookup2 = policy.lookup ?? defaultTargetLookup;
6874
+ let resolved2;
6875
+ try {
6876
+ resolved2 = await lookup2(hostname);
6877
+ } catch {
6878
+ throw new Error(`Webhook target ${hostname} could not be resolved`);
6879
+ }
6880
+ if (!Array.isArray(resolved2) || resolved2.length === 0) {
6881
+ throw new Error(`Webhook target ${hostname} resolved to no addresses`);
6882
+ }
6883
+ const addresses = resolved2.map((entry) => normalizeHostname(entry.address));
6884
+ return { hostname, addresses };
6885
+ }
6886
+ const version = isIP(hostname);
6887
+ if (version === 4 || version === 6) {
6888
+ if (isPrivateAddress(hostname)) {
6889
+ throw new Error(`Webhook target ${hostname} is a private or special-use address`);
6890
+ }
6891
+ return { hostname, addresses: [hostname] };
6892
+ }
6893
+ const lookup = policy.lookup ?? defaultTargetLookup;
6894
+ let resolved;
6895
+ try {
6896
+ resolved = await lookup(hostname);
6897
+ } catch {
6898
+ throw new Error(`Webhook target ${hostname} could not be resolved`);
6899
+ }
6900
+ if (!Array.isArray(resolved) || resolved.length === 0) {
6901
+ throw new Error(`Webhook target ${hostname} resolved to no addresses`);
6902
+ }
6903
+ const allowed = [];
6904
+ for (const entry of resolved) {
6905
+ const address = normalizeHostname(entry.address);
6906
+ if (isPrivateAddress(address)) {
6907
+ if (allowlist.includes(address)) {
6908
+ allowed.push(address);
6909
+ continue;
6910
+ }
6911
+ throw new Error(`Webhook target ${hostname} resolves to private or special-use address ${address}`);
6912
+ }
6913
+ allowed.push(address);
6914
+ }
6915
+ if (allowed.length === 0) {
6916
+ throw new Error(`Webhook target ${hostname} resolved to no public addresses`);
6917
+ }
6918
+ return { hostname, addresses: allowed };
6919
+ }
6920
+ function normalizeMaxRedirects(value) {
6921
+ if (value === undefined)
6922
+ return DEFAULT_MAX_REDIRECTS;
6923
+ if (!Number.isInteger(value) || value < 0)
6924
+ throw new Error("webhookTargetPolicy.maxRedirects must be a non-negative integer");
6925
+ return value;
6926
+ }
6927
+ function normalizeHostname(hostname) {
6928
+ const lower = hostname.toLowerCase();
6929
+ if (lower.startsWith("[") && lower.endsWith("]"))
6930
+ return lower.slice(1, -1);
6931
+ return lower;
6932
+ }
6933
+ function stripZoneId(address) {
6934
+ const percent = address.indexOf("%");
6935
+ return percent === -1 ? address : address.slice(0, percent);
6936
+ }
6937
+ function ipv4ToInt(address) {
6938
+ const parts = address.split(".");
6939
+ if (parts.length !== 4)
6940
+ return;
6941
+ let value = 0;
6942
+ for (const part of parts) {
6943
+ if (!/^\d{1,3}$/.test(part))
6944
+ return;
6945
+ const octet = Number(part);
6946
+ if (octet > 255)
6947
+ return;
6948
+ value = value << 8 | octet;
6949
+ }
6950
+ return value >>> 0;
6951
+ }
6952
+ function ipv4IntToString(integer) {
6953
+ return [
6954
+ integer >>> 24 & 255,
6955
+ integer >>> 16 & 255,
6956
+ integer >>> 8 & 255,
6957
+ integer & 255
6958
+ ].join(".");
6959
+ }
6960
+ function ipv6Groups(address) {
6961
+ const raw = stripZoneId(address);
6962
+ const doubleColon = raw.indexOf("::");
6963
+ const headText = doubleColon === -1 ? raw : raw.slice(0, doubleColon);
6964
+ const tailText = doubleColon === -1 ? "" : raw.slice(doubleColon + 2);
6965
+ const parseGroups = (text) => {
6966
+ if (text === "")
6967
+ return [];
6968
+ const out = [];
6969
+ for (const part of text.split(":")) {
6970
+ if (part.includes(".")) {
6971
+ const v4 = ipv4ToInt(part);
6972
+ if (v4 === undefined)
6973
+ return;
6974
+ out.push(v4 >>> 16 & 65535, v4 & 65535);
6975
+ } else {
6976
+ if (!/^[0-9a-fA-F]{1,4}$/.test(part))
6977
+ return;
6978
+ out.push(parseInt(part, 16));
6979
+ }
6980
+ }
6981
+ return out;
6982
+ };
6983
+ const head = parseGroups(headText);
6984
+ if (!head)
6985
+ return;
6986
+ const tail = parseGroups(tailText);
6987
+ if (!tail)
6988
+ return;
6989
+ const total = head.length + tail.length;
6990
+ if (doubleColon === -1) {
6991
+ return total === 8 ? head : undefined;
6992
+ }
6993
+ if (total >= 8)
6994
+ return;
6995
+ return [...head, ...new Array(8 - total).fill(0), ...tail];
6996
+ }
6997
+ function ipv6MatchesPrefix(groups, prefixGroups, prefixBits) {
6998
+ let remaining = prefixBits;
6999
+ for (let index = 0;index < prefixGroups.length && remaining > 0; index += 1) {
7000
+ const take = Math.min(16, remaining);
7001
+ const mask = 65535 << 16 - take & 65535;
7002
+ if ((groups[index] & mask) !== (prefixGroups[index] & mask))
7003
+ return false;
7004
+ remaining -= take;
7005
+ }
7006
+ return true;
7007
+ }
6646
7008
  function now2() {
6647
7009
  return new Date().toISOString();
6648
7010
  }
@@ -6673,9 +7035,18 @@ function buildWebhookRequest(event, channel, options = {}) {
6673
7035
  }
6674
7036
  return { body, headers };
6675
7037
  }
7038
+ function normalizeWebhookUrl(raw) {
7039
+ const url = new URL(raw);
7040
+ if (url.username !== "" || url.password !== "") {
7041
+ url.username = "";
7042
+ url.password = "";
7043
+ }
7044
+ return url.toString();
7045
+ }
6676
7046
  async function dispatchWebhook(event, channel, options = {}) {
6677
7047
  if (!channel.webhook)
6678
7048
  throw new Error(`Channel ${channel.id} has no webhook config`);
7049
+ const webhookUrl = normalizeWebhookUrl(channel.webhook.url);
6679
7050
  const startedAt = now2();
6680
7051
  let secret = channel.webhook.secret;
6681
7052
  if (channel.webhook.secretRef) {
@@ -6692,10 +7063,14 @@ async function dispatchWebhook(event, channel, options = {}) {
6692
7063
  }
6693
7064
  const timestamp2 = (options.now?.() ?? new Date).toISOString();
6694
7065
  const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
7066
+ const validateTargets = options.webhookTargetPolicy !== undefined || options.fetchImpl === undefined;
7067
+ if (validateTargets) {
7068
+ return dispatchValidatedWebhook(event, channel, { body, headers, startedAt, options });
7069
+ }
6695
7070
  const controller = new AbortController;
6696
7071
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
6697
7072
  try {
6698
- const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
7073
+ const response = await (options.fetchImpl ?? fetch)(webhookUrl, {
6699
7074
  method: "POST",
6700
7075
  headers,
6701
7076
  body,
@@ -6723,6 +7098,130 @@ async function dispatchWebhook(event, channel, options = {}) {
6723
7098
  clearTimeout(timeout);
6724
7099
  }
6725
7100
  }
7101
+ function isRedirectStatus(status) {
7102
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
7103
+ }
7104
+ function redirectKeepsBody(status) {
7105
+ return status === 307 || status === 308;
7106
+ }
7107
+ async function pinnedNativeRequest(target, addresses, method, headers, body, signal, tls) {
7108
+ const isHttps = target.protocol === "https:";
7109
+ if (!isHttps && target.protocol !== "http:") {
7110
+ throw new Error(`Webhook target uses unsupported protocol ${target.protocol}`);
7111
+ }
7112
+ const defaultPort = isHttps ? 443 : 80;
7113
+ const port = target.port ? Number(target.port) : defaultPort;
7114
+ const requestOptions = {
7115
+ hostname: target.hostname,
7116
+ port,
7117
+ path: `${target.pathname}${target.search}`,
7118
+ method,
7119
+ headers,
7120
+ ...tls?.ca ? { ca: tls.ca } : {},
7121
+ lookup: (hostname, _options, callback) => {
7122
+ const entries = addresses.map((address) => ({
7123
+ address,
7124
+ family: address.includes(":") ? 6 : 4
7125
+ }));
7126
+ callback(null, entries);
7127
+ }
7128
+ };
7129
+ return new Promise((resolve22, reject) => {
7130
+ const request = isHttps ? nodeHttpsRequest(requestOptions, onResponse) : nodeHttpRequest(requestOptions, onResponse);
7131
+ const onAbort = () => {
7132
+ const error = new Error("The operation was aborted.");
7133
+ error.name = "AbortError";
7134
+ request.destroy(error);
7135
+ };
7136
+ if (signal.aborted)
7137
+ onAbort();
7138
+ else
7139
+ signal.addEventListener("abort", onAbort, { once: true });
7140
+ request.on("error", reject);
7141
+ if (body !== undefined)
7142
+ request.write(body);
7143
+ request.end();
7144
+ function onResponse(response) {
7145
+ const chunks = [];
7146
+ response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
7147
+ response.on("error", reject);
7148
+ response.on("end", () => {
7149
+ const headersRecord = {};
7150
+ for (const [name, value] of Object.entries(response.headers)) {
7151
+ if (typeof value === "string")
7152
+ headersRecord[name] = value;
7153
+ else if (Array.isArray(value))
7154
+ headersRecord[name] = value.join(", ");
7155
+ }
7156
+ resolve22(new Response(Buffer.concat(chunks), { status: response.statusCode ?? 200, headers: headersRecord }));
7157
+ });
7158
+ }
7159
+ });
7160
+ }
7161
+ async function dispatchValidatedWebhook(event, channel, input) {
7162
+ const { body, headers, startedAt, options } = input;
7163
+ const webhook = channel.webhook;
7164
+ if (!webhook)
7165
+ throw new Error(`Channel ${channel.id} has no webhook config`);
7166
+ const policy = options.webhookTargetPolicy ?? {};
7167
+ const maxRedirects = normalizeMaxRedirects(policy.maxRedirects);
7168
+ const controller = new AbortController;
7169
+ const timeout = setTimeout(() => controller.abort(), webhook.timeoutMs ?? 15000);
7170
+ try {
7171
+ let target = new URL(normalizeWebhookUrl(webhook.url));
7172
+ let requestHeaders = headers;
7173
+ let method = "POST";
7174
+ let requestBody = body;
7175
+ let redirectsFollowed = 0;
7176
+ for (;; ) {
7177
+ const resolved = await resolveWebhookTarget(target, policy).catch((error) => {
7178
+ throw new Error(`Webhook target rejected by SSRF guard: ${error.message}`);
7179
+ });
7180
+ const response = options.fetchImpl ? await options.fetchImpl(target, {
7181
+ method,
7182
+ headers: requestHeaders,
7183
+ body: requestBody,
7184
+ signal: controller.signal,
7185
+ redirect: "manual"
7186
+ }) : await pinnedNativeRequest(target, resolved.addresses, method, requestHeaders, requestBody, controller.signal, options.tls);
7187
+ const location = response.headers.get("location");
7188
+ if (isRedirectStatus(response.status) && location) {
7189
+ if (redirectsFollowed >= maxRedirects) {
7190
+ return failedAttempt(startedAt, `Webhook target exceeded ${maxRedirects} redirects`);
7191
+ }
7192
+ redirectsFollowed += 1;
7193
+ const next = new URL(location, target);
7194
+ target = next;
7195
+ if (!redirectKeepsBody(response.status)) {
7196
+ method = "GET";
7197
+ requestBody = undefined;
7198
+ requestHeaders = Object.fromEntries(Object.entries(requestHeaders).filter(([name]) => name.toLowerCase() !== "content-type" && name.toLowerCase() !== "content-length"));
7199
+ }
7200
+ continue;
7201
+ }
7202
+ const responseBody = truncate(await response.text());
7203
+ return {
7204
+ attempt: 1,
7205
+ status: response.ok ? "success" : "failed",
7206
+ startedAt,
7207
+ completedAt: now2(),
7208
+ responseStatus: response.status,
7209
+ responseBody,
7210
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
7211
+ };
7212
+ }
7213
+ } catch (error) {
7214
+ return {
7215
+ attempt: 1,
7216
+ status: "failed",
7217
+ startedAt,
7218
+ completedAt: now2(),
7219
+ error: error instanceof Error ? error.message : String(error)
7220
+ };
7221
+ } finally {
7222
+ clearTimeout(timeout);
7223
+ }
7224
+ }
6726
7225
  function failedAttempt(startedAt, error) {
6727
7226
  return {
6728
7227
  attempt: 1,
@@ -6737,7 +7236,7 @@ async function dispatchCommand(event, channel) {
6737
7236
  throw new Error(`Channel ${channel.id} has no command config`);
6738
7237
  const startedAt = now2();
6739
7238
  const eventJson = JSON.stringify(event);
6740
- const env = {
7239
+ const env2 = {
6741
7240
  ...process.env,
6742
7241
  ...channel.command.env,
6743
7242
  HASNA_CHANNEL_ID: channel.id,
@@ -6751,10 +7250,10 @@ async function dispatchCommand(event, channel) {
6751
7250
  HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
6752
7251
  HASNA_EVENT_JSON: eventJson
6753
7252
  };
6754
- return new Promise((resolve8) => {
7253
+ return new Promise((resolve22) => {
6755
7254
  const child = spawn(channel.command.command, channel.command.args ?? [], {
6756
7255
  cwd: channel.command.cwd,
6757
- env,
7256
+ env: env2,
6758
7257
  stdio: ["pipe", "pipe", "pipe"]
6759
7258
  });
6760
7259
  let stdout = "";
@@ -6769,7 +7268,7 @@ async function dispatchCommand(event, channel) {
6769
7268
  });
6770
7269
  child.on("error", (error) => {
6771
7270
  clearTimeout(timeout);
6772
- resolve8({
7271
+ resolve22({
6773
7272
  attempt: 1,
6774
7273
  status: "failed",
6775
7274
  startedAt,
@@ -6782,7 +7281,7 @@ async function dispatchCommand(event, channel) {
6782
7281
  child.on("close", (code, signal) => {
6783
7282
  clearTimeout(timeout);
6784
7283
  const success = code === 0;
6785
- resolve8({
7284
+ resolve22({
6786
7285
  attempt: 1,
6787
7286
  status: success ? "success" : "failed",
6788
7287
  startedAt,
@@ -6918,7 +7417,9 @@ class EventsClient {
6918
7417
  this.transportOptions = {
6919
7418
  fetchImpl: options.fetchImpl,
6920
7419
  secretResolver: options.secretResolver,
6921
- now: options.now
7420
+ now: options.now,
7421
+ tls: options.tls,
7422
+ webhookTargetPolicy: options.webhookTargetPolicy
6922
7423
  };
6923
7424
  this.catalog = options.catalog ?? defaultEventTypeCatalog;
6924
7425
  this.validateCatalogTypes = options.validateCatalogTypes ?? false;
@@ -7123,9 +7624,51 @@ function normalizeRetryPolicy(policy) {
7123
7624
  multiplier: Math.max(1, policy?.multiplier ?? 2)
7124
7625
  };
7125
7626
  }
7126
- 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;
7127
- var init_dist2 = __esm(() => {
7627
+ var KIND_ENV, APP_SLUG_RE, HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", EVENTS_STORE_SENTINEL_FILE = "events.json", LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:", DEFAULT_EVENT_PAGE_LIMIT = 100, MAX_EVENT_PAGE_LIMIT = 1000, DEFAULT_SIGNATURE_TOLERANCE_MS, DEFAULT_MAX_REDIRECTS = 5, IPV4_PRIVATE_RANGES, IPV6_SPECIAL_PREFIXES, defaultTargetLookup = async (hostname) => {
7628
+ return dnsLookup(hostname, { all: true, verbatim: false });
7629
+ }, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES;
7630
+ var init_dist = __esm(() => {
7631
+ KIND_ENV = {
7632
+ config: "HASNA_CONFIG_HOME",
7633
+ data: "HASNA_DATA_HOME",
7634
+ state: "HASNA_STATE_HOME",
7635
+ cache: "HASNA_CACHE_HOME"
7636
+ };
7637
+ APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
7128
7638
  DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
7639
+ IPV4_PRIVATE_RANGES = [
7640
+ [0, 16777215],
7641
+ [167772160, 184549375],
7642
+ [1681915904, 1686110207],
7643
+ [2130706432, 2147483647],
7644
+ [2851995648, 2852061183],
7645
+ [2886729728, 2887778303],
7646
+ [3221225472, 3221225727],
7647
+ [3221225984, 3221226239],
7648
+ [3227017984, 3227018239],
7649
+ [3232235520, 3232301055],
7650
+ [3323068416, 3323199487],
7651
+ [3325256704, 3325256959],
7652
+ [3405803776, 3405804031],
7653
+ [3758096384, 4294967295]
7654
+ ];
7655
+ IPV6_SPECIAL_PREFIXES = [
7656
+ { groups: [0, 0, 0, 0, 0, 0, 0, 0], bits: 128 },
7657
+ { groups: [0, 0, 0, 0, 0, 0, 0, 1], bits: 128 },
7658
+ { groups: [0, 0, 0, 0, 0, 65535, 0, 0], bits: 96 },
7659
+ { groups: [100, 65435, 0, 0, 0, 0, 0, 0], bits: 96 },
7660
+ { groups: [256, 0, 0, 0, 0, 0, 0, 0], bits: 64 },
7661
+ { groups: [8193, 0, 0, 0, 0, 0, 0, 0], bits: 32 },
7662
+ { groups: [8193, 2, 0, 0, 0, 0, 0, 0], bits: 48 },
7663
+ { groups: [8193, 16, 0, 0, 0, 0, 0, 0], bits: 28 },
7664
+ { groups: [8193, 3512, 0, 0, 0, 0, 0, 0], bits: 32 },
7665
+ { groups: [8194, 0, 0, 0, 0, 0, 0, 0], bits: 16 },
7666
+ { groups: [16383, 0, 0, 0, 0, 0, 0, 0], bits: 20 },
7667
+ { groups: [64512, 0, 0, 0, 0, 0, 0, 0], bits: 7 },
7668
+ { groups: [65152, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
7669
+ { groups: [65216, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
7670
+ { groups: [65280, 0, 0, 0, 0, 0, 0, 0], bits: 8 }
7671
+ ];
7129
7672
  EventValidationError = class EventValidationError extends Error {
7130
7673
  eventType;
7131
7674
  issues;
@@ -7149,6 +7692,8 @@ function rowToTaskList(row) {
7149
7692
  };
7150
7693
  }
7151
7694
  function createTaskList(input, db) {
7695
+ if (input.status !== undefined)
7696
+ throw new Error("The explicit SQLite storage cannot retain task-list status; use the shared API");
7152
7697
  const d = db || getDatabase();
7153
7698
  return d.transaction(() => {
7154
7699
  const id = uuid();
@@ -7190,6 +7735,8 @@ function listTaskLists(projectId, db) {
7190
7735
  return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
7191
7736
  }
7192
7737
  function updateTaskList(id, input, db) {
7738
+ if (input.status !== undefined)
7739
+ throw new Error("The explicit SQLite storage cannot retain task-list status; use the shared API");
7193
7740
  const d = db || getDatabase();
7194
7741
  return d.transaction(() => {
7195
7742
  const existing = getTaskList(id, d);
@@ -7547,7 +8094,7 @@ function emitSharedTaskEventQuiet(input) {
7547
8094
  }
7548
8095
  var SOURCE = "todos";
7549
8096
  var init_shared_events = __esm(() => {
7550
- init_dist2();
8097
+ init_dist();
7551
8098
  init_database();
7552
8099
  init_projects();
7553
8100
  init_task_lists();
@@ -11188,7 +11735,13 @@ function resolveCreateSlug(input, projectId, db) {
11188
11735
  }
11189
11736
  return nextPlanSlug(planSlugBase2(input.name), projectId, db);
11190
11737
  }
11738
+ function assertLegacyPlanInput(input) {
11739
+ if (input.start_date !== undefined || input.end_date !== undefined || input.status === "planning" || input.status === "cancelled") {
11740
+ throw new Error("Plan calendar dates and planning/cancelled statuses require the shared Todos API; explicit SQLite storage cannot retain them");
11741
+ }
11742
+ }
11191
11743
  function createPlan(input, db) {
11744
+ assertLegacyPlanInput(input);
11192
11745
  const d = db || getDatabase();
11193
11746
  const id = uuid();
11194
11747
  const timestamp2 = now();
@@ -11269,6 +11822,7 @@ function updatePlanStored(id, input, db) {
11269
11822
  return updated;
11270
11823
  }
11271
11824
  function updatePlan(id, input, db) {
11825
+ assertLegacyPlanInput(input);
11272
11826
  const d = db || getDatabase();
11273
11827
  return d.transaction(() => {
11274
11828
  guardPlanRowsSqlite([id], d);
@@ -11743,27 +12297,27 @@ var init_boards = __esm(() => {
11743
12297
  // src/lib/artifact-store.ts
11744
12298
  import { createHash as createHash3 } from "crypto";
11745
12299
  import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
11746
- import { basename, dirname as dirname4, join as join7, resolve as resolve8 } from "path";
12300
+ import { basename, dirname as dirname4, join as join6, resolve as resolve9 } from "path";
11747
12301
  import { tmpdir as tmpdir2 } from "os";
11748
12302
  function isInMemoryDb2(path) {
11749
12303
  return path === ":memory:" || path.startsWith("file::memory:");
11750
12304
  }
11751
12305
  function artifactStoreRoot() {
11752
12306
  if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
11753
- return resolve8(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
12307
+ return resolve9(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
11754
12308
  if (process.env["TODOS_ARTIFACTS_DIR"])
11755
- return resolve8(process.env["TODOS_ARTIFACTS_DIR"]);
12309
+ return resolve9(process.env["TODOS_ARTIFACTS_DIR"]);
11756
12310
  const dbPath = getDatabasePath();
11757
12311
  if (isInMemoryDb2(dbPath))
11758
- return join7(tmpdir2(), "hasna-todos-artifacts");
11759
- return join7(dirname4(resolve8(dbPath)), "artifacts");
12312
+ return join6(tmpdir2(), "hasna-todos-artifacts");
12313
+ return join6(dirname4(resolve9(dbPath)), "artifacts");
11760
12314
  }
11761
12315
  function artifactStorePath(relativePath) {
11762
12316
  const normalized = relativePath.replace(/\\/g, "/");
11763
12317
  if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
11764
12318
  throw new Error("Invalid artifact store path");
11765
12319
  }
11766
- return join7(artifactStoreRoot(), normalized);
12320
+ return join6(artifactStoreRoot(), normalized);
11767
12321
  }
11768
12322
  function sha256(buffer) {
11769
12323
  return createHash3("sha256").update(buffer).digest("hex");
@@ -11803,7 +12357,7 @@ function mediaTypeFor(path, textLike) {
11803
12357
  return "application/octet-stream";
11804
12358
  }
11805
12359
  function storeArtifactContent(input) {
11806
- const sourcePath = resolve8(input.path);
12360
+ const sourcePath = resolve9(input.path);
11807
12361
  if (!existsSync8(sourcePath))
11808
12362
  return null;
11809
12363
  const sourceStat = statSync2(sourcePath);
@@ -11821,7 +12375,7 @@ function storeArtifactContent(input) {
11821
12375
  redactionStatus = "redacted";
11822
12376
  }
11823
12377
  const storedSha = sha256(storedBuffer);
11824
- const relativePath = join7("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
12378
+ const relativePath = join6("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
11825
12379
  const destination = artifactStorePath(relativePath);
11826
12380
  if (!existsSync8(destination)) {
11827
12381
  mkdirSync4(dirname4(destination), { recursive: true });
@@ -11958,19 +12512,19 @@ function importStoredArtifactContent(content) {
11958
12512
  }
11959
12513
  function getArtifactStoreRoot(dbPath) {
11960
12514
  if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
11961
- return resolve8(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
12515
+ return resolve9(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
11962
12516
  if (process.env["TODOS_ARTIFACTS_DIR"])
11963
- return resolve8(process.env["TODOS_ARTIFACTS_DIR"]);
12517
+ return resolve9(process.env["TODOS_ARTIFACTS_DIR"]);
11964
12518
  const path = dbPath ?? getDatabasePath();
11965
12519
  if (isInMemoryDb2(path))
11966
- return join7(tmpdir2(), "hasna-todos-artifacts");
11967
- return join7(dirname4(resolve8(path)), "artifacts");
12520
+ return join6(tmpdir2(), "hasna-todos-artifacts");
12521
+ return join6(dirname4(resolve9(path)), "artifacts");
11968
12522
  }
11969
12523
  function computeContentHash(path) {
11970
- return sha256(readFileSync4(resolve8(path)));
12524
+ return sha256(readFileSync4(resolve9(path)));
11971
12525
  }
11972
12526
  function storeArtifactFile(input) {
11973
- const sourcePath = resolve8(input.sourcePath);
12527
+ const sourcePath = resolve9(input.sourcePath);
11974
12528
  if (!existsSync8(sourcePath)) {
11975
12529
  throw new Error(`Source file not found: ${input.sourcePath}`);
11976
12530
  }
@@ -11984,7 +12538,7 @@ function storeArtifactFile(input) {
11984
12538
  let localPath = sourcePath;
11985
12539
  if (storageMode === "copy") {
11986
12540
  const fileName = input.name && input.name.trim().length > 0 ? basename(input.name) : basename(sourcePath);
11987
- const destination = join7(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
12541
+ const destination = join6(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
11988
12542
  mkdirSync4(dirname4(destination), { recursive: true });
11989
12543
  writeFileSync2(destination, buffer);
11990
12544
  localPath = destination;
@@ -12019,7 +12573,7 @@ function buildArtifactExportManifest(artifacts, dbPath) {
12019
12573
  };
12020
12574
  }
12021
12575
  function writeArtifactExportManifest(manifest, outputPath) {
12022
- const destination = resolve8(outputPath);
12576
+ const destination = resolve9(outputPath);
12023
12577
  mkdirSync4(dirname4(destination), { recursive: true });
12024
12578
  writeFileSync2(destination, `${JSON.stringify(manifest, null, 2)}
12025
12579
  `);
@@ -12707,7 +13261,9 @@ function finishTaskRunTransaction(input, db) {
12707
13261
  const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
12708
13262
  const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
12709
13263
  if (!run) {
12710
- throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
13264
+ if (input.run_id)
13265
+ throw new InputValidationError(`Run not found: ${input.run_id}`, "Pass an existing run id, or --key.");
13266
+ throw new InputValidationError("runs finish requires a run id or --key", "Pass run_id, or key with task_id.");
12711
13267
  }
12712
13268
  if (input.task_id && run.task_id !== input.task_id) {
12713
13269
  throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
@@ -13176,7 +13732,7 @@ var init_tasks = __esm(() => {
13176
13732
  // package.json
13177
13733
  var package_default = {
13178
13734
  name: "@hasna/todos",
13179
- version: "0.15.52",
13735
+ version: "0.16.0",
13180
13736
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
13181
13737
  type: "module",
13182
13738
  main: "dist/index.js",
@@ -13229,37 +13785,40 @@ var package_default = {
13229
13785
  }
13230
13786
  },
13231
13787
  workspaces: [
13232
- "dashboard",
13233
13788
  "ai"
13234
13789
  ],
13235
13790
  files: [
13236
13791
  "dist",
13237
- "dashboard/dist",
13238
13792
  "postinstall.js",
13239
13793
  "LICENSE",
13240
- "README.md"
13794
+ "README.md",
13795
+ "CHANGELOG.md",
13796
+ "docs/PLAN_API.md",
13797
+ "docs/TASK_LIST_API.md",
13798
+ "docs/TEMPLATE_API.md",
13799
+ "docs/TASK_QUERY_API.md",
13800
+ "docs/native-storage.md"
13241
13801
  ],
13242
13802
  scripts: {
13243
- 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 && 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 src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
13803
+ build: "cd ai && bun install --frozen-lockfile && cd .. && 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 && 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 src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
13244
13804
  "build:js": "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 && 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 src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
13245
13805
  "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 && 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 src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
13246
13806
  migrate: "bun run src/server/index.ts migrate",
13247
13807
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
13248
13808
  "generate:sdk": "bun run scripts/generate-sdk.ts",
13249
- "build:dashboard": "cd dashboard && bun install --frozen-lockfile && bun run build",
13250
13809
  typecheck: "tsc --noEmit -p tsconfig.typecheck.json",
13251
- test: "bun test",
13252
- "test:no-cloud": "bun test src/no-cloud-boundary.test.ts src/local-first.test.ts src/lib/public-release-gate.test.ts",
13810
+ test: "bun test --timeout 120000",
13811
+ "test:no-cloud": "bun test --timeout 120000 src/no-cloud-boundary.test.ts src/local-first.test.ts src/lib/public-release-gate.test.ts",
13253
13812
  "dev:cli": "bun run src/cli/index.tsx",
13254
13813
  "dev:mcp": "bun run src/mcp/index.ts",
13255
13814
  "dev:serve": "bun run src/server/index.ts",
13256
13815
  "verify:release": "bun run scripts/verify-public-release.ts --mode=review",
13257
13816
  "verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
13258
13817
  "verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
13259
- "test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
13818
+ "test:attested-container-candidate": "bun test --timeout 120000 scripts/attested-container-candidate.test.ts",
13260
13819
  "emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
13261
13820
  "verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
13262
- "test:attested-container-compatibility-vector": "bun test scripts/attested-container-compatibility-vector.test.ts",
13821
+ "test:attested-container-compatibility-vector": "bun test --timeout 120000 scripts/attested-container-compatibility-vector.test.ts",
13263
13822
  "issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
13264
13823
  prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
13265
13824
  postinstall: "node postinstall.js"
@@ -13283,11 +13842,12 @@ var package_default = {
13283
13842
  },
13284
13843
  repository: {
13285
13844
  type: "git",
13286
- url: "https://github.com/hasna/todos.git"
13845
+ url: "https://github.com/hasna/apps.git",
13846
+ directory: "apps/todos"
13287
13847
  },
13288
- homepage: "https://github.com/hasna/todos",
13848
+ homepage: "https://github.com/hasna/apps",
13289
13849
  bugs: {
13290
- url: "https://github.com/hasna/todos/issues"
13850
+ url: "https://github.com/hasna/apps/issues"
13291
13851
  },
13292
13852
  engines: {
13293
13853
  bun: ">=1.0.0"
@@ -13296,9 +13856,8 @@ var package_default = {
13296
13856
  author: "Andrei Hasna <andrei@hasna.com>",
13297
13857
  license: "Apache-2.0",
13298
13858
  dependencies: {
13299
- "@hasna/contracts": "0.14.2",
13859
+ "@hasna/contracts": "1.0.2",
13300
13860
  "@hasna/events": "^0.1.11",
13301
- "@hasna/paths": "0.1.0",
13302
13861
  "@modelcontextprotocol/sdk": "^1.12.1",
13303
13862
  chalk: "^5.4.1",
13304
13863
  commander: "^13.1.0",
@@ -13327,6 +13886,7 @@ function getPackageVersion(_fromUrl = import.meta.url) {
13327
13886
  }
13328
13887
 
13329
13888
  // src/mcp/token-utils.ts
13889
+ init_env();
13330
13890
  var CORE_MCP_TOOLS = new Set([
13331
13891
  "add_comment",
13332
13892
  "bootstrap",
@@ -13728,7 +14288,7 @@ function addGroupTools(toolNames, groupName) {
13728
14288
  toolNames.add(tool);
13729
14289
  return true;
13730
14290
  }
13731
- function shouldRegisterToolForProfile(name, profileValue = process.env["TODOS_PROFILE"], groupValue = process.env["TODOS_TOOL_GROUPS"]) {
14291
+ function shouldRegisterToolForProfile(name, profileValue = env.profile(), groupValue = env.toolGroups()) {
13732
14292
  const profileTokens = splitTokens(profileValue || "minimal");
13733
14293
  const groupTokens = splitTokens(groupValue);
13734
14294
  if (profileTokens.includes("full") || profileTokens.includes("all"))
@@ -13800,8 +14360,8 @@ function createMcpManifest(options = {}) {
13800
14360
  name: "todos",
13801
14361
  binary: "todos-mcp",
13802
14362
  transport: "stdio",
13803
- profileEnvironmentVariable: "TODOS_PROFILE",
13804
- groupEnvironmentVariable: "TODOS_TOOL_GROUPS"
14363
+ profileEnvironmentVariable: "HASNA_TODOS_PROFILE",
14364
+ groupEnvironmentVariable: "HASNA_TODOS_TOOL_GROUPS"
13805
14365
  },
13806
14366
  groups: MCP_TOOL_GROUPS,
13807
14367
  profiles: MCP_PROFILE_GROUPS,
@@ -14781,7 +15341,7 @@ var TODOS_JSON_CONTRACTS = [
14781
15341
  contract({
14782
15342
  id: "tester_issue_report",
14783
15343
  name: "Tester Issue Report",
14784
- description: "Generic issue report payload emitted by @hasna/testers or compatible test runners for conversion into local tasks.",
15344
+ description: "Generic issue report payload emitted by the testers QA CLI or compatible test runners for conversion into local tasks.",
14785
15345
  surfaces: ["cli", "sdk"],
14786
15346
  stability: "stable",
14787
15347
  required: {
@@ -16206,14 +16766,15 @@ function normalizeApprovalRefs(values) {
16206
16766
  function resolveTodosAiCommandOptions(input) {
16207
16767
  const cli = input.cli ?? {};
16208
16768
  const config = input.config ?? {};
16209
- const env = input.env ?? {};
16210
- const format = parseEnum(selectedString(cli.format, env["TODOS_AI_FORMAT"], config.format), TODOS_AI_FORMATS, "format", TODOS_AI_DEFAULTS.format);
16211
- const configuredWriteMode = parseEnum(selectedString(cli.writeMode, env["TODOS_AI_WRITE_MODE"], config.write_mode), TODOS_AI_WRITE_MODES, "write mode", TODOS_AI_DEFAULTS.write_mode);
16769
+ const env2 = input.env ?? {};
16770
+ const envFormat = env2["HASNA_TODOS_AI_FORMAT"] ?? env2["TODOS_AI_FORMAT"];
16771
+ const format = parseEnum(selectedString(cli.format, envFormat, config.format), TODOS_AI_FORMATS, "format", TODOS_AI_DEFAULTS.format);
16772
+ const configuredWriteMode = parseEnum(selectedString(cli.writeMode, env2["TODOS_AI_WRITE_MODE"], config.write_mode), TODOS_AI_WRITE_MODES, "write mode", TODOS_AI_DEFAULTS.write_mode);
16212
16773
  if (cli.dryRun && cli.writeMode === "execute") {
16213
16774
  throw new TodosAiContractError("invalid_configuration", "--dry-run cannot be combined with --write-mode execute");
16214
16775
  }
16215
16776
  const writeMode = cli.dryRun ? "plan" : configuredWriteMode;
16216
- const explicitApproval = cli.dryRun ? selectedString(cli.approvalMode) : selectedString(cli.approvalMode, env["TODOS_AI_APPROVAL_MODE"], config.approval_mode);
16777
+ const explicitApproval = cli.dryRun ? selectedString(cli.approvalMode) : selectedString(cli.approvalMode, env2["TODOS_AI_APPROVAL_MODE"], config.approval_mode);
16217
16778
  const defaultApproval = writeMode === "execute" ? input.interactive ? "prompt" : "required" : "deny";
16218
16779
  const approvalMode = cli.dryRun ? parseEnum(explicitApproval, TODOS_AI_APPROVAL_MODES, "approval mode", "deny") : parseEnum(explicitApproval, TODOS_AI_APPROVAL_MODES, "approval mode", defaultApproval);
16219
16780
  const approvalRefs = normalizeApprovalRefs(cli.approvalRefs);
@@ -16236,12 +16797,12 @@ function resolveTodosAiCommandOptions(input) {
16236
16797
  throw new TodosAiContractError("invalid_configuration", "--approval references require --approval-mode existing");
16237
16798
  }
16238
16799
  return {
16239
- provider: selectedString(cli.provider, env["TODOS_AI_PROVIDER"], config.provider),
16240
- model: selectedString(cli.model, env["TODOS_AI_MODEL"], config.model),
16241
- profile: selectedString(cli.profile, env["TODOS_AI_PROFILE"], config.profile),
16800
+ provider: selectedString(cli.provider, env2["TODOS_AI_PROVIDER"], config.provider),
16801
+ model: selectedString(cli.model, env2["TODOS_AI_MODEL"], config.model),
16802
+ profile: selectedString(cli.profile, env2["TODOS_AI_PROFILE"], config.profile),
16242
16803
  format,
16243
- max_steps: parseBoundedInteger(cli.maxSteps ?? env["TODOS_AI_MAX_STEPS"] ?? config.max_steps, "max steps", TODOS_AI_DEFAULTS.max_steps, TODOS_AI_LIMITS.min_steps, TODOS_AI_LIMITS.max_steps),
16244
- timeout_ms: parseBoundedInteger(cli.timeoutMs ?? env["TODOS_AI_TIMEOUT_MS"] ?? config.timeout_ms, "timeout", TODOS_AI_DEFAULTS.timeout_ms, TODOS_AI_LIMITS.min_timeout_ms, TODOS_AI_LIMITS.max_timeout_ms),
16804
+ max_steps: parseBoundedInteger(cli.maxSteps ?? env2["TODOS_AI_MAX_STEPS"] ?? config.max_steps, "max steps", TODOS_AI_DEFAULTS.max_steps, TODOS_AI_LIMITS.min_steps, TODOS_AI_LIMITS.max_steps),
16805
+ timeout_ms: parseBoundedInteger(cli.timeoutMs ?? env2["TODOS_AI_TIMEOUT_MS"] ?? config.timeout_ms, "timeout", TODOS_AI_DEFAULTS.timeout_ms, TODOS_AI_LIMITS.min_timeout_ms, TODOS_AI_LIMITS.max_timeout_ms),
16245
16806
  write_mode: writeMode,
16246
16807
  approval_mode: approvalMode,
16247
16808
  approval_refs: approvalRefs,
@@ -16539,7 +17100,7 @@ function todosAiExitCodeForResult(result) {
16539
17100
  }
16540
17101
  // src/lib/onboarding-fixtures.ts
16541
17102
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
16542
- import { join as join8 } from "path";
17103
+ import { join as join7 } from "path";
16543
17104
 
16544
17105
  // src/lib/local-bridge.ts
16545
17106
  init_database();
@@ -17476,7 +18037,7 @@ function writeOnboardingFixtureFiles(directory) {
17476
18037
  mkdirSync5(directory, { recursive: true });
17477
18038
  const files = [];
17478
18039
  for (const fixture of allFixtures()) {
17479
- const path = join8(directory, `${fixture.summary.name}.bridge.json`);
18040
+ const path = join7(directory, `${fixture.summary.name}.bridge.json`);
17480
18041
  writeFileSync3(path, `${JSON.stringify(fixture.bundle, null, 2)}
17481
18042
  `, "utf-8");
17482
18043
  files.push(path);
@@ -17494,7 +18055,7 @@ function importOnboardingFixture(options = {}) {
17494
18055
  init_database();
17495
18056
  import { createHash as createHash4 } from "crypto";
17496
18057
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
17497
- import { dirname as dirname5, resolve as resolve9 } from "path";
18058
+ import { dirname as dirname5, resolve as resolve10 } from "path";
17498
18059
  import { mkdirSync as mkdirSync6 } from "fs";
17499
18060
  var TODOS_LOCAL_BACKUP_KIND = "hasna.todos.local-backup";
17500
18061
  var TODOS_LOCAL_BACKUP_SCHEMA_VERSION = 1;
@@ -17601,14 +18162,14 @@ function createLocalBackup(options = {}, db) {
17601
18162
  return backup;
17602
18163
  }
17603
18164
  function writeLocalBackupFile(backup, outputPath) {
17604
- const path = resolve9(outputPath);
18165
+ const path = resolve10(outputPath);
17605
18166
  mkdirSync6(dirname5(path), { recursive: true });
17606
18167
  writeFileSync4(path, `${JSON.stringify(backup, null, 2)}
17607
18168
  `);
17608
18169
  return path;
17609
18170
  }
17610
18171
  function readLocalBackupFile(path) {
17611
- return JSON.parse(readFileSync5(resolve9(path), "utf-8"));
18172
+ return JSON.parse(readFileSync5(resolve10(path), "utf-8"));
17612
18173
  }
17613
18174
  function verifyLocalBackup(value, options = {}, db) {
17614
18175
  const verifiedAt = options.verified_at ?? now();
@@ -18302,7 +18863,7 @@ function renderLocalSnapshotMarkdown(snapshot) {
18302
18863
  }
18303
18864
  // src/lib/sdk-integration-fixtures.ts
18304
18865
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
18305
- import { join as join9 } from "path";
18866
+ import { join as join8 } from "path";
18306
18867
 
18307
18868
  // src/cli-mcp-parity.ts
18308
18869
  function source4(version) {
@@ -20273,7 +20834,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
20273
20834
  ];
20274
20835
  const written = [];
20275
20836
  for (const [name, payload] of files) {
20276
- const file = join9(directory, name);
20837
+ const file = join8(directory, name);
20277
20838
  writeFileSync5(file, `${JSON.stringify(payload, null, 2)}
20278
20839
  `, "utf-8");
20279
20840
  written.push(file);
@@ -21556,11 +22117,11 @@ function renderLocalAuditLedgerMarkdown(ledger) {
21556
22117
  init_migrations();
21557
22118
  init_schema();
21558
22119
  import { readFileSync as readFileSync6 } from "fs";
21559
- import { join as join10, resolve as resolve10 } from "path";
22120
+ import { join as join9, resolve as resolve11 } from "path";
21560
22121
  import { Database as Database2 } from "bun:sqlite";
21561
22122
  var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1;
21562
22123
  var EXPECTED_PACKAGE_NAME = "@hasna/todos";
21563
- var EXPECTED_REPOSITORY = "https://github.com/hasna/todos.git";
22124
+ var EXPECTED_REPOSITORY = "https://github.com/hasna/apps.git";
21564
22125
  var EXPECTED_BINS = ["todos", "todos-mcp", "todos-serve"];
21565
22126
  var EXPECTED_EXPORTS = [".", "./sdk", "./mcp", "./registry", "./contracts", "./storage"];
21566
22127
  var REQUIRED_SCRIPTS = ["build", "test:no-cloud", "verify:release", "prepublishOnly"];
@@ -21602,7 +22163,7 @@ function warn(id, message, details) {
21602
22163
  return { id, status: "warning", message, details };
21603
22164
  }
21604
22165
  function readPackageJson(root) {
21605
- return JSON.parse(readFileSync6(join10(root, "package.json"), "utf8"));
22166
+ return JSON.parse(readFileSync6(join9(root, "package.json"), "utf8"));
21606
22167
  }
21607
22168
  function sortedKeys(value) {
21608
22169
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -21664,7 +22225,7 @@ function checkPackage(packageJson) {
21664
22225
  const checks = [];
21665
22226
  checks.push(packageJson.name === EXPECTED_PACKAGE_NAME ? pass("package-name", "Package name is @hasna/todos.") : fail("package-name", "Package name must be @hasna/todos.", { actual: packageJson.name ?? null }));
21666
22227
  checks.push(packageJson.publishConfig?.access === "public" ? pass("publish-access", "Package publish access is public.") : fail("publish-access", "Package publish access must be public.", { actual: packageJson.publishConfig?.access ?? null }));
21667
- checks.push(packageJson.repository?.url === EXPECTED_REPOSITORY ? pass("repository", "Repository points at hasna/todos.") : fail("repository", "Repository must point at hasna/todos.", { actual: packageJson.repository?.url ?? null }));
22228
+ checks.push(packageJson.repository?.url === EXPECTED_REPOSITORY ? pass("repository", "Repository points at hasna/apps.") : fail("repository", "Repository must point at hasna/apps.", { actual: packageJson.repository?.url ?? null }));
21668
22229
  const dependencyNames = Object.keys(packageJson.dependencies ?? {});
21669
22230
  const privateStudio = `hasna${"studio"}`;
21670
22231
  const privatePlatform = `platform${"-"}todos`;
@@ -21698,7 +22259,7 @@ function checkChangelog() {
21698
22259
  ];
21699
22260
  }
21700
22261
  function createReleaseCompatibilityReport(options = {}) {
21701
- const root = resolve10(options.root ?? process.cwd());
22262
+ const root = resolve11(options.root ?? process.cwd());
21702
22263
  const packageJson = readPackageJson(root);
21703
22264
  const simulatedLevels = options.simulated_levels ?? defaultSimulationLevels();
21704
22265
  const checks = [
@@ -24028,22 +24589,22 @@ function millisBetween(start, end) {
24028
24589
  return 0;
24029
24590
  return endMs - startMs;
24030
24591
  }
24031
- function addTaskScope(where, params, options, db, alias = "t") {
24592
+ function addTaskScope(where, params, options, db, alias2 = "t") {
24032
24593
  if (options.project_id) {
24033
- where.push(`${alias}.project_id = ?`);
24594
+ where.push(`${alias2}.project_id = ?`);
24034
24595
  params.push(options.project_id);
24035
24596
  }
24036
24597
  if (options.agent_id) {
24037
24598
  params.push(options.agent_id);
24038
- const assignedInClause = lowerInClause(`${alias}.assigned_to`, resolveAssignedToAliases(db, options.agent_id), params);
24039
- where.push(`(${alias}.agent_id = ? OR ${assignedInClause})`);
24599
+ const assignedInClause = lowerInClause(`${alias2}.assigned_to`, resolveAssignedToAliases(db, options.agent_id), params);
24600
+ where.push(`(${alias2}.agent_id = ? OR ${assignedInClause})`);
24040
24601
  }
24041
24602
  if (options.since) {
24042
- where.push(`${alias}.created_at >= ?`);
24603
+ where.push(`${alias2}.created_at >= ?`);
24043
24604
  params.push(options.since);
24044
24605
  }
24045
24606
  if (options.until) {
24046
- where.push(`${alias}.created_at <= ?`);
24607
+ where.push(`${alias2}.created_at <= ?`);
24047
24608
  params.push(options.until);
24048
24609
  }
24049
24610
  }
@@ -24712,16 +25273,25 @@ var DEFAULT_ENCRYPTION_KEY_ENV = "TODOS_ENCRYPTION_KEY";
24712
25273
  class EncryptionKeyUnavailableError extends Error {
24713
25274
  keyEnv;
24714
25275
  profile;
25276
+ static code = "ENCRYPTION_KEY_UNAVAILABLE";
25277
+ code = EncryptionKeyUnavailableError.code;
25278
+ suggestion;
24715
25279
  constructor(keyEnv, profile) {
24716
25280
  super(`Encryption key is locked: set ${keyEnv} to use profile ${profile}`);
24717
25281
  this.keyEnv = keyEnv;
24718
25282
  this.profile = profile;
25283
+ this.name = "EncryptionKeyUnavailableError";
25284
+ this.suggestion = `Set ${keyEnv} in the environment of the process serving this store, then retry.`;
24719
25285
  }
24720
25286
  }
24721
25287
 
24722
25288
  class EncryptedPayloadError extends Error {
25289
+ static code = "ENCRYPTED_PAYLOAD_INVALID";
25290
+ code = EncryptedPayloadError.code;
25291
+ static suggestion = "Pass a value produced by encrypt_local_value for this profile; a plain value is not an encrypted envelope.";
24723
25292
  constructor(message) {
24724
25293
  super(message);
25294
+ this.name = "EncryptedPayloadError";
24725
25295
  }
24726
25296
  }
24727
25297
  function now3() {
@@ -24793,17 +25363,17 @@ function removeEncryptionProfile(name) {
24793
25363
  saveConfig({ ...config, encryption_profiles: next });
24794
25364
  return true;
24795
25365
  }
24796
- function encryptionProfileStatus(name = DEFAULT_ENCRYPTION_PROFILE, env = process.env) {
25366
+ function encryptionProfileStatus(name = DEFAULT_ENCRYPTION_PROFILE, env2 = process.env) {
24797
25367
  const profile = ensureEncryptionProfile(name);
24798
25368
  return {
24799
25369
  profile: redactValue(profile),
24800
- locked: !env[profile.key_env],
25370
+ locked: !env2[profile.key_env],
24801
25371
  key_env: profile.key_env,
24802
- key_present: Boolean(env[profile.key_env])
25372
+ key_present: Boolean(env2[profile.key_env])
24803
25373
  };
24804
25374
  }
24805
- function keyForProfile(profile, env) {
24806
- const secret = env[profile.key_env];
25375
+ function keyForProfile(profile, env2) {
25376
+ const secret = env2[profile.key_env];
24807
25377
  if (!secret)
24808
25378
  throw new EncryptionKeyUnavailableError(profile.key_env, profile.name);
24809
25379
  return deriveKey(secret, profile.salt);
@@ -24834,7 +25404,7 @@ function isEncryptedValue(value) {
24834
25404
  const record = value && typeof value === "object" && !Array.isArray(value) ? value : null;
24835
25405
  return Boolean(record && record.kind === TODOS_ENCRYPTED_VALUE_KIND && record.schemaVersion === TODOS_ENCRYPTION_SCHEMA_VERSION);
24836
25406
  }
24837
- function decryptString(envelope, env = process.env) {
25407
+ function decryptString(envelope, env2 = process.env) {
24838
25408
  if (!isEncryptedValue(envelope))
24839
25409
  throw new EncryptedPayloadError("value is not a hasna/todos encrypted envelope");
24840
25410
  const profile = profileFromConfig(envelope.profile) ?? {
@@ -24844,7 +25414,7 @@ function decryptString(envelope, env = process.env) {
24844
25414
  key_env: envelope.key_env,
24845
25415
  salt: envelope.salt
24846
25416
  };
24847
- const key = keyForProfile(profile, env);
25417
+ const key = keyForProfile(profile, env2);
24848
25418
  const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(envelope.iv, "base64"));
24849
25419
  decipher.setAuthTag(Buffer.from(envelope.auth_tag, "base64"));
24850
25420
  const plaintext = Buffer.concat([
@@ -24861,8 +25431,8 @@ function decryptString(envelope, env = process.env) {
24861
25431
  function encryptValue(value, options = {}) {
24862
25432
  return encryptString(JSON.stringify(value), options);
24863
25433
  }
24864
- function decryptValue(envelope, env = process.env) {
24865
- return JSON.parse(decryptString(envelope, env));
25434
+ function decryptValue(envelope, env2 = process.env) {
25435
+ return JSON.parse(decryptString(envelope, env2));
24866
25436
  }
24867
25437
  function looksSensitiveKey(key) {
24868
25438
  return /api[_-]?key|token|secret|password|credential|evidence|artifact|output|summary|metadata/i.test(key);
@@ -24920,7 +25490,7 @@ function isEncryptedBridgeBundle(value) {
24920
25490
  const record = value && typeof value === "object" && !Array.isArray(value) ? value : null;
24921
25491
  return Boolean(record && record.kind === TODOS_ENCRYPTED_BRIDGE_KIND && record.schemaVersion === TODOS_ENCRYPTION_SCHEMA_VERSION);
24922
25492
  }
24923
- function decryptBridgeBundle(bundle, env = process.env) {
25493
+ function decryptBridgeBundle(bundle, env2 = process.env) {
24924
25494
  if (!isEncryptedBridgeBundle(bundle))
24925
25495
  throw new EncryptedPayloadError("bundle is not an encrypted hasna/todos bridge export");
24926
25496
  const plaintext = decryptString({
@@ -24936,7 +25506,7 @@ function decryptBridgeBundle(bundle, env = process.env) {
24936
25506
  auth_tag: bundle.encryption.auth_tag,
24937
25507
  ciphertext: bundle.encryption.ciphertext,
24938
25508
  plaintext_sha256: bundle.plaintext.sha256
24939
- }, env);
25509
+ }, env2);
24940
25510
  return JSON.parse(plaintext);
24941
25511
  }
24942
25512
  function assertExportProfileAllowed(profile) {
@@ -25433,7 +26003,7 @@ function buildCapabilities(version) {
25433
26003
  id: "server.local-api",
25434
26004
  kind: "server",
25435
26005
  name: "todos-serve",
25436
- description: "Start the local HTTP server for task APIs and dashboard support.",
26006
+ description: "Start the local HTTP server for task APIs.",
25437
26007
  tags: ["server", "api", "local"],
25438
26008
  docsPath: "README.md#server",
25439
26009
  stability: "experimental",