@hasna/todos 0.15.52 → 0.17.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 (206) hide show
  1. package/CHANGELOG.md +1993 -0
  2. package/README.md +356 -42
  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 +162 -72
  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 +22072 -15949
  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 +25 -5
  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/cli/template-remote.d.ts +90 -0
  41. package/dist/cli/template-remote.d.ts.map +1 -0
  42. package/dist/cli-mcp-parity.d.ts.map +1 -1
  43. package/dist/contracts.js +908 -291
  44. package/dist/db/agents.d.ts +8 -0
  45. package/dist/db/agents.d.ts.map +1 -1
  46. package/dist/db/builtin-templates.d.ts +2 -33
  47. package/dist/db/builtin-templates.d.ts.map +1 -1
  48. package/dist/db/database.d.ts +6 -0
  49. package/dist/db/database.d.ts.map +1 -1
  50. package/dist/db/handoffs.d.ts.map +1 -1
  51. package/dist/db/identity-mapping.d.ts +25 -0
  52. package/dist/db/identity-mapping.d.ts.map +1 -1
  53. package/dist/db/machines.d.ts.map +1 -1
  54. package/dist/db/migrations.d.ts.map +1 -1
  55. package/dist/db/plans.d.ts.map +1 -1
  56. package/dist/db/retrospectives.d.ts.map +1 -1
  57. package/dist/db/schema.d.ts.map +1 -1
  58. package/dist/db/task-crud.d.ts.map +1 -1
  59. package/dist/db/task-lists.d.ts.map +1 -1
  60. package/dist/db/task-runs.d.ts +2 -0
  61. package/dist/db/task-runs.d.ts.map +1 -1
  62. package/dist/index.d.ts +2 -2
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +4341 -1756
  65. package/dist/lib/assignee-validation.d.ts.map +1 -1
  66. package/dist/lib/builtin-template-library.d.ts +32 -0
  67. package/dist/lib/builtin-template-library.d.ts.map +1 -0
  68. package/dist/lib/cli-reference.d.ts.map +1 -1
  69. package/dist/lib/config.d.ts +10 -13
  70. package/dist/lib/config.d.ts.map +1 -1
  71. package/dist/lib/db-backup.d.ts +1 -1
  72. package/dist/lib/db-backup.d.ts.map +1 -1
  73. package/dist/lib/env.d.ts +18 -0
  74. package/dist/lib/env.d.ts.map +1 -0
  75. package/dist/lib/environment-snapshots.d.ts.map +1 -1
  76. package/dist/lib/feature-manifest.d.ts.map +1 -1
  77. package/dist/lib/gatherer.d.ts.map +1 -1
  78. package/dist/lib/headless-boundaries.d.ts +4 -3
  79. package/dist/lib/headless-boundaries.d.ts.map +1 -1
  80. package/dist/lib/local-encryption.d.ts +12 -0
  81. package/dist/lib/local-encryption.d.ts.map +1 -1
  82. package/dist/lib/local-opt-in.d.ts +104 -0
  83. package/dist/lib/local-opt-in.d.ts.map +1 -0
  84. package/dist/lib/paths.d.ts +20 -1
  85. package/dist/lib/paths.d.ts.map +1 -1
  86. package/dist/lib/plan-artifacts.d.ts +1 -0
  87. package/dist/lib/plan-artifacts.d.ts.map +1 -1
  88. package/dist/lib/plan-client-boundary.d.ts +2 -0
  89. package/dist/lib/plan-client-boundary.d.ts.map +1 -0
  90. package/dist/lib/plan-schedule.d.ts +6 -0
  91. package/dist/lib/plan-schedule.d.ts.map +1 -0
  92. package/dist/lib/project-panel.d.ts +6 -0
  93. package/dist/lib/project-panel.d.ts.map +1 -1
  94. package/dist/lib/project-receipt.d.ts +3 -0
  95. package/dist/lib/project-receipt.d.ts.map +1 -0
  96. package/dist/lib/sandbox-profiles.d.ts.map +1 -1
  97. package/dist/lib/shared-plan-artifacts.d.ts +14 -0
  98. package/dist/lib/shared-plan-artifacts.d.ts.map +1 -0
  99. package/dist/lib/sync.d.ts.map +1 -1
  100. package/dist/lib/task-list-client-boundary.d.ts +3 -0
  101. package/dist/lib/task-list-client-boundary.d.ts.map +1 -0
  102. package/dist/lib/task-routing.d.ts.map +1 -1
  103. package/dist/lib/template-client-boundary.d.ts +2 -0
  104. package/dist/lib/template-client-boundary.d.ts.map +1 -0
  105. package/dist/mcp/index.d.ts +53 -0
  106. package/dist/mcp/index.d.ts.map +1 -1
  107. package/dist/mcp/index.js +50124 -60838
  108. package/dist/mcp/plan-api.d.ts +6 -0
  109. package/dist/mcp/plan-api.d.ts.map +1 -0
  110. package/dist/mcp/remote-authority.d.ts +49 -0
  111. package/dist/mcp/remote-authority.d.ts.map +1 -0
  112. package/dist/mcp/task-coordination-api.d.ts +15 -0
  113. package/dist/mcp/task-coordination-api.d.ts.map +1 -0
  114. package/dist/mcp/task-list-api.d.ts +8 -0
  115. package/dist/mcp/task-list-api.d.ts.map +1 -0
  116. package/dist/mcp/token-utils.d.ts.map +1 -1
  117. package/dist/mcp/tools/agents.d.ts.map +1 -1
  118. package/dist/mcp/tools/environment-snapshots.d.ts.map +1 -1
  119. package/dist/mcp/tools/machines.d.ts.map +1 -1
  120. package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
  121. package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
  122. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  123. package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
  124. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  125. package/dist/mcp/tools/task-resources.d.ts.map +1 -1
  126. package/dist/mcp/tools/templates.d.ts.map +1 -1
  127. package/dist/mcp.d.ts +2 -2
  128. package/dist/mcp.d.ts.map +1 -1
  129. package/dist/mcp.js +45 -19
  130. package/dist/project-registration.js +3349 -645
  131. package/dist/registry.js +909 -292
  132. package/dist/release-provenance.json +6 -6
  133. package/dist/sdk/client.d.ts +53 -4
  134. package/dist/sdk/client.d.ts.map +1 -1
  135. package/dist/sdk/index.d.ts +2 -0
  136. package/dist/sdk/index.d.ts.map +1 -1
  137. package/dist/sdk/index.js +1253 -1088
  138. package/dist/sdk/resolve.d.ts +101 -0
  139. package/dist/sdk/resolve.d.ts.map +1 -0
  140. package/dist/sdk/types.d.ts +17 -2
  141. package/dist/sdk/types.d.ts.map +1 -1
  142. package/dist/sdk/v1.generated.d.ts +334 -3
  143. package/dist/sdk/v1.generated.d.ts.map +1 -1
  144. package/dist/server/auth-posture.d.ts +66 -7
  145. package/dist/server/auth-posture.d.ts.map +1 -1
  146. package/dist/server/client-ip.d.ts +39 -0
  147. package/dist/server/client-ip.d.ts.map +1 -0
  148. package/dist/server/cloud.d.ts +2 -1
  149. package/dist/server/cloud.d.ts.map +1 -1
  150. package/dist/server/index.d.ts +1 -1
  151. package/dist/server/index.js +24758 -16687
  152. package/dist/server/openapi.d.ts +2514 -469
  153. package/dist/server/openapi.d.ts.map +1 -1
  154. package/dist/server/port.d.ts +2 -2
  155. package/dist/server/port.d.ts.map +1 -1
  156. package/dist/server/rate-limit-config.d.ts +6 -0
  157. package/dist/server/rate-limit-config.d.ts.map +1 -0
  158. package/dist/server/routes.d.ts +3 -6
  159. package/dist/server/routes.d.ts.map +1 -1
  160. package/dist/server/serve.d.ts +2 -6
  161. package/dist/server/serve.d.ts.map +1 -1
  162. package/dist/server/v1.d.ts +3 -1
  163. package/dist/server/v1.d.ts.map +1 -1
  164. package/dist/storage/atomic-project-migration.d.ts +53 -0
  165. package/dist/storage/atomic-project-migration.d.ts.map +1 -0
  166. package/dist/storage/index.d.ts +4 -2
  167. package/dist/storage/index.d.ts.map +1 -1
  168. package/dist/storage/interfaces.d.ts +135 -3
  169. package/dist/storage/interfaces.d.ts.map +1 -1
  170. package/dist/storage/local-sqlite.d.ts.map +1 -1
  171. package/dist/storage/machine-registry.d.ts +36 -0
  172. package/dist/storage/machine-registry.d.ts.map +1 -0
  173. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  174. package/dist/storage/postgres-sync.d.ts +2 -2
  175. package/dist/storage/postgres-sync.d.ts.map +1 -1
  176. package/dist/storage/run-artifact-creation-upload.d.ts +50 -0
  177. package/dist/storage/run-artifact-creation-upload.d.ts.map +1 -0
  178. package/dist/storage/s3-artifact-sync.d.ts +18 -0
  179. package/dist/storage/s3-artifact-sync.d.ts.map +1 -1
  180. package/dist/storage/sqlite-snapshot.d.ts +2 -0
  181. package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
  182. package/dist/storage.d.ts +2 -2
  183. package/dist/storage.d.ts.map +1 -1
  184. package/dist/storage.js +3154 -350
  185. package/dist/task-manifest/postgres.d.ts +1 -1
  186. package/dist/task-manifest/sqlite.d.ts +1 -1
  187. package/dist/task-manifest.js +250 -73
  188. package/dist/task-subtree-transfer.js +198 -4
  189. package/dist/testing.d.ts +90 -29
  190. package/dist/testing.d.ts.map +1 -1
  191. package/dist/testing.js +34 -10
  192. package/dist/types/index.d.ts +43 -4
  193. package/dist/types/index.d.ts.map +1 -1
  194. package/docs/PLAN_API.md +93 -0
  195. package/docs/TASK_LIST_API.md +58 -0
  196. package/docs/TASK_QUERY_API.md +14 -0
  197. package/docs/TEMPLATE_API.md +18 -0
  198. package/docs/native-storage.md +405 -0
  199. package/package.json +18 -15
  200. package/postinstall.js +52 -1
  201. package/dashboard/dist/assets/index-BNQ4gJua.js +0 -342
  202. package/dashboard/dist/assets/index-DjzvHUWt.css +0 -1
  203. package/dashboard/dist/index.html +0 -13
  204. package/dashboard/dist/logo.jpg +0 -0
  205. package/dist/mcp/tools/code-tools.d.ts +0 -15
  206. package/dist/mcp/tools/code-tools.d.ts.map +0 -1
package/dist/contracts.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
@@ -4000,20 +4117,17 @@ function ensureAgentIdentitySchema(db) {
4000
4117
  SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
4001
4118
  END;
4002
4119
 
4120
+ -- Drop first so an older definition is replaced rather than silently
4121
+ -- retained (the upgrade path). AGENT_IDENTITY_HISTORY_TRIGGER_DDL then
4122
+ -- re-creates them with CREATE TRIGGER IF NOT EXISTS. The two statements
4123
+ -- do different jobs and both are kept: the DROP is what refreshes a
4124
+ -- stale definition, and the IF NOT EXISTS is what keeps the CREATE safe
4125
+ -- on its own \u2014 so a caller that reaches it without a preceding DROP, in
4126
+ -- its own connection or transaction, gets a no-op instead of
4127
+ -- "trigger ... already exists".
4003
4128
  DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_immutable;
4004
- CREATE TRIGGER trg_agent_identity_mapping_history_immutable
4005
- BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
4006
- ON agent_identity_source_mappings
4007
- BEGIN
4008
- SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
4009
- END;
4010
-
4011
4129
  DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_append_only;
4012
- CREATE TRIGGER trg_agent_identity_mapping_history_append_only
4013
- BEFORE DELETE ON agent_identity_source_mappings
4014
- BEGIN
4015
- SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
4016
- END;
4130
+ ${AGENT_IDENTITY_HISTORY_TRIGGER_DDL}
4017
4131
  `);
4018
4132
  db.run("INSERT OR IGNORE INTO _migrations (id) VALUES (?)", [IDENTITY_MIGRATION_ID]);
4019
4133
  });
@@ -4053,7 +4167,20 @@ function storeAgentAlias(agentId, label, aliasKind, status, db) {
4053
4167
  function recordAgentAlias(agentId, label, db) {
4054
4168
  return storeAgentAlias(agentId, label, "historical", "active", db);
4055
4169
  }
4056
- var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65;
4170
+ var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65, AGENT_IDENTITY_HISTORY_TRIGGER_DDL = `
4171
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_immutable
4172
+ BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
4173
+ ON agent_identity_source_mappings
4174
+ BEGIN
4175
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
4176
+ END;
4177
+
4178
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_append_only
4179
+ BEFORE DELETE ON agent_identity_source_mappings
4180
+ BEGIN
4181
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
4182
+ END;
4183
+ `;
4057
4184
  var init_identity_mapping = __esm(() => {
4058
4185
  init_types();
4059
4186
  init_types();
@@ -4075,108 +4202,106 @@ var init_identity_mapping = __esm(() => {
4075
4202
  });
4076
4203
  });
4077
4204
 
4078
- // node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
4205
+ // src/lib/paths.ts
4206
+ import { existsSync as existsSync2 } from "fs";
4079
4207
  import { homedir } from "os";
4080
- import { join } from "path";
4081
- function assertApp(app) {
4208
+ import { isAbsolute, join, resolve as resolve2 } from "path";
4209
+ import { homedir as pathsResolverHomedir } from "os";
4210
+ import { join as pathsResolverJoin } from "path";
4211
+ function pathsResolverAssertApp(app) {
4082
4212
  if (typeof app !== "string" || app.length === 0) {
4083
4213
  throw new TypeError("paths: app must be a non-empty string");
4084
4214
  }
4085
- if (!APP_SLUG_RE.test(app)) {
4215
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
4086
4216
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
4087
4217
  }
4088
4218
  }
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";
4219
+ function pathsResolverAssertKind(kind) {
4220
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
4221
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
4222
+ }
4098
4223
  }
4099
- function baseDir(kind, options) {
4100
- const override = envValue(options, kind);
4101
- if (override)
4224
+ function pathsResolverBaseDir(kind, options) {
4225
+ pathsResolverAssertKind(kind);
4226
+ const env2 = options.env ?? process.env;
4227
+ const override = env2[PATHS_RESOLVER_KIND_ENV[kind]];
4228
+ if (typeof override === "string" && override.length > 0)
4102
4229
  return override;
4103
- const home = options.home ?? homedir();
4230
+ const home = options.home ?? pathsResolverHomedir();
4104
4231
  const platform = options.platform ?? process.platform;
4105
- if (isMacOS(platform)) {
4232
+ if (platform === "darwin") {
4106
4233
  switch (kind) {
4107
4234
  case "config":
4108
4235
  case "data":
4109
- return join(home, "Library", "Application Support", "Hasna");
4236
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
4110
4237
  case "cache":
4111
- return join(home, "Library", "Caches", "Hasna");
4238
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
4112
4239
  case "state":
4113
- return join(home, "Library", "Logs", "Hasna");
4240
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
4114
4241
  }
4115
4242
  }
4116
4243
  switch (kind) {
4117
4244
  case "config":
4118
- return join(home, ".config", "hasna");
4245
+ return pathsResolverJoin(home, ".config", "hasna");
4119
4246
  case "data":
4120
- return join(home, ".local", "share", "hasna");
4247
+ return pathsResolverJoin(home, ".local", "share", "hasna");
4121
4248
  case "state":
4122
- return join(home, ".local", "state", "hasna");
4249
+ return pathsResolverJoin(home, ".local", "state", "hasna");
4123
4250
  case "cache":
4124
- return join(home, ".cache", "hasna");
4251
+ return pathsResolverJoin(home, ".cache", "hasna");
4125
4252
  }
4126
4253
  }
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);
4254
+ function pathsResolverResolve(kind, options) {
4255
+ pathsResolverAssertApp(options.app);
4256
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
4257
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
4131
4258
  }
4132
4259
  function dataDir(options) {
4133
- return resolvePath("data", options);
4260
+ return pathsResolverResolve("data", options);
4134
4261
  }
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();
4262
+ function effectiveHome(env2 = process.env) {
4263
+ return env2.HOME || env2.USERPROFILE || homedir();
4264
+ }
4265
+ function hasnaHomeRoot(env2 = process.env) {
4266
+ const override = env2["HASNA_HOME"]?.trim();
4267
+ if (override && isAbsolute(override))
4268
+ return override;
4269
+ return join(effectiveHome(env2), ".hasna");
4152
4270
  }
4153
- function legacyHomeDir(env = process.env) {
4154
- return join2(effectiveHome(env), ".hasna", "todos");
4271
+ function legacyHomeDir(env2 = process.env) {
4272
+ return join(hasnaHomeRoot(env2), "todos");
4155
4273
  }
4156
- function resolverHome(env = process.env) {
4157
- return dataDir({ app: "todos", home: effectiveHome(env), env });
4274
+ function resolverHome(env2 = process.env) {
4275
+ return dataDir({ app: "todos", home: effectiveHome(env2), env: env2 });
4158
4276
  }
4159
- function adoptResolverHome(resolved, env = process.env) {
4160
- const dataOverride = env.HASNA_DATA_HOME;
4277
+ function adoptResolverHome(resolved, env2 = process.env) {
4278
+ const dataOverride = env2.HASNA_DATA_HOME;
4161
4279
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
4162
4280
  return true;
4163
- return existsSync2(join2(resolved, "todos.db")) || existsSync2(join2(resolved, "config.json"));
4281
+ return existsSync2(join(resolved, "todos.db")) || existsSync2(join(resolved, "config.json"));
4164
4282
  }
4165
- function getTodosDir(env = process.env) {
4166
- const resolved = resolverHome(env);
4167
- return resolve2(adoptResolverHome(resolved, env) ? resolved : legacyHomeDir(env));
4283
+ function getTodosDir(env2 = process.env) {
4284
+ const resolved = resolverHome(env2);
4285
+ return resolve2(adoptResolverHome(resolved, env2) ? resolved : legacyHomeDir(env2));
4168
4286
  }
4287
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
4169
4288
  var init_paths = __esm(() => {
4170
- init_dist();
4289
+ PATHS_RESOLVER_KIND_ENV = {
4290
+ config: "HASNA_CONFIG_HOME",
4291
+ data: "HASNA_DATA_HOME",
4292
+ state: "HASNA_STATE_HOME",
4293
+ cache: "HASNA_CACHE_HOME"
4294
+ };
4295
+ PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4171
4296
  });
4172
4297
 
4173
4298
  // src/lib/sync-utils.ts
4174
4299
  import { existsSync as existsSync3, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
4175
4300
  import { createHash } from "crypto";
4176
- import { homedir as homedir3 } from "os";
4177
- import { join as join3 } from "path";
4301
+ import { homedir as homedir2 } from "os";
4302
+ import { join as join2 } from "path";
4178
4303
  function getHomeDir() {
4179
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir3();
4304
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
4180
4305
  }
4181
4306
  function getTodosGlobalDir() {
4182
4307
  return getTodosDir();
@@ -4202,14 +4327,14 @@ function writeJsonFile(path, data) {
4202
4327
  `);
4203
4328
  }
4204
4329
  function readHighWaterMark(dir) {
4205
- const path = join3(dir, ".highwatermark");
4330
+ const path = join2(dir, ".highwatermark");
4206
4331
  if (!existsSync3(path))
4207
4332
  return 1;
4208
4333
  const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
4209
4334
  return isNaN(val) ? 1 : val;
4210
4335
  }
4211
4336
  function writeHighWaterMark(dir, value) {
4212
- writeFileSync(join3(dir, ".highwatermark"), String(value));
4337
+ writeFileSync(join2(dir, ".highwatermark"), String(value));
4213
4338
  }
4214
4339
  function getFileMtimeMs(path) {
4215
4340
  try {
@@ -4294,23 +4419,23 @@ __export(exports_config, {
4294
4419
  CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
4295
4420
  CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
4296
4421
  });
4297
- function getCanonicalTodosRdsConfig(env = process.env) {
4422
+ function getCanonicalTodosRdsConfig(env2 = process.env) {
4298
4423
  return {
4299
- cluster: clean(env[CANONICAL_TODOS_RDS_CLUSTER_ENV]) ?? null,
4424
+ cluster: clean(env2[CANONICAL_TODOS_RDS_CLUSTER_ENV]) ?? null,
4300
4425
  database: CANONICAL_TODOS_RDS_DATABASE,
4301
- runtimeSecretPath: clean(env[CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV]) ?? null,
4426
+ runtimeSecretPath: clean(env2[CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV]) ?? null,
4302
4427
  primaryEnv: TODOS_STORAGE_ENV.databaseUrl,
4303
4428
  fallbackEnv: TODOS_STORAGE_FALLBACK_ENV.databaseUrl
4304
4429
  };
4305
4430
  }
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;
4431
+ function loadTodosStorageConfig(env2 = process.env) {
4432
+ const mode = getTodosStorageBackend(env2);
4433
+ const databaseUrl = getTodosStorageDatabaseUrl(env2);
4434
+ const bucket = readStorageEnv(env2, "s3Bucket").value;
4435
+ const prefix = readStorageEnv(env2, "s3Prefix").value ?? "todos/";
4436
+ const region = readStorageEnv(env2, "awsRegion").value;
4437
+ const endpoint = readStorageEnv(env2, "s3Endpoint").value;
4438
+ const schema = readStorageEnv(env2, "databaseSchema").value;
4314
4439
  return {
4315
4440
  service: "todos",
4316
4441
  mode,
@@ -4318,7 +4443,7 @@ function loadTodosStorageConfig(env = process.env) {
4318
4443
  database: {
4319
4444
  provider: "postgres",
4320
4445
  url: databaseUrl,
4321
- ssl: parseBoolean(readStorageEnv(env, "databaseSsl").value, true),
4446
+ ssl: parseBoolean(readStorageEnv(env2, "databaseSsl").value, true),
4322
4447
  ...schema ? { schema } : {}
4323
4448
  }
4324
4449
  } : {},
@@ -4329,17 +4454,17 @@ function loadTodosStorageConfig(env = process.env) {
4329
4454
  prefix,
4330
4455
  ...region ? { region } : {},
4331
4456
  ...endpoint ? { endpoint } : {},
4332
- forcePathStyle: parseBoolean(readStorageEnv(env, "s3ForcePathStyle").value, false)
4457
+ forcePathStyle: parseBoolean(readStorageEnv(env2, "s3ForcePathStyle").value, false)
4333
4458
  }
4334
4459
  } : {},
4335
4460
  sync: {
4336
- batchSize: parsePositiveInteger(readStorageEnv(env, "syncBatchSize").value, 500),
4337
- dryRun: parseBoolean(readStorageEnv(env, "syncDryRun").value, false)
4461
+ batchSize: parsePositiveInteger(readStorageEnv(env2, "syncBatchSize").value, 500),
4462
+ dryRun: parseBoolean(readStorageEnv(env2, "syncDryRun").value, false)
4338
4463
  }
4339
4464
  };
4340
4465
  }
4341
- function loadStorageConfig(env = process.env) {
4342
- return loadTodosStorageConfig(env);
4466
+ function loadStorageConfig(env2 = process.env) {
4467
+ return loadTodosStorageConfig(env2);
4343
4468
  }
4344
4469
  function isTodosPostgresBackend(config) {
4345
4470
  return config.mode === "postgres";
@@ -4361,51 +4486,51 @@ function parseStorageBackend(value) {
4361
4486
  return "postgres";
4362
4487
  throw new Error(`Storage backend must be sqlite or postgres`);
4363
4488
  }
4364
- function getTodosStorageBackend(env = process.env) {
4365
- if (!getTodosStorageDatabaseUrl(env))
4489
+ function getTodosStorageBackend(env2 = process.env) {
4490
+ if (!getTodosStorageDatabaseUrl(env2))
4366
4491
  return "sqlite";
4367
- if (isTodosShadowEnabled(env))
4492
+ if (isTodosShadowEnabled(env2))
4368
4493
  return "sqlite";
4369
4494
  return "postgres";
4370
4495
  }
4371
- function isTodosShadowEnabled(env = process.env) {
4372
- return parseBoolean(readStorageEnv(env, "shadow").value, false);
4496
+ function isTodosShadowEnabled(env2 = process.env) {
4497
+ return parseBoolean(readStorageEnv(env2, "shadow").value, false);
4373
4498
  }
4374
- function getTodosStorageShadowEnvName(env = process.env) {
4375
- return readStorageEnv(env, "shadow").name;
4499
+ function getTodosStorageShadowEnvName(env2 = process.env) {
4500
+ return readStorageEnv(env2, "shadow").name;
4376
4501
  }
4377
- function assertTodosShadowConfig(config, env = process.env) {
4378
- if (!isTodosShadowEnabled(env))
4502
+ function assertTodosShadowConfig(config, env2 = process.env) {
4503
+ if (!isTodosShadowEnabled(env2))
4379
4504
  return;
4380
4505
  if (config.mode !== "sqlite") {
4381
- throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
4506
+ throw new Error(`${readStorageEnv(env2, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
4382
4507
  }
4383
4508
  if (!config.database?.url) {
4384
- throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
4509
+ throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env2, "shadow").name} is enabled`);
4385
4510
  }
4386
4511
  }
4387
- function getTodosStorageDatabaseEnv(env = process.env) {
4388
- return readStorageEnv(env, "databaseUrl").name;
4512
+ function getTodosStorageDatabaseEnv(env2 = process.env) {
4513
+ return readStorageEnv(env2, "databaseUrl").name;
4389
4514
  }
4390
- function getStorageDatabaseEnv(env = process.env) {
4391
- return getTodosStorageDatabaseEnv(env);
4515
+ function getStorageDatabaseEnv(env2 = process.env) {
4516
+ return getTodosStorageDatabaseEnv(env2);
4392
4517
  }
4393
- function getTodosStorageDatabaseUrl(env = process.env) {
4394
- return readStorageEnv(env, "databaseUrl").value;
4518
+ function getTodosStorageDatabaseUrl(env2 = process.env) {
4519
+ return readStorageEnv(env2, "databaseUrl").value;
4395
4520
  }
4396
- function getStorageDatabaseUrl(env = process.env) {
4397
- return getTodosStorageDatabaseUrl(env);
4521
+ function getStorageDatabaseUrl(env2 = process.env) {
4522
+ return getTodosStorageDatabaseUrl(env2);
4398
4523
  }
4399
- function getTodosStorageEnvName(env, key) {
4400
- return readStorageEnv(env, key).name;
4524
+ function getTodosStorageEnvName(env2, key) {
4525
+ return readStorageEnv(env2, key).name;
4401
4526
  }
4402
- function readStorageEnv(env, key) {
4527
+ function readStorageEnv(env2, key) {
4403
4528
  const primaryName = TODOS_STORAGE_ENV[key];
4404
- const primaryValue = clean(env[primaryName]);
4529
+ const primaryValue = clean(env2[primaryName]);
4405
4530
  if (primaryValue !== undefined)
4406
4531
  return { name: primaryName, value: primaryValue };
4407
4532
  const fallbackName = TODOS_STORAGE_FALLBACK_ENV[key];
4408
- const fallbackValue = clean(env[fallbackName]);
4533
+ const fallbackValue = clean(env2[fallbackName]);
4409
4534
  if (fallbackValue !== undefined)
4410
4535
  return { name: fallbackName, value: fallbackValue };
4411
4536
  return { name: primaryName };
@@ -4563,20 +4688,23 @@ __export(exports_database, {
4563
4688
  resolvePartialId: () => resolvePartialId,
4564
4689
  resolveAssignedToAliases: () => resolveAssignedToAliases,
4565
4690
  resetDatabase: () => resetDatabase,
4691
+ refuseLocalStore: () => refuseLocalStore,
4566
4692
  now: () => now,
4567
4693
  lowerInClause: () => lowerInClause,
4568
4694
  lockExpiryCutoff: () => lockExpiryCutoff,
4569
4695
  isLockExpired: () => isLockExpired,
4696
+ isLocalStoreRefused: () => isLocalStoreRefused,
4570
4697
  getDatabasePath: () => getDatabasePath,
4571
4698
  getDatabase: () => getDatabase,
4572
4699
  closeDatabase: () => closeDatabase,
4573
4700
  clearExpiredLocks: () => clearExpiredLocks,
4574
4701
  assignedToAliasSet: () => assignedToAliasSet,
4702
+ allowLocalStore: () => allowLocalStore,
4575
4703
  LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
4576
4704
  });
4577
4705
  import { Database } from "bun:sqlite";
4578
4706
  import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
4579
- import { dirname, join as join4, resolve as resolve3 } from "path";
4707
+ import { dirname, join as join3, resolve as resolve3 } from "path";
4580
4708
  function isInMemoryDb(path) {
4581
4709
  return path === ":memory:" || path.startsWith("file::memory:");
4582
4710
  }
@@ -4585,7 +4713,7 @@ function findNearestProjectDb(startDir) {
4585
4713
  const stopAt = gitRoot ? resolve3(gitRoot) : resolve3(startDir);
4586
4714
  let dir = resolve3(startDir);
4587
4715
  while (true) {
4588
- const candidate = join4(dir, ".hasna", "todos", "todos.db");
4716
+ const candidate = join3(dir, ".hasna", "todos", "todos.db");
4589
4717
  if (existsSync4(candidate))
4590
4718
  return candidate;
4591
4719
  if (dir === stopAt)
@@ -4600,7 +4728,7 @@ function findNearestProjectDb(startDir) {
4600
4728
  function findGitRoot(startDir) {
4601
4729
  let dir = resolve3(startDir);
4602
4730
  while (true) {
4603
- if (existsSync4(join4(dir, ".git")))
4731
+ if (existsSync4(join3(dir, ".git")))
4604
4732
  return dir;
4605
4733
  const parent = dirname(dir);
4606
4734
  if (parent === dir)
@@ -4610,7 +4738,7 @@ function findGitRoot(startDir) {
4610
4738
  return null;
4611
4739
  }
4612
4740
  function getGlobalDbPath() {
4613
- return join4(getTodosGlobalDir(), "todos.db");
4741
+ return join3(getTodosGlobalDir(), "todos.db");
4614
4742
  }
4615
4743
  function hasExplicitProjectArg(args = process.argv.slice(2)) {
4616
4744
  return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
@@ -4648,7 +4776,7 @@ function getDbPath() {
4648
4776
  if (process.env["TODOS_DB_SCOPE"] === "project") {
4649
4777
  const gitRoot = findGitRoot(cwd);
4650
4778
  if (gitRoot && canCreateScopedProjectDb()) {
4651
- return join4(gitRoot, ".hasna", "todos", "todos.db");
4779
+ return join3(gitRoot, ".hasna", "todos", "todos.db");
4652
4780
  }
4653
4781
  }
4654
4782
  return getGlobalDbPath();
@@ -4688,7 +4816,21 @@ function maybeInstallShadowCapture(db) {
4688
4816
  console.error(`[todos] shadow capture install failed: ${error instanceof Error ? error.message : String(error)}`);
4689
4817
  }
4690
4818
  }
4819
+ function refuseLocalStore(message) {
4820
+ localStoreRefusal = message;
4821
+ }
4822
+ function allowLocalStore() {
4823
+ localStoreRefusal = null;
4824
+ }
4825
+ function isLocalStoreRefused() {
4826
+ return localStoreRefusal !== null;
4827
+ }
4691
4828
  function getDatabase(dbPath) {
4829
+ if (localStoreRefusal !== null)
4830
+ throw new Error(localStoreRefusal);
4831
+ if (dbPath === undefined && !selectsTodosLocalStore()) {
4832
+ throw new Error("API_DATABASE_FALLBACK_FORBIDDEN: this operation must use the shared Todos API; implicit SQLite access is unavailable");
4833
+ }
4692
4834
  const path = dbPath || getDbPath();
4693
4835
  if (_db && _dbPath === path)
4694
4836
  return _db;
@@ -4826,8 +4968,9 @@ function lowerInClause(column, values, params) {
4826
4968
  params.push(...values.map((v) => v.toLowerCase()));
4827
4969
  return `LOWER(${column}) IN (${values.map(() => "?").join(",")})`;
4828
4970
  }
4829
- var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
4971
+ var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, localStoreRefusal = null, ALLOWED_TABLES;
4830
4972
  var init_database = __esm(() => {
4973
+ init_local_opt_in();
4831
4974
  init_schema();
4832
4975
  init_machines();
4833
4976
  init_identity_mapping();
@@ -4838,9 +4981,9 @@ var init_database = __esm(() => {
4838
4981
 
4839
4982
  // src/lib/config.ts
4840
4983
  import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
4841
- import { dirname as dirname2, join as join5 } from "path";
4984
+ import { dirname as dirname2, join as join4 } from "path";
4842
4985
  function getConfigPath() {
4843
- return join5(getTodosGlobalDir(), "config.json");
4986
+ return join4(getTodosGlobalDir(), "config.json");
4844
4987
  }
4845
4988
  function normalizeAgent(agent) {
4846
4989
  return agent.trim().toLowerCase();
@@ -4891,21 +5034,6 @@ function normalizeApiUrl(value) {
4891
5034
  return null;
4892
5035
  return trimmed.replace(/\/+$/, "");
4893
5036
  }
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
5037
  function getSyncAgentsFromConfig() {
4910
5038
  const config = loadConfig();
4911
5039
  const agents = config.sync_agents;
@@ -5756,11 +5884,11 @@ function writeAllowed(profile, root, writePath) {
5756
5884
  return isPathInside(scopeRoot, target);
5757
5885
  });
5758
5886
  }
5759
- function redactedEnvKeys(profile, env) {
5760
- if (!env)
5887
+ function redactedEnvKeys(profile, env2) {
5888
+ if (!env2)
5761
5889
  return [];
5762
5890
  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)));
5891
+ return Object.keys(env2).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
5764
5892
  }
5765
5893
  function checkWorkspacePermission(input = {}) {
5766
5894
  const status = getWorkspaceTrustStatus(input.path || process.cwd());
@@ -5898,18 +6026,18 @@ function profileByName(name, path) {
5898
6026
  const resolved = normalizePath2(path);
5899
6027
  return profiles.find((profile) => isPathInside2(profile.root, resolved)) || defaultProfile2("default", resolved);
5900
6028
  }
5901
- function redactedEnvKeys2(profile, env) {
5902
- if (!env)
6029
+ function redactedEnvKeys2(profile, env2) {
6030
+ if (!env2)
5903
6031
  return [];
5904
6032
  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)));
6033
+ return Object.keys(env2).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
5906
6034
  }
5907
- function omittedEnvKeys(profile, env) {
5908
- if (!env)
6035
+ function omittedEnvKeys(profile, env2) {
6036
+ if (!env2)
5909
6037
  return [];
5910
6038
  if (profile.env_allowlist.includes("*"))
5911
6039
  return [];
5912
- return Object.keys(env).filter((key) => !profile.env_allowlist.some((pattern) => matchesPattern2(key, pattern)));
6040
+ return Object.keys(env2).filter((key) => !profile.env_allowlist.some((pattern) => matchesPattern2(key, pattern)));
5913
6041
  }
5914
6042
  function resolveFromCwd(cwd, path) {
5915
6043
  return normalizePath2(path.startsWith("/") ? path : `${cwd}/${path}`);
@@ -6292,15 +6420,23 @@ var init_event_hooks = __esm(() => {
6292
6420
  VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
6293
6421
  });
6294
6422
 
6295
- // node_modules/.bun/@hasna+events@0.1.16/node_modules/@hasna/events/dist/index.js
6423
+ // node_modules/.bun/@hasna+events@0.1.17/node_modules/@hasna/events/dist/index.js
6296
6424
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
6297
6425
  import { Buffer as Buffer2 } from "buffer";
6426
+ import { existsSync as existsSync22 } from "fs";
6427
+ import { join as join32 } from "path";
6298
6428
  import { existsSync as existsSync6 } from "fs";
6299
- import { homedir as homedir4 } from "os";
6300
- import { join as join6 } from "path";
6429
+ import { homedir as homedir22 } from "os";
6430
+ import { join as join22, resolve as resolve8 } from "path";
6431
+ import { homedir as homedir3 } from "os";
6432
+ import { join as join5 } from "path";
6301
6433
  import { createHmac, timingSafeEqual } from "crypto";
6434
+ import { lookup as dnsLookup } from "dns/promises";
6435
+ import { isIP } from "net";
6302
6436
  import { randomUUID as randomUUID2 } from "crypto";
6303
6437
  import { spawn } from "child_process";
6438
+ import { request as nodeHttpRequest } from "http";
6439
+ import { request as nodeHttpsRequest } from "https";
6304
6440
  import { randomUUID as randomUUID22 } from "crypto";
6305
6441
  function getPathValue(input, path) {
6306
6442
  return path.split(".").reduce((value, part) => {
@@ -6397,8 +6533,93 @@ function channelMatchesEvent(channel, event) {
6397
6533
  return true;
6398
6534
  return channel.filters.some((filter) => eventMatchesFilter(event, filter));
6399
6535
  }
6536
+ function assertApp(app) {
6537
+ if (typeof app !== "string" || app.length === 0) {
6538
+ throw new TypeError("paths: app must be a non-empty string");
6539
+ }
6540
+ if (!APP_SLUG_RE.test(app)) {
6541
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
6542
+ }
6543
+ }
6544
+ function envOf(options) {
6545
+ return options.env ?? process.env;
6546
+ }
6547
+ function envValue(options, kind) {
6548
+ const value = envOf(options)[KIND_ENV[kind]];
6549
+ return typeof value === "string" && value.length > 0 ? value : undefined;
6550
+ }
6551
+ function isMacOS(platform) {
6552
+ return platform === "darwin";
6553
+ }
6554
+ function baseDir(kind, options) {
6555
+ const override = envValue(options, kind);
6556
+ if (override)
6557
+ return override;
6558
+ const home = options.home ?? homedir3();
6559
+ const platform = options.platform ?? process.platform;
6560
+ if (isMacOS(platform)) {
6561
+ switch (kind) {
6562
+ case "config":
6563
+ case "data":
6564
+ return join5(home, "Library", "Application Support", "Hasna");
6565
+ case "cache":
6566
+ return join5(home, "Library", "Caches", "Hasna");
6567
+ case "state":
6568
+ return join5(home, "Library", "Logs", "Hasna");
6569
+ }
6570
+ }
6571
+ switch (kind) {
6572
+ case "config":
6573
+ return join5(home, ".config", "hasna");
6574
+ case "data":
6575
+ return join5(home, ".local", "share", "hasna");
6576
+ case "state":
6577
+ return join5(home, ".local", "state", "hasna");
6578
+ case "cache":
6579
+ return join5(home, ".cache", "hasna");
6580
+ }
6581
+ }
6582
+ function resolvePath(kind, options) {
6583
+ assertApp(options.app);
6584
+ const appSegment = options.internal === true ? join5("internal", options.app) : options.app;
6585
+ return join5(baseDir(kind, options), appSegment);
6586
+ }
6587
+ function dataDir2(options) {
6588
+ return resolvePath("data", options);
6589
+ }
6590
+ function effectiveHome2() {
6591
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir22();
6592
+ }
6593
+ function legacyHomeDir2() {
6594
+ return join22(effectiveHome2(), ".hasna", "events");
6595
+ }
6596
+ function resolverHome2() {
6597
+ return dataDir2({ app: "events", home: effectiveHome2() || undefined });
6598
+ }
6599
+ function adoptResolverHome2(resolved, env2 = process.env) {
6600
+ const dataOverride = env2.HASNA_DATA_HOME;
6601
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
6602
+ return true;
6603
+ return existsSync6(join22(resolved, EVENTS_STORE_SENTINEL_FILE));
6604
+ }
6605
+ function exactEventsHome() {
6606
+ const dir = process.env[HASNA_EVENTS_DIR_ENV];
6607
+ if (dir && dir.trim())
6608
+ return dir.trim();
6609
+ const home = process.env[HASNA_EVENTS_HOME_ENV];
6610
+ if (home && home.trim())
6611
+ return home.trim();
6612
+ return;
6613
+ }
6614
+ function getEventsHome() {
6615
+ const exact = exactEventsHome();
6616
+ if (exact)
6617
+ return resolve8(exact);
6618
+ const resolved = resolverHome2();
6619
+ return adoptResolverHome2(resolved) ? resolve8(resolved) : resolve8(legacyHomeDir2());
6620
+ }
6400
6621
  function getEventsDataDir(override) {
6401
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join6(homedir4(), ".hasna", "events");
6622
+ return override || getEventsHome();
6402
6623
  }
6403
6624
 
6404
6625
  class JsonEventsStore {
@@ -6407,12 +6628,12 @@ class JsonEventsStore {
6407
6628
  channelsPath;
6408
6629
  eventsPath;
6409
6630
  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");
6631
+ constructor(dataDir22 = getEventsDataDir()) {
6632
+ this.dataDir = dataDir22;
6633
+ this.runtime = localJsonRuntime(dataDir22);
6634
+ this.channelsPath = join32(dataDir22, "channels.json");
6635
+ this.eventsPath = join32(dataDir22, "events.json");
6636
+ this.deliveriesPath = join32(dataDir22, "deliveries.json");
6416
6637
  }
6417
6638
  async init() {
6418
6639
  await mkdir(this.dataDir, { recursive: true, mode: 448 });
@@ -6529,7 +6750,7 @@ class JsonEventsStore {
6529
6750
  };
6530
6751
  }
6531
6752
  async ensureArrayFile(path) {
6532
- if (!existsSync6(path)) {
6753
+ if (!existsSync22(path)) {
6533
6754
  await writeFile(path, `[]
6534
6755
  `, { encoding: "utf-8", mode: 384 });
6535
6756
  }
@@ -6559,7 +6780,7 @@ class JsonEventsStore {
6559
6780
  });
6560
6781
  }
6561
6782
  }
6562
- function localJsonRuntime(dataDir2 = getEventsDataDir()) {
6783
+ function localJsonRuntime(dataDir22 = getEventsDataDir()) {
6563
6784
  return {
6564
6785
  mode: "local-files",
6565
6786
  name: "json-events-store",
@@ -6572,7 +6793,7 @@ function localJsonRuntime(dataDir2 = getEventsDataDir()) {
6572
6793
  durable: true,
6573
6794
  idempotency: "best-effort-local",
6574
6795
  replayCursors: true,
6575
- description: `Local JSON files in ${dataDir2}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
6796
+ description: `Local JSON files in ${dataDir22}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
6576
6797
  };
6577
6798
  }
6578
6799
  function encodeLocalJsonEventCursor(offset, options = {}) {
@@ -6643,6 +6864,177 @@ function signPayload(secret, timestamp2, body) {
6643
6864
  const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp2, body)).digest("hex");
6644
6865
  return `sha256=${digest}`;
6645
6866
  }
6867
+ function isPrivateAddress(address) {
6868
+ const normalized = stripZoneId(address);
6869
+ const version = isIP(normalized);
6870
+ if (version === 4) {
6871
+ const integer = ipv4ToInt(normalized);
6872
+ if (integer === undefined)
6873
+ return true;
6874
+ return IPV4_PRIVATE_RANGES.some(([low, high]) => integer >= low && integer <= high);
6875
+ }
6876
+ if (version === 6) {
6877
+ const groups = ipv6Groups(normalized);
6878
+ if (!groups)
6879
+ return true;
6880
+ for (const prefix of IPV6_SPECIAL_PREFIXES) {
6881
+ if (!ipv6MatchesPrefix(groups, prefix.groups, prefix.bits))
6882
+ continue;
6883
+ if (prefix.bits === 96 && groups[5] === 65535) {
6884
+ return isPrivateAddress(ipv4IntToString(groups[6] << 16 | groups[7]));
6885
+ }
6886
+ if (prefix.bits === 16 && groups[0] === 8194) {
6887
+ return isPrivateAddress(ipv4IntToString(groups[1] << 16 | groups[2]));
6888
+ }
6889
+ return true;
6890
+ }
6891
+ return false;
6892
+ }
6893
+ return true;
6894
+ }
6895
+ async function resolveWebhookTarget(url, policy = {}) {
6896
+ const hostname = normalizeHostname(url.hostname);
6897
+ const allowlist = (policy.allowPrivateHosts ?? []).map((entry) => normalizeHostname(entry.toLowerCase()));
6898
+ if (allowlist.includes(hostname)) {
6899
+ const version2 = isIP(hostname);
6900
+ if (version2 === 4 || version2 === 6) {
6901
+ return { hostname, addresses: [hostname] };
6902
+ }
6903
+ const lookup2 = policy.lookup ?? defaultTargetLookup;
6904
+ let resolved2;
6905
+ try {
6906
+ resolved2 = await lookup2(hostname);
6907
+ } catch {
6908
+ throw new Error(`Webhook target ${hostname} could not be resolved`);
6909
+ }
6910
+ if (!Array.isArray(resolved2) || resolved2.length === 0) {
6911
+ throw new Error(`Webhook target ${hostname} resolved to no addresses`);
6912
+ }
6913
+ const addresses = resolved2.map((entry) => normalizeHostname(entry.address));
6914
+ return { hostname, addresses };
6915
+ }
6916
+ const version = isIP(hostname);
6917
+ if (version === 4 || version === 6) {
6918
+ if (isPrivateAddress(hostname)) {
6919
+ throw new Error(`Webhook target ${hostname} is a private or special-use address`);
6920
+ }
6921
+ return { hostname, addresses: [hostname] };
6922
+ }
6923
+ const lookup = policy.lookup ?? defaultTargetLookup;
6924
+ let resolved;
6925
+ try {
6926
+ resolved = await lookup(hostname);
6927
+ } catch {
6928
+ throw new Error(`Webhook target ${hostname} could not be resolved`);
6929
+ }
6930
+ if (!Array.isArray(resolved) || resolved.length === 0) {
6931
+ throw new Error(`Webhook target ${hostname} resolved to no addresses`);
6932
+ }
6933
+ const allowed = [];
6934
+ for (const entry of resolved) {
6935
+ const address = normalizeHostname(entry.address);
6936
+ if (isPrivateAddress(address)) {
6937
+ if (allowlist.includes(address)) {
6938
+ allowed.push(address);
6939
+ continue;
6940
+ }
6941
+ throw new Error(`Webhook target ${hostname} resolves to private or special-use address ${address}`);
6942
+ }
6943
+ allowed.push(address);
6944
+ }
6945
+ if (allowed.length === 0) {
6946
+ throw new Error(`Webhook target ${hostname} resolved to no public addresses`);
6947
+ }
6948
+ return { hostname, addresses: allowed };
6949
+ }
6950
+ function normalizeMaxRedirects(value) {
6951
+ if (value === undefined)
6952
+ return DEFAULT_MAX_REDIRECTS;
6953
+ if (!Number.isInteger(value) || value < 0)
6954
+ throw new Error("webhookTargetPolicy.maxRedirects must be a non-negative integer");
6955
+ return value;
6956
+ }
6957
+ function normalizeHostname(hostname) {
6958
+ const lower = hostname.toLowerCase();
6959
+ if (lower.startsWith("[") && lower.endsWith("]"))
6960
+ return lower.slice(1, -1);
6961
+ return lower;
6962
+ }
6963
+ function stripZoneId(address) {
6964
+ const percent = address.indexOf("%");
6965
+ return percent === -1 ? address : address.slice(0, percent);
6966
+ }
6967
+ function ipv4ToInt(address) {
6968
+ const parts = address.split(".");
6969
+ if (parts.length !== 4)
6970
+ return;
6971
+ let value = 0;
6972
+ for (const part of parts) {
6973
+ if (!/^\d{1,3}$/.test(part))
6974
+ return;
6975
+ const octet = Number(part);
6976
+ if (octet > 255)
6977
+ return;
6978
+ value = value << 8 | octet;
6979
+ }
6980
+ return value >>> 0;
6981
+ }
6982
+ function ipv4IntToString(integer) {
6983
+ return [
6984
+ integer >>> 24 & 255,
6985
+ integer >>> 16 & 255,
6986
+ integer >>> 8 & 255,
6987
+ integer & 255
6988
+ ].join(".");
6989
+ }
6990
+ function ipv6Groups(address) {
6991
+ const raw = stripZoneId(address);
6992
+ const doubleColon = raw.indexOf("::");
6993
+ const headText = doubleColon === -1 ? raw : raw.slice(0, doubleColon);
6994
+ const tailText = doubleColon === -1 ? "" : raw.slice(doubleColon + 2);
6995
+ const parseGroups = (text) => {
6996
+ if (text === "")
6997
+ return [];
6998
+ const out = [];
6999
+ for (const part of text.split(":")) {
7000
+ if (part.includes(".")) {
7001
+ const v4 = ipv4ToInt(part);
7002
+ if (v4 === undefined)
7003
+ return;
7004
+ out.push(v4 >>> 16 & 65535, v4 & 65535);
7005
+ } else {
7006
+ if (!/^[0-9a-fA-F]{1,4}$/.test(part))
7007
+ return;
7008
+ out.push(parseInt(part, 16));
7009
+ }
7010
+ }
7011
+ return out;
7012
+ };
7013
+ const head = parseGroups(headText);
7014
+ if (!head)
7015
+ return;
7016
+ const tail = parseGroups(tailText);
7017
+ if (!tail)
7018
+ return;
7019
+ const total = head.length + tail.length;
7020
+ if (doubleColon === -1) {
7021
+ return total === 8 ? head : undefined;
7022
+ }
7023
+ if (total >= 8)
7024
+ return;
7025
+ return [...head, ...new Array(8 - total).fill(0), ...tail];
7026
+ }
7027
+ function ipv6MatchesPrefix(groups, prefixGroups, prefixBits) {
7028
+ let remaining = prefixBits;
7029
+ for (let index = 0;index < prefixGroups.length && remaining > 0; index += 1) {
7030
+ const take = Math.min(16, remaining);
7031
+ const mask = 65535 << 16 - take & 65535;
7032
+ if ((groups[index] & mask) !== (prefixGroups[index] & mask))
7033
+ return false;
7034
+ remaining -= take;
7035
+ }
7036
+ return true;
7037
+ }
6646
7038
  function now2() {
6647
7039
  return new Date().toISOString();
6648
7040
  }
@@ -6673,9 +7065,18 @@ function buildWebhookRequest(event, channel, options = {}) {
6673
7065
  }
6674
7066
  return { body, headers };
6675
7067
  }
7068
+ function normalizeWebhookUrl(raw) {
7069
+ const url = new URL(raw);
7070
+ if (url.username !== "" || url.password !== "") {
7071
+ url.username = "";
7072
+ url.password = "";
7073
+ }
7074
+ return url.toString();
7075
+ }
6676
7076
  async function dispatchWebhook(event, channel, options = {}) {
6677
7077
  if (!channel.webhook)
6678
7078
  throw new Error(`Channel ${channel.id} has no webhook config`);
7079
+ const webhookUrl = normalizeWebhookUrl(channel.webhook.url);
6679
7080
  const startedAt = now2();
6680
7081
  let secret = channel.webhook.secret;
6681
7082
  if (channel.webhook.secretRef) {
@@ -6692,10 +7093,14 @@ async function dispatchWebhook(event, channel, options = {}) {
6692
7093
  }
6693
7094
  const timestamp2 = (options.now?.() ?? new Date).toISOString();
6694
7095
  const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
7096
+ const validateTargets = options.webhookTargetPolicy !== undefined || options.fetchImpl === undefined;
7097
+ if (validateTargets) {
7098
+ return dispatchValidatedWebhook(event, channel, { body, headers, startedAt, options });
7099
+ }
6695
7100
  const controller = new AbortController;
6696
7101
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
6697
7102
  try {
6698
- const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
7103
+ const response = await (options.fetchImpl ?? fetch)(webhookUrl, {
6699
7104
  method: "POST",
6700
7105
  headers,
6701
7106
  body,
@@ -6723,6 +7128,130 @@ async function dispatchWebhook(event, channel, options = {}) {
6723
7128
  clearTimeout(timeout);
6724
7129
  }
6725
7130
  }
7131
+ function isRedirectStatus(status) {
7132
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
7133
+ }
7134
+ function redirectKeepsBody(status) {
7135
+ return status === 307 || status === 308;
7136
+ }
7137
+ async function pinnedNativeRequest(target, addresses, method, headers, body, signal, tls) {
7138
+ const isHttps = target.protocol === "https:";
7139
+ if (!isHttps && target.protocol !== "http:") {
7140
+ throw new Error(`Webhook target uses unsupported protocol ${target.protocol}`);
7141
+ }
7142
+ const defaultPort = isHttps ? 443 : 80;
7143
+ const port = target.port ? Number(target.port) : defaultPort;
7144
+ const requestOptions = {
7145
+ hostname: target.hostname,
7146
+ port,
7147
+ path: `${target.pathname}${target.search}`,
7148
+ method,
7149
+ headers,
7150
+ ...tls?.ca ? { ca: tls.ca } : {},
7151
+ lookup: (hostname, _options, callback) => {
7152
+ const entries = addresses.map((address) => ({
7153
+ address,
7154
+ family: address.includes(":") ? 6 : 4
7155
+ }));
7156
+ callback(null, entries);
7157
+ }
7158
+ };
7159
+ return new Promise((resolve22, reject) => {
7160
+ const request = isHttps ? nodeHttpsRequest(requestOptions, onResponse) : nodeHttpRequest(requestOptions, onResponse);
7161
+ const onAbort = () => {
7162
+ const error = new Error("The operation was aborted.");
7163
+ error.name = "AbortError";
7164
+ request.destroy(error);
7165
+ };
7166
+ if (signal.aborted)
7167
+ onAbort();
7168
+ else
7169
+ signal.addEventListener("abort", onAbort, { once: true });
7170
+ request.on("error", reject);
7171
+ if (body !== undefined)
7172
+ request.write(body);
7173
+ request.end();
7174
+ function onResponse(response) {
7175
+ const chunks = [];
7176
+ response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
7177
+ response.on("error", reject);
7178
+ response.on("end", () => {
7179
+ const headersRecord = {};
7180
+ for (const [name, value] of Object.entries(response.headers)) {
7181
+ if (typeof value === "string")
7182
+ headersRecord[name] = value;
7183
+ else if (Array.isArray(value))
7184
+ headersRecord[name] = value.join(", ");
7185
+ }
7186
+ resolve22(new Response(Buffer.concat(chunks), { status: response.statusCode ?? 200, headers: headersRecord }));
7187
+ });
7188
+ }
7189
+ });
7190
+ }
7191
+ async function dispatchValidatedWebhook(event, channel, input) {
7192
+ const { body, headers, startedAt, options } = input;
7193
+ const webhook = channel.webhook;
7194
+ if (!webhook)
7195
+ throw new Error(`Channel ${channel.id} has no webhook config`);
7196
+ const policy = options.webhookTargetPolicy ?? {};
7197
+ const maxRedirects = normalizeMaxRedirects(policy.maxRedirects);
7198
+ const controller = new AbortController;
7199
+ const timeout = setTimeout(() => controller.abort(), webhook.timeoutMs ?? 15000);
7200
+ try {
7201
+ let target = new URL(normalizeWebhookUrl(webhook.url));
7202
+ let requestHeaders = headers;
7203
+ let method = "POST";
7204
+ let requestBody = body;
7205
+ let redirectsFollowed = 0;
7206
+ for (;; ) {
7207
+ const resolved = await resolveWebhookTarget(target, policy).catch((error) => {
7208
+ throw new Error(`Webhook target rejected by SSRF guard: ${error.message}`);
7209
+ });
7210
+ const response = options.fetchImpl ? await options.fetchImpl(target, {
7211
+ method,
7212
+ headers: requestHeaders,
7213
+ body: requestBody,
7214
+ signal: controller.signal,
7215
+ redirect: "manual"
7216
+ }) : await pinnedNativeRequest(target, resolved.addresses, method, requestHeaders, requestBody, controller.signal, options.tls);
7217
+ const location = response.headers.get("location");
7218
+ if (isRedirectStatus(response.status) && location) {
7219
+ if (redirectsFollowed >= maxRedirects) {
7220
+ return failedAttempt(startedAt, `Webhook target exceeded ${maxRedirects} redirects`);
7221
+ }
7222
+ redirectsFollowed += 1;
7223
+ const next = new URL(location, target);
7224
+ target = next;
7225
+ if (!redirectKeepsBody(response.status)) {
7226
+ method = "GET";
7227
+ requestBody = undefined;
7228
+ requestHeaders = Object.fromEntries(Object.entries(requestHeaders).filter(([name]) => name.toLowerCase() !== "content-type" && name.toLowerCase() !== "content-length"));
7229
+ }
7230
+ continue;
7231
+ }
7232
+ const responseBody = truncate(await response.text());
7233
+ return {
7234
+ attempt: 1,
7235
+ status: response.ok ? "success" : "failed",
7236
+ startedAt,
7237
+ completedAt: now2(),
7238
+ responseStatus: response.status,
7239
+ responseBody,
7240
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
7241
+ };
7242
+ }
7243
+ } catch (error) {
7244
+ return {
7245
+ attempt: 1,
7246
+ status: "failed",
7247
+ startedAt,
7248
+ completedAt: now2(),
7249
+ error: error instanceof Error ? error.message : String(error)
7250
+ };
7251
+ } finally {
7252
+ clearTimeout(timeout);
7253
+ }
7254
+ }
6726
7255
  function failedAttempt(startedAt, error) {
6727
7256
  return {
6728
7257
  attempt: 1,
@@ -6737,7 +7266,7 @@ async function dispatchCommand(event, channel) {
6737
7266
  throw new Error(`Channel ${channel.id} has no command config`);
6738
7267
  const startedAt = now2();
6739
7268
  const eventJson = JSON.stringify(event);
6740
- const env = {
7269
+ const env2 = {
6741
7270
  ...process.env,
6742
7271
  ...channel.command.env,
6743
7272
  HASNA_CHANNEL_ID: channel.id,
@@ -6751,10 +7280,10 @@ async function dispatchCommand(event, channel) {
6751
7280
  HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
6752
7281
  HASNA_EVENT_JSON: eventJson
6753
7282
  };
6754
- return new Promise((resolve8) => {
7283
+ return new Promise((resolve22) => {
6755
7284
  const child = spawn(channel.command.command, channel.command.args ?? [], {
6756
7285
  cwd: channel.command.cwd,
6757
- env,
7286
+ env: env2,
6758
7287
  stdio: ["pipe", "pipe", "pipe"]
6759
7288
  });
6760
7289
  let stdout = "";
@@ -6769,7 +7298,7 @@ async function dispatchCommand(event, channel) {
6769
7298
  });
6770
7299
  child.on("error", (error) => {
6771
7300
  clearTimeout(timeout);
6772
- resolve8({
7301
+ resolve22({
6773
7302
  attempt: 1,
6774
7303
  status: "failed",
6775
7304
  startedAt,
@@ -6782,7 +7311,7 @@ async function dispatchCommand(event, channel) {
6782
7311
  child.on("close", (code, signal) => {
6783
7312
  clearTimeout(timeout);
6784
7313
  const success = code === 0;
6785
- resolve8({
7314
+ resolve22({
6786
7315
  attempt: 1,
6787
7316
  status: success ? "success" : "failed",
6788
7317
  startedAt,
@@ -6918,7 +7447,9 @@ class EventsClient {
6918
7447
  this.transportOptions = {
6919
7448
  fetchImpl: options.fetchImpl,
6920
7449
  secretResolver: options.secretResolver,
6921
- now: options.now
7450
+ now: options.now,
7451
+ tls: options.tls,
7452
+ webhookTargetPolicy: options.webhookTargetPolicy
6922
7453
  };
6923
7454
  this.catalog = options.catalog ?? defaultEventTypeCatalog;
6924
7455
  this.validateCatalogTypes = options.validateCatalogTypes ?? false;
@@ -7123,9 +7654,51 @@ function normalizeRetryPolicy(policy) {
7123
7654
  multiplier: Math.max(1, policy?.multiplier ?? 2)
7124
7655
  };
7125
7656
  }
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(() => {
7657
+ 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) => {
7658
+ return dnsLookup(hostname, { all: true, verbatim: false });
7659
+ }, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES;
7660
+ var init_dist = __esm(() => {
7661
+ KIND_ENV = {
7662
+ config: "HASNA_CONFIG_HOME",
7663
+ data: "HASNA_DATA_HOME",
7664
+ state: "HASNA_STATE_HOME",
7665
+ cache: "HASNA_CACHE_HOME"
7666
+ };
7667
+ APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
7128
7668
  DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
7669
+ IPV4_PRIVATE_RANGES = [
7670
+ [0, 16777215],
7671
+ [167772160, 184549375],
7672
+ [1681915904, 1686110207],
7673
+ [2130706432, 2147483647],
7674
+ [2851995648, 2852061183],
7675
+ [2886729728, 2887778303],
7676
+ [3221225472, 3221225727],
7677
+ [3221225984, 3221226239],
7678
+ [3227017984, 3227018239],
7679
+ [3232235520, 3232301055],
7680
+ [3323068416, 3323199487],
7681
+ [3325256704, 3325256959],
7682
+ [3405803776, 3405804031],
7683
+ [3758096384, 4294967295]
7684
+ ];
7685
+ IPV6_SPECIAL_PREFIXES = [
7686
+ { groups: [0, 0, 0, 0, 0, 0, 0, 0], bits: 128 },
7687
+ { groups: [0, 0, 0, 0, 0, 0, 0, 1], bits: 128 },
7688
+ { groups: [0, 0, 0, 0, 0, 65535, 0, 0], bits: 96 },
7689
+ { groups: [100, 65435, 0, 0, 0, 0, 0, 0], bits: 96 },
7690
+ { groups: [256, 0, 0, 0, 0, 0, 0, 0], bits: 64 },
7691
+ { groups: [8193, 0, 0, 0, 0, 0, 0, 0], bits: 32 },
7692
+ { groups: [8193, 2, 0, 0, 0, 0, 0, 0], bits: 48 },
7693
+ { groups: [8193, 16, 0, 0, 0, 0, 0, 0], bits: 28 },
7694
+ { groups: [8193, 3512, 0, 0, 0, 0, 0, 0], bits: 32 },
7695
+ { groups: [8194, 0, 0, 0, 0, 0, 0, 0], bits: 16 },
7696
+ { groups: [16383, 0, 0, 0, 0, 0, 0, 0], bits: 20 },
7697
+ { groups: [64512, 0, 0, 0, 0, 0, 0, 0], bits: 7 },
7698
+ { groups: [65152, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
7699
+ { groups: [65216, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
7700
+ { groups: [65280, 0, 0, 0, 0, 0, 0, 0], bits: 8 }
7701
+ ];
7129
7702
  EventValidationError = class EventValidationError extends Error {
7130
7703
  eventType;
7131
7704
  issues;
@@ -7149,6 +7722,8 @@ function rowToTaskList(row) {
7149
7722
  };
7150
7723
  }
7151
7724
  function createTaskList(input, db) {
7725
+ if (input.status !== undefined)
7726
+ throw new Error("The explicit SQLite storage cannot retain task-list status; use the shared API");
7152
7727
  const d = db || getDatabase();
7153
7728
  return d.transaction(() => {
7154
7729
  const id = uuid();
@@ -7190,6 +7765,8 @@ function listTaskLists(projectId, db) {
7190
7765
  return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
7191
7766
  }
7192
7767
  function updateTaskList(id, input, db) {
7768
+ if (input.status !== undefined)
7769
+ throw new Error("The explicit SQLite storage cannot retain task-list status; use the shared API");
7193
7770
  const d = db || getDatabase();
7194
7771
  return d.transaction(() => {
7195
7772
  const existing = getTaskList(id, d);
@@ -7547,7 +8124,7 @@ function emitSharedTaskEventQuiet(input) {
7547
8124
  }
7548
8125
  var SOURCE = "todos";
7549
8126
  var init_shared_events = __esm(() => {
7550
- init_dist2();
8127
+ init_dist();
7551
8128
  init_database();
7552
8129
  init_projects();
7553
8130
  init_task_lists();
@@ -10132,7 +10709,9 @@ function listTasks(filter = {}, db) {
10132
10709
  params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
10133
10710
  } catch {}
10134
10711
  }
10135
- if (!filter.include_archived) {
10712
+ if (filter.archived_only) {
10713
+ conditions.push("archived_at IS NOT NULL");
10714
+ } else if (!filter.include_archived) {
10136
10715
  conditions.push("archived_at IS NULL");
10137
10716
  }
10138
10717
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
@@ -10145,7 +10724,8 @@ function listTasks(filter = {}, db) {
10145
10724
  params.push(filter.offset);
10146
10725
  }
10147
10726
  }
10148
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC, id ASC${limitClause}`).all(...params);
10727
+ const orderBy = filter.archived_only ? "archived_at DESC, id DESC" : `${PRIORITY_RANK}, created_at DESC, id ASC`;
10728
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${orderBy}${limitClause}`).all(...params);
10149
10729
  return rows.map(rowToTask);
10150
10730
  }
10151
10731
  function getTaskByFingerprint(fingerprint, db) {
@@ -10288,7 +10868,9 @@ function countTasks(filter = {}, db) {
10288
10868
  }
10289
10869
  }
10290
10870
  addMetadataConditions(filter.metadata, conditions, params);
10291
- if (!filter.include_archived) {
10871
+ if (filter.archived_only) {
10872
+ conditions.push("archived_at IS NOT NULL");
10873
+ } else if (!filter.include_archived) {
10292
10874
  conditions.push("archived_at IS NULL");
10293
10875
  }
10294
10876
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
@@ -10425,6 +11007,10 @@ function updateTaskStored(id, input, db) {
10425
11007
  sets.push("completed_at = ?");
10426
11008
  params.push(input.completed_at);
10427
11009
  }
11010
+ if (input.archived_at !== undefined) {
11011
+ sets.push("archived_at = ?");
11012
+ params.push(input.archived_at);
11013
+ }
10428
11014
  if (input.confidence !== undefined) {
10429
11015
  sets.push("confidence = ?");
10430
11016
  params.push(input.confidence);
@@ -10488,6 +11074,8 @@ function updateTaskStored(id, input, db) {
10488
11074
  logTaskChange(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
10489
11075
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
10490
11076
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
11077
+ if (input.archived_at !== undefined && input.archived_at !== task.archived_at)
11078
+ logTaskChange(id, "update", "archived_at", task.archived_at, input.archived_at, agentId, d);
10491
11079
  if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
10492
11080
  logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
10493
11081
  if (input.approved_by !== undefined)
@@ -11188,7 +11776,13 @@ function resolveCreateSlug(input, projectId, db) {
11188
11776
  }
11189
11777
  return nextPlanSlug(planSlugBase2(input.name), projectId, db);
11190
11778
  }
11779
+ function assertLegacyPlanInput(input) {
11780
+ if (input.start_date !== undefined || input.end_date !== undefined || input.status === "planning" || input.status === "cancelled") {
11781
+ throw new Error("Plan calendar dates and planning/cancelled statuses require the shared Todos API; explicit SQLite storage cannot retain them");
11782
+ }
11783
+ }
11191
11784
  function createPlan(input, db) {
11785
+ assertLegacyPlanInput(input);
11192
11786
  const d = db || getDatabase();
11193
11787
  const id = uuid();
11194
11788
  const timestamp2 = now();
@@ -11269,6 +11863,7 @@ function updatePlanStored(id, input, db) {
11269
11863
  return updated;
11270
11864
  }
11271
11865
  function updatePlan(id, input, db) {
11866
+ assertLegacyPlanInput(input);
11272
11867
  const d = db || getDatabase();
11273
11868
  return d.transaction(() => {
11274
11869
  guardPlanRowsSqlite([id], d);
@@ -11743,27 +12338,27 @@ var init_boards = __esm(() => {
11743
12338
  // src/lib/artifact-store.ts
11744
12339
  import { createHash as createHash3 } from "crypto";
11745
12340
  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";
12341
+ import { basename, dirname as dirname4, join as join6, resolve as resolve9 } from "path";
11747
12342
  import { tmpdir as tmpdir2 } from "os";
11748
12343
  function isInMemoryDb2(path) {
11749
12344
  return path === ":memory:" || path.startsWith("file::memory:");
11750
12345
  }
11751
12346
  function artifactStoreRoot() {
11752
12347
  if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
11753
- return resolve8(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
12348
+ return resolve9(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
11754
12349
  if (process.env["TODOS_ARTIFACTS_DIR"])
11755
- return resolve8(process.env["TODOS_ARTIFACTS_DIR"]);
12350
+ return resolve9(process.env["TODOS_ARTIFACTS_DIR"]);
11756
12351
  const dbPath = getDatabasePath();
11757
12352
  if (isInMemoryDb2(dbPath))
11758
- return join7(tmpdir2(), "hasna-todos-artifacts");
11759
- return join7(dirname4(resolve8(dbPath)), "artifacts");
12353
+ return join6(tmpdir2(), "hasna-todos-artifacts");
12354
+ return join6(dirname4(resolve9(dbPath)), "artifacts");
11760
12355
  }
11761
12356
  function artifactStorePath(relativePath) {
11762
12357
  const normalized = relativePath.replace(/\\/g, "/");
11763
12358
  if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
11764
12359
  throw new Error("Invalid artifact store path");
11765
12360
  }
11766
- return join7(artifactStoreRoot(), normalized);
12361
+ return join6(artifactStoreRoot(), normalized);
11767
12362
  }
11768
12363
  function sha256(buffer) {
11769
12364
  return createHash3("sha256").update(buffer).digest("hex");
@@ -11803,7 +12398,7 @@ function mediaTypeFor(path, textLike) {
11803
12398
  return "application/octet-stream";
11804
12399
  }
11805
12400
  function storeArtifactContent(input) {
11806
- const sourcePath = resolve8(input.path);
12401
+ const sourcePath = resolve9(input.path);
11807
12402
  if (!existsSync8(sourcePath))
11808
12403
  return null;
11809
12404
  const sourceStat = statSync2(sourcePath);
@@ -11821,7 +12416,7 @@ function storeArtifactContent(input) {
11821
12416
  redactionStatus = "redacted";
11822
12417
  }
11823
12418
  const storedSha = sha256(storedBuffer);
11824
- const relativePath = join7("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
12419
+ const relativePath = join6("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
11825
12420
  const destination = artifactStorePath(relativePath);
11826
12421
  if (!existsSync8(destination)) {
11827
12422
  mkdirSync4(dirname4(destination), { recursive: true });
@@ -11958,19 +12553,19 @@ function importStoredArtifactContent(content) {
11958
12553
  }
11959
12554
  function getArtifactStoreRoot(dbPath) {
11960
12555
  if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
11961
- return resolve8(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
12556
+ return resolve9(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
11962
12557
  if (process.env["TODOS_ARTIFACTS_DIR"])
11963
- return resolve8(process.env["TODOS_ARTIFACTS_DIR"]);
12558
+ return resolve9(process.env["TODOS_ARTIFACTS_DIR"]);
11964
12559
  const path = dbPath ?? getDatabasePath();
11965
12560
  if (isInMemoryDb2(path))
11966
- return join7(tmpdir2(), "hasna-todos-artifacts");
11967
- return join7(dirname4(resolve8(path)), "artifacts");
12561
+ return join6(tmpdir2(), "hasna-todos-artifacts");
12562
+ return join6(dirname4(resolve9(path)), "artifacts");
11968
12563
  }
11969
12564
  function computeContentHash(path) {
11970
- return sha256(readFileSync4(resolve8(path)));
12565
+ return sha256(readFileSync4(resolve9(path)));
11971
12566
  }
11972
12567
  function storeArtifactFile(input) {
11973
- const sourcePath = resolve8(input.sourcePath);
12568
+ const sourcePath = resolve9(input.sourcePath);
11974
12569
  if (!existsSync8(sourcePath)) {
11975
12570
  throw new Error(`Source file not found: ${input.sourcePath}`);
11976
12571
  }
@@ -11984,7 +12579,7 @@ function storeArtifactFile(input) {
11984
12579
  let localPath = sourcePath;
11985
12580
  if (storageMode === "copy") {
11986
12581
  const fileName = input.name && input.name.trim().length > 0 ? basename(input.name) : basename(sourcePath);
11987
- const destination = join7(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
12582
+ const destination = join6(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
11988
12583
  mkdirSync4(dirname4(destination), { recursive: true });
11989
12584
  writeFileSync2(destination, buffer);
11990
12585
  localPath = destination;
@@ -12019,7 +12614,7 @@ function buildArtifactExportManifest(artifacts, dbPath) {
12019
12614
  };
12020
12615
  }
12021
12616
  function writeArtifactExportManifest(manifest, outputPath) {
12022
- const destination = resolve8(outputPath);
12617
+ const destination = resolve9(outputPath);
12023
12618
  mkdirSync4(dirname4(destination), { recursive: true });
12024
12619
  writeFileSync2(destination, `${JSON.stringify(manifest, null, 2)}
12025
12620
  `);
@@ -12707,7 +13302,9 @@ function finishTaskRunTransaction(input, db) {
12707
13302
  const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
12708
13303
  const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
12709
13304
  if (!run) {
12710
- throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
13305
+ if (input.run_id)
13306
+ throw new InputValidationError(`Run not found: ${input.run_id}`, "Pass an existing run id, or --key.");
13307
+ throw new InputValidationError("runs finish requires a run id or --key", "Pass run_id, or key with task_id.");
12711
13308
  }
12712
13309
  if (input.task_id && run.task_id !== input.task_id) {
12713
13310
  throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
@@ -13176,7 +13773,7 @@ var init_tasks = __esm(() => {
13176
13773
  // package.json
13177
13774
  var package_default = {
13178
13775
  name: "@hasna/todos",
13179
- version: "0.15.52",
13776
+ version: "0.17.0",
13180
13777
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
13181
13778
  type: "module",
13182
13779
  main: "dist/index.js",
@@ -13229,37 +13826,40 @@ var package_default = {
13229
13826
  }
13230
13827
  },
13231
13828
  workspaces: [
13232
- "dashboard",
13233
13829
  "ai"
13234
13830
  ],
13235
13831
  files: [
13236
13832
  "dist",
13237
- "dashboard/dist",
13238
13833
  "postinstall.js",
13239
13834
  "LICENSE",
13240
- "README.md"
13835
+ "README.md",
13836
+ "CHANGELOG.md",
13837
+ "docs/PLAN_API.md",
13838
+ "docs/TASK_LIST_API.md",
13839
+ "docs/TEMPLATE_API.md",
13840
+ "docs/TASK_QUERY_API.md",
13841
+ "docs/native-storage.md"
13241
13842
  ],
13242
13843
  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",
13844
+ 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
13845
  "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
13846
  "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
13847
  migrate: "bun run src/server/index.ts migrate",
13247
13848
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
13248
13849
  "generate:sdk": "bun run scripts/generate-sdk.ts",
13249
- "build:dashboard": "cd dashboard && bun install --frozen-lockfile && bun run build",
13250
13850
  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",
13851
+ test: "bun test --timeout 120000",
13852
+ "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
13853
  "dev:cli": "bun run src/cli/index.tsx",
13254
13854
  "dev:mcp": "bun run src/mcp/index.ts",
13255
13855
  "dev:serve": "bun run src/server/index.ts",
13256
13856
  "verify:release": "bun run scripts/verify-public-release.ts --mode=review",
13257
13857
  "verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
13258
13858
  "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",
13859
+ "test:attested-container-candidate": "bun test --timeout 120000 scripts/attested-container-candidate.test.ts",
13260
13860
  "emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
13261
13861
  "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",
13862
+ "test:attested-container-compatibility-vector": "bun test --timeout 120000 scripts/attested-container-compatibility-vector.test.ts",
13263
13863
  "issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
13264
13864
  prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
13265
13865
  postinstall: "node postinstall.js"
@@ -13283,11 +13883,12 @@ var package_default = {
13283
13883
  },
13284
13884
  repository: {
13285
13885
  type: "git",
13286
- url: "https://github.com/hasna/todos.git"
13886
+ url: "https://github.com/hasna/apps.git",
13887
+ directory: "apps/todos"
13287
13888
  },
13288
- homepage: "https://github.com/hasna/todos",
13889
+ homepage: "https://github.com/hasna/apps/tree/main/apps/todos#readme",
13289
13890
  bugs: {
13290
- url: "https://github.com/hasna/todos/issues"
13891
+ url: "https://github.com/hasna/apps/issues"
13291
13892
  },
13292
13893
  engines: {
13293
13894
  bun: ">=1.0.0"
@@ -13296,9 +13897,8 @@ var package_default = {
13296
13897
  author: "Andrei Hasna <andrei@hasna.com>",
13297
13898
  license: "Apache-2.0",
13298
13899
  dependencies: {
13299
- "@hasna/contracts": "0.14.2",
13900
+ "@hasna/contracts": "1.0.2",
13300
13901
  "@hasna/events": "^0.1.11",
13301
- "@hasna/paths": "0.1.0",
13302
13902
  "@modelcontextprotocol/sdk": "^1.12.1",
13303
13903
  chalk: "^5.4.1",
13304
13904
  commander: "^13.1.0",
@@ -13327,6 +13927,7 @@ function getPackageVersion(_fromUrl = import.meta.url) {
13327
13927
  }
13328
13928
 
13329
13929
  // src/mcp/token-utils.ts
13930
+ init_env();
13330
13931
  var CORE_MCP_TOOLS = new Set([
13331
13932
  "add_comment",
13332
13933
  "bootstrap",
@@ -13708,7 +14309,7 @@ var MCP_TOOL_GROUPS = {
13708
14309
  "machines_topology",
13709
14310
  "machines_unarchive"
13710
14311
  ],
13711
- maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
14312
+ maintenance: ["get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task"]
13712
14313
  };
13713
14314
  var MCP_PROFILE_GROUPS = {
13714
14315
  minimal: ["core", "loops"],
@@ -13728,7 +14329,7 @@ function addGroupTools(toolNames, groupName) {
13728
14329
  toolNames.add(tool);
13729
14330
  return true;
13730
14331
  }
13731
- function shouldRegisterToolForProfile(name, profileValue = process.env["TODOS_PROFILE"], groupValue = process.env["TODOS_TOOL_GROUPS"]) {
14332
+ function shouldRegisterToolForProfile(name, profileValue = env.profile(), groupValue = env.toolGroups()) {
13732
14333
  const profileTokens = splitTokens(profileValue || "minimal");
13733
14334
  const groupTokens = splitTokens(groupValue);
13734
14335
  if (profileTokens.includes("full") || profileTokens.includes("all"))
@@ -13800,8 +14401,8 @@ function createMcpManifest(options = {}) {
13800
14401
  name: "todos",
13801
14402
  binary: "todos-mcp",
13802
14403
  transport: "stdio",
13803
- profileEnvironmentVariable: "TODOS_PROFILE",
13804
- groupEnvironmentVariable: "TODOS_TOOL_GROUPS"
14404
+ profileEnvironmentVariable: "HASNA_TODOS_PROFILE",
14405
+ groupEnvironmentVariable: "HASNA_TODOS_TOOL_GROUPS"
13805
14406
  },
13806
14407
  groups: MCP_TOOL_GROUPS,
13807
14408
  profiles: MCP_PROFILE_GROUPS,
@@ -14781,7 +15382,7 @@ var TODOS_JSON_CONTRACTS = [
14781
15382
  contract({
14782
15383
  id: "tester_issue_report",
14783
15384
  name: "Tester Issue Report",
14784
- description: "Generic issue report payload emitted by @hasna/testers or compatible test runners for conversion into local tasks.",
15385
+ description: "Generic issue report payload emitted by the testers QA CLI or compatible test runners for conversion into local tasks.",
14785
15386
  surfaces: ["cli", "sdk"],
14786
15387
  stability: "stable",
14787
15388
  required: {
@@ -16206,14 +16807,15 @@ function normalizeApprovalRefs(values) {
16206
16807
  function resolveTodosAiCommandOptions(input) {
16207
16808
  const cli = input.cli ?? {};
16208
16809
  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);
16810
+ const env2 = input.env ?? {};
16811
+ const envFormat = env2["HASNA_TODOS_AI_FORMAT"] ?? env2["TODOS_AI_FORMAT"];
16812
+ const format = parseEnum(selectedString(cli.format, envFormat, config.format), TODOS_AI_FORMATS, "format", TODOS_AI_DEFAULTS.format);
16813
+ 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
16814
  if (cli.dryRun && cli.writeMode === "execute") {
16213
16815
  throw new TodosAiContractError("invalid_configuration", "--dry-run cannot be combined with --write-mode execute");
16214
16816
  }
16215
16817
  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);
16818
+ const explicitApproval = cli.dryRun ? selectedString(cli.approvalMode) : selectedString(cli.approvalMode, env2["TODOS_AI_APPROVAL_MODE"], config.approval_mode);
16217
16819
  const defaultApproval = writeMode === "execute" ? input.interactive ? "prompt" : "required" : "deny";
16218
16820
  const approvalMode = cli.dryRun ? parseEnum(explicitApproval, TODOS_AI_APPROVAL_MODES, "approval mode", "deny") : parseEnum(explicitApproval, TODOS_AI_APPROVAL_MODES, "approval mode", defaultApproval);
16219
16821
  const approvalRefs = normalizeApprovalRefs(cli.approvalRefs);
@@ -16236,12 +16838,12 @@ function resolveTodosAiCommandOptions(input) {
16236
16838
  throw new TodosAiContractError("invalid_configuration", "--approval references require --approval-mode existing");
16237
16839
  }
16238
16840
  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),
16841
+ provider: selectedString(cli.provider, env2["TODOS_AI_PROVIDER"], config.provider),
16842
+ model: selectedString(cli.model, env2["TODOS_AI_MODEL"], config.model),
16843
+ profile: selectedString(cli.profile, env2["TODOS_AI_PROFILE"], config.profile),
16242
16844
  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),
16845
+ 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),
16846
+ 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
16847
  write_mode: writeMode,
16246
16848
  approval_mode: approvalMode,
16247
16849
  approval_refs: approvalRefs,
@@ -16539,7 +17141,7 @@ function todosAiExitCodeForResult(result) {
16539
17141
  }
16540
17142
  // src/lib/onboarding-fixtures.ts
16541
17143
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
16542
- import { join as join8 } from "path";
17144
+ import { join as join7 } from "path";
16543
17145
 
16544
17146
  // src/lib/local-bridge.ts
16545
17147
  init_database();
@@ -17476,7 +18078,7 @@ function writeOnboardingFixtureFiles(directory) {
17476
18078
  mkdirSync5(directory, { recursive: true });
17477
18079
  const files = [];
17478
18080
  for (const fixture of allFixtures()) {
17479
- const path = join8(directory, `${fixture.summary.name}.bridge.json`);
18081
+ const path = join7(directory, `${fixture.summary.name}.bridge.json`);
17480
18082
  writeFileSync3(path, `${JSON.stringify(fixture.bundle, null, 2)}
17481
18083
  `, "utf-8");
17482
18084
  files.push(path);
@@ -17494,7 +18096,7 @@ function importOnboardingFixture(options = {}) {
17494
18096
  init_database();
17495
18097
  import { createHash as createHash4 } from "crypto";
17496
18098
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
17497
- import { dirname as dirname5, resolve as resolve9 } from "path";
18099
+ import { dirname as dirname5, resolve as resolve10 } from "path";
17498
18100
  import { mkdirSync as mkdirSync6 } from "fs";
17499
18101
  var TODOS_LOCAL_BACKUP_KIND = "hasna.todos.local-backup";
17500
18102
  var TODOS_LOCAL_BACKUP_SCHEMA_VERSION = 1;
@@ -17601,14 +18203,14 @@ function createLocalBackup(options = {}, db) {
17601
18203
  return backup;
17602
18204
  }
17603
18205
  function writeLocalBackupFile(backup, outputPath) {
17604
- const path = resolve9(outputPath);
18206
+ const path = resolve10(outputPath);
17605
18207
  mkdirSync6(dirname5(path), { recursive: true });
17606
18208
  writeFileSync4(path, `${JSON.stringify(backup, null, 2)}
17607
18209
  `);
17608
18210
  return path;
17609
18211
  }
17610
18212
  function readLocalBackupFile(path) {
17611
- return JSON.parse(readFileSync5(resolve9(path), "utf-8"));
18213
+ return JSON.parse(readFileSync5(resolve10(path), "utf-8"));
17612
18214
  }
17613
18215
  function verifyLocalBackup(value, options = {}, db) {
17614
18216
  const verifiedAt = options.verified_at ?? now();
@@ -18302,7 +18904,7 @@ function renderLocalSnapshotMarkdown(snapshot) {
18302
18904
  }
18303
18905
  // src/lib/sdk-integration-fixtures.ts
18304
18906
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
18305
- import { join as join9 } from "path";
18907
+ import { join as join8 } from "path";
18306
18908
 
18307
18909
  // src/cli-mcp-parity.ts
18308
18910
  function source4(version) {
@@ -19132,17 +19734,23 @@ var TODOS_CLI_MCP_PARITY = [
19132
19734
  "todos extract",
19133
19735
  "todos extract-watch"
19134
19736
  ],
19135
- mcpTools: [
19136
- "extract_todos",
19137
- "watch_source_todos"
19138
- ],
19737
+ mcpTools: [],
19139
19738
  jsonContracts: ["source_code_index", "source_todo_comment", "task", "structured_error", "api_error"],
19140
19739
  errorContracts: ["structured_error", "api_error"],
19141
- status: "matched",
19142
- intentionalGaps: [],
19740
+ status: "intentional-gap",
19741
+ intentionalGaps: [
19742
+ {
19743
+ cliCommand: "todos extract",
19744
+ reason: "The source TODO index scans the local checkout the CLI runs in; the former extract_todos MCP tool read the agent host's filesystem behind a task-store tool surface and had no hosted arm, so it was removed (fleet alignment 2026-09-11)."
19745
+ },
19746
+ {
19747
+ cliCommand: "todos extract-watch",
19748
+ reason: "A polling filesystem watcher is a long-running local process, not an MCP tool call; the former watch_source_todos tool was removed with extract_todos \u2014 run `todos extract-watch` from the checkout instead."
19749
+ }
19750
+ ],
19751
+ gapReason: "Source scanning and watching are local-filesystem CLI operations; no MCP tool mapping.",
19143
19752
  example: {
19144
- cli: "todos extract . --dry-run --index --json",
19145
- mcpTool: "extract_todos"
19753
+ cli: "todos extract . --dry-run --index --json"
19146
19754
  }
19147
19755
  },
19148
19756
  {
@@ -20273,7 +20881,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
20273
20881
  ];
20274
20882
  const written = [];
20275
20883
  for (const [name, payload] of files) {
20276
- const file = join9(directory, name);
20884
+ const file = join8(directory, name);
20277
20885
  writeFileSync5(file, `${JSON.stringify(payload, null, 2)}
20278
20886
  `, "utf-8");
20279
20887
  written.push(file);
@@ -21556,11 +22164,11 @@ function renderLocalAuditLedgerMarkdown(ledger) {
21556
22164
  init_migrations();
21557
22165
  init_schema();
21558
22166
  import { readFileSync as readFileSync6 } from "fs";
21559
- import { join as join10, resolve as resolve10 } from "path";
22167
+ import { join as join9, resolve as resolve11 } from "path";
21560
22168
  import { Database as Database2 } from "bun:sqlite";
21561
22169
  var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1;
21562
22170
  var EXPECTED_PACKAGE_NAME = "@hasna/todos";
21563
- var EXPECTED_REPOSITORY = "https://github.com/hasna/todos.git";
22171
+ var EXPECTED_REPOSITORY = "https://github.com/hasna/apps.git";
21564
22172
  var EXPECTED_BINS = ["todos", "todos-mcp", "todos-serve"];
21565
22173
  var EXPECTED_EXPORTS = [".", "./sdk", "./mcp", "./registry", "./contracts", "./storage"];
21566
22174
  var REQUIRED_SCRIPTS = ["build", "test:no-cloud", "verify:release", "prepublishOnly"];
@@ -21602,7 +22210,7 @@ function warn(id, message, details) {
21602
22210
  return { id, status: "warning", message, details };
21603
22211
  }
21604
22212
  function readPackageJson(root) {
21605
- return JSON.parse(readFileSync6(join10(root, "package.json"), "utf8"));
22213
+ return JSON.parse(readFileSync6(join9(root, "package.json"), "utf8"));
21606
22214
  }
21607
22215
  function sortedKeys(value) {
21608
22216
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -21664,7 +22272,7 @@ function checkPackage(packageJson) {
21664
22272
  const checks = [];
21665
22273
  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
22274
  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 }));
22275
+ 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
22276
  const dependencyNames = Object.keys(packageJson.dependencies ?? {});
21669
22277
  const privateStudio = `hasna${"studio"}`;
21670
22278
  const privatePlatform = `platform${"-"}todos`;
@@ -21698,7 +22306,7 @@ function checkChangelog() {
21698
22306
  ];
21699
22307
  }
21700
22308
  function createReleaseCompatibilityReport(options = {}) {
21701
- const root = resolve10(options.root ?? process.cwd());
22309
+ const root = resolve11(options.root ?? process.cwd());
21702
22310
  const packageJson = readPackageJson(root);
21703
22311
  const simulatedLevels = options.simulated_levels ?? defaultSimulationLevels();
21704
22312
  const checks = [
@@ -24028,22 +24636,22 @@ function millisBetween(start, end) {
24028
24636
  return 0;
24029
24637
  return endMs - startMs;
24030
24638
  }
24031
- function addTaskScope(where, params, options, db, alias = "t") {
24639
+ function addTaskScope(where, params, options, db, alias2 = "t") {
24032
24640
  if (options.project_id) {
24033
- where.push(`${alias}.project_id = ?`);
24641
+ where.push(`${alias2}.project_id = ?`);
24034
24642
  params.push(options.project_id);
24035
24643
  }
24036
24644
  if (options.agent_id) {
24037
24645
  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})`);
24646
+ const assignedInClause = lowerInClause(`${alias2}.assigned_to`, resolveAssignedToAliases(db, options.agent_id), params);
24647
+ where.push(`(${alias2}.agent_id = ? OR ${assignedInClause})`);
24040
24648
  }
24041
24649
  if (options.since) {
24042
- where.push(`${alias}.created_at >= ?`);
24650
+ where.push(`${alias2}.created_at >= ?`);
24043
24651
  params.push(options.since);
24044
24652
  }
24045
24653
  if (options.until) {
24046
- where.push(`${alias}.created_at <= ?`);
24654
+ where.push(`${alias2}.created_at <= ?`);
24047
24655
  params.push(options.until);
24048
24656
  }
24049
24657
  }
@@ -24712,16 +25320,25 @@ var DEFAULT_ENCRYPTION_KEY_ENV = "TODOS_ENCRYPTION_KEY";
24712
25320
  class EncryptionKeyUnavailableError extends Error {
24713
25321
  keyEnv;
24714
25322
  profile;
25323
+ static code = "ENCRYPTION_KEY_UNAVAILABLE";
25324
+ code = EncryptionKeyUnavailableError.code;
25325
+ suggestion;
24715
25326
  constructor(keyEnv, profile) {
24716
25327
  super(`Encryption key is locked: set ${keyEnv} to use profile ${profile}`);
24717
25328
  this.keyEnv = keyEnv;
24718
25329
  this.profile = profile;
25330
+ this.name = "EncryptionKeyUnavailableError";
25331
+ this.suggestion = `Set ${keyEnv} in the environment of the process serving this store, then retry.`;
24719
25332
  }
24720
25333
  }
24721
25334
 
24722
25335
  class EncryptedPayloadError extends Error {
25336
+ static code = "ENCRYPTED_PAYLOAD_INVALID";
25337
+ code = EncryptedPayloadError.code;
25338
+ static suggestion = "Pass a value produced by encrypt_local_value for this profile; a plain value is not an encrypted envelope.";
24723
25339
  constructor(message) {
24724
25340
  super(message);
25341
+ this.name = "EncryptedPayloadError";
24725
25342
  }
24726
25343
  }
24727
25344
  function now3() {
@@ -24793,17 +25410,17 @@ function removeEncryptionProfile(name) {
24793
25410
  saveConfig({ ...config, encryption_profiles: next });
24794
25411
  return true;
24795
25412
  }
24796
- function encryptionProfileStatus(name = DEFAULT_ENCRYPTION_PROFILE, env = process.env) {
25413
+ function encryptionProfileStatus(name = DEFAULT_ENCRYPTION_PROFILE, env2 = process.env) {
24797
25414
  const profile = ensureEncryptionProfile(name);
24798
25415
  return {
24799
25416
  profile: redactValue(profile),
24800
- locked: !env[profile.key_env],
25417
+ locked: !env2[profile.key_env],
24801
25418
  key_env: profile.key_env,
24802
- key_present: Boolean(env[profile.key_env])
25419
+ key_present: Boolean(env2[profile.key_env])
24803
25420
  };
24804
25421
  }
24805
- function keyForProfile(profile, env) {
24806
- const secret = env[profile.key_env];
25422
+ function keyForProfile(profile, env2) {
25423
+ const secret = env2[profile.key_env];
24807
25424
  if (!secret)
24808
25425
  throw new EncryptionKeyUnavailableError(profile.key_env, profile.name);
24809
25426
  return deriveKey(secret, profile.salt);
@@ -24834,7 +25451,7 @@ function isEncryptedValue(value) {
24834
25451
  const record = value && typeof value === "object" && !Array.isArray(value) ? value : null;
24835
25452
  return Boolean(record && record.kind === TODOS_ENCRYPTED_VALUE_KIND && record.schemaVersion === TODOS_ENCRYPTION_SCHEMA_VERSION);
24836
25453
  }
24837
- function decryptString(envelope, env = process.env) {
25454
+ function decryptString(envelope, env2 = process.env) {
24838
25455
  if (!isEncryptedValue(envelope))
24839
25456
  throw new EncryptedPayloadError("value is not a hasna/todos encrypted envelope");
24840
25457
  const profile = profileFromConfig(envelope.profile) ?? {
@@ -24844,7 +25461,7 @@ function decryptString(envelope, env = process.env) {
24844
25461
  key_env: envelope.key_env,
24845
25462
  salt: envelope.salt
24846
25463
  };
24847
- const key = keyForProfile(profile, env);
25464
+ const key = keyForProfile(profile, env2);
24848
25465
  const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(envelope.iv, "base64"));
24849
25466
  decipher.setAuthTag(Buffer.from(envelope.auth_tag, "base64"));
24850
25467
  const plaintext = Buffer.concat([
@@ -24861,8 +25478,8 @@ function decryptString(envelope, env = process.env) {
24861
25478
  function encryptValue(value, options = {}) {
24862
25479
  return encryptString(JSON.stringify(value), options);
24863
25480
  }
24864
- function decryptValue(envelope, env = process.env) {
24865
- return JSON.parse(decryptString(envelope, env));
25481
+ function decryptValue(envelope, env2 = process.env) {
25482
+ return JSON.parse(decryptString(envelope, env2));
24866
25483
  }
24867
25484
  function looksSensitiveKey(key) {
24868
25485
  return /api[_-]?key|token|secret|password|credential|evidence|artifact|output|summary|metadata/i.test(key);
@@ -24920,7 +25537,7 @@ function isEncryptedBridgeBundle(value) {
24920
25537
  const record = value && typeof value === "object" && !Array.isArray(value) ? value : null;
24921
25538
  return Boolean(record && record.kind === TODOS_ENCRYPTED_BRIDGE_KIND && record.schemaVersion === TODOS_ENCRYPTION_SCHEMA_VERSION);
24922
25539
  }
24923
- function decryptBridgeBundle(bundle, env = process.env) {
25540
+ function decryptBridgeBundle(bundle, env2 = process.env) {
24924
25541
  if (!isEncryptedBridgeBundle(bundle))
24925
25542
  throw new EncryptedPayloadError("bundle is not an encrypted hasna/todos bridge export");
24926
25543
  const plaintext = decryptString({
@@ -24936,7 +25553,7 @@ function decryptBridgeBundle(bundle, env = process.env) {
24936
25553
  auth_tag: bundle.encryption.auth_tag,
24937
25554
  ciphertext: bundle.encryption.ciphertext,
24938
25555
  plaintext_sha256: bundle.plaintext.sha256
24939
- }, env);
25556
+ }, env2);
24940
25557
  return JSON.parse(plaintext);
24941
25558
  }
24942
25559
  function assertExportProfileAllowed(profile) {