@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
@@ -13,7 +13,7 @@ export declare class PostgresTodosTaskManifestBackend implements TodosTaskManife
13
13
  private persistTerminal;
14
14
  apply(input: NormalizedTaskManifest, faults: PreparedTaskManifestFaults): Promise<TodosTaskManifestApplyResult>;
15
15
  readExact(receiptId: string): Promise<TodosTaskManifestApplyResult>;
16
- lookupBindingByPlanId(planId: string): Promise<Omit<import("./types.js").TodosTaskManifestBindingLookupResult, "schema_version" | "authority" | "route" | "tenant_id">>;
16
+ lookupBindingByPlanId(planId: string): Promise<Omit<import("./types.js").TodosTaskManifestBindingLookupResult, "authority" | "route" | "tenant_id" | "schema_version">>;
17
17
  markOutboxDelivered(outboxId: string, deliveredAt: string): Promise<void>;
18
18
  compensate(input: TodosTaskManifestCompensateRequest, receipt: TodosTaskManifestReceipt, compensationReceiptId: string, requestDigest: string, now: string): Promise<TodosTaskManifestCompensationResult>;
19
19
  private readback;
@@ -10,7 +10,7 @@ export declare class SqliteTodosTaskManifestBackend implements TodosTaskManifest
10
10
  apply(input: NormalizedTaskManifest, faults: PreparedTaskManifestFaults): Promise<TodosTaskManifestApplyResult>;
11
11
  private persistTerminal;
12
12
  readExact(receiptId: string): Promise<TodosTaskManifestApplyResult>;
13
- lookupBindingByPlanId(planId: string): Promise<Omit<import("./types.js").TodosTaskManifestBindingLookupResult, "schema_version" | "authority" | "route" | "tenant_id">>;
13
+ lookupBindingByPlanId(planId: string): Promise<Omit<import("./types.js").TodosTaskManifestBindingLookupResult, "authority" | "route" | "tenant_id" | "schema_version">>;
14
14
  markOutboxDelivered(outboxId: string, deliveredAt: string): Promise<void>;
15
15
  compensate(input: TodosTaskManifestCompensateRequest, receipt: TodosTaskManifestReceipt, compensationReceiptId: string, requestDigest: string, now: string): Promise<TodosTaskManifestCompensationResult>;
16
16
  private readback;
@@ -46,7 +46,7 @@ function isBlockingDependencyStatus(status) {
46
46
  function isTerminalStatus(status) {
47
47
  return status === "completed" || status === "failed" || status === "cancelled";
48
48
  }
49
- 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;
49
+ 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;
50
50
  var init_types = __esm(() => {
51
51
  TASK_STATUSES = [
52
52
  "pending",
@@ -61,7 +61,7 @@ var init_types = __esm(() => {
61
61
  "high",
62
62
  "critical"
63
63
  ];
64
- PLAN_STATUSES = ["active", "completed", "archived"];
64
+ PLAN_STATUSES = ["active", "completed", "archived", "planning", "cancelled"];
65
65
  VersionConflictError = class VersionConflictError extends Error {
66
66
  taskId;
67
67
  expectedVersion;
@@ -250,6 +250,16 @@ var init_types = __esm(() => {
250
250
  this.name = "DispatchNotFoundError";
251
251
  }
252
252
  };
253
+ InputValidationError = class InputValidationError extends Error {
254
+ static code = "INVALID_INPUT";
255
+ code = InputValidationError.code;
256
+ suggestion;
257
+ constructor(message, suggestion) {
258
+ super(message);
259
+ this.name = "InputValidationError";
260
+ this.suggestion = suggestion;
261
+ }
262
+ };
253
263
  });
254
264
 
255
265
  // src/lib/slugs.ts
@@ -323,83 +333,74 @@ function validateSnapshotRoutingDestinationConflicts(projects, taskLists, existi
323
333
  return errors2;
324
334
  }
325
335
 
326
- // node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
336
+ // src/lib/paths.ts
337
+ import { existsSync } from "fs";
327
338
  import { homedir } from "os";
328
- import { join } from "path";
329
- function assertApp(app) {
339
+ import { isAbsolute, join, resolve } from "path";
340
+ import { homedir as pathsResolverHomedir } from "os";
341
+ import { join as pathsResolverJoin } from "path";
342
+ function pathsResolverAssertApp(app) {
330
343
  if (typeof app !== "string" || app.length === 0) {
331
344
  throw new TypeError("paths: app must be a non-empty string");
332
345
  }
333
- if (!APP_SLUG_RE.test(app)) {
346
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
334
347
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
335
348
  }
336
349
  }
337
- function envOf(options) {
338
- return options.env ?? process.env;
339
- }
340
- function envValue(options, kind) {
341
- const value = envOf(options)[KIND_ENV[kind]];
342
- return typeof value === "string" && value.length > 0 ? value : undefined;
343
- }
344
- function isMacOS(platform) {
345
- return platform === "darwin";
350
+ function pathsResolverAssertKind(kind) {
351
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
352
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
353
+ }
346
354
  }
347
- function baseDir(kind, options) {
348
- const override = envValue(options, kind);
349
- if (override)
355
+ function pathsResolverBaseDir(kind, options) {
356
+ pathsResolverAssertKind(kind);
357
+ const env = options.env ?? process.env;
358
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
359
+ if (typeof override === "string" && override.length > 0)
350
360
  return override;
351
- const home = options.home ?? homedir();
361
+ const home = options.home ?? pathsResolverHomedir();
352
362
  const platform = options.platform ?? process.platform;
353
- if (isMacOS(platform)) {
363
+ if (platform === "darwin") {
354
364
  switch (kind) {
355
365
  case "config":
356
366
  case "data":
357
- return join(home, "Library", "Application Support", "Hasna");
367
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
358
368
  case "cache":
359
- return join(home, "Library", "Caches", "Hasna");
369
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
360
370
  case "state":
361
- return join(home, "Library", "Logs", "Hasna");
371
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
362
372
  }
363
373
  }
364
374
  switch (kind) {
365
375
  case "config":
366
- return join(home, ".config", "hasna");
376
+ return pathsResolverJoin(home, ".config", "hasna");
367
377
  case "data":
368
- return join(home, ".local", "share", "hasna");
378
+ return pathsResolverJoin(home, ".local", "share", "hasna");
369
379
  case "state":
370
- return join(home, ".local", "state", "hasna");
380
+ return pathsResolverJoin(home, ".local", "state", "hasna");
371
381
  case "cache":
372
- return join(home, ".cache", "hasna");
382
+ return pathsResolverJoin(home, ".cache", "hasna");
373
383
  }
374
384
  }
375
- function resolvePath(kind, options) {
376
- assertApp(options.app);
377
- const appSegment = options.internal === true ? join("internal", options.app) : options.app;
378
- return join(baseDir(kind, options), appSegment);
385
+ function pathsResolverResolve(kind, options) {
386
+ pathsResolverAssertApp(options.app);
387
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
388
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
379
389
  }
380
390
  function dataDir(options) {
381
- return resolvePath("data", options);
391
+ return pathsResolverResolve("data", options);
382
392
  }
383
- var KIND_ENV, APP_SLUG_RE;
384
- var init_dist = __esm(() => {
385
- KIND_ENV = {
386
- config: "HASNA_CONFIG_HOME",
387
- data: "HASNA_DATA_HOME",
388
- state: "HASNA_STATE_HOME",
389
- cache: "HASNA_CACHE_HOME"
390
- };
391
- APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
392
- });
393
-
394
- // src/lib/paths.ts
395
- import { existsSync } from "fs";
396
- import { homedir as homedir2 } from "os";
397
- import { join as join2, resolve } from "path";
398
393
  function effectiveHome(env = process.env) {
399
- return env.HOME || env.USERPROFILE || homedir2();
394
+ return env.HOME || env.USERPROFILE || homedir();
395
+ }
396
+ function hasnaHomeRoot(env = process.env) {
397
+ const override = env["HASNA_HOME"]?.trim();
398
+ if (override && isAbsolute(override))
399
+ return override;
400
+ return join(effectiveHome(env), ".hasna");
400
401
  }
401
402
  function legacyHomeDir(env = process.env) {
402
- return join2(effectiveHome(env), ".hasna", "todos");
403
+ return join(hasnaHomeRoot(env), "todos");
403
404
  }
404
405
  function resolverHome(env = process.env) {
405
406
  return dataDir({ app: "todos", home: effectiveHome(env), env });
@@ -408,23 +409,30 @@ function adoptResolverHome(resolved, env = process.env) {
408
409
  const dataOverride = env.HASNA_DATA_HOME;
409
410
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
410
411
  return true;
411
- return existsSync(join2(resolved, "todos.db")) || existsSync(join2(resolved, "config.json"));
412
+ return existsSync(join(resolved, "todos.db")) || existsSync(join(resolved, "config.json"));
412
413
  }
413
414
  function getTodosDir(env = process.env) {
414
415
  const resolved = resolverHome(env);
415
416
  return resolve(adoptResolverHome(resolved, env) ? resolved : legacyHomeDir(env));
416
417
  }
418
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
417
419
  var init_paths = __esm(() => {
418
- init_dist();
420
+ PATHS_RESOLVER_KIND_ENV = {
421
+ config: "HASNA_CONFIG_HOME",
422
+ data: "HASNA_DATA_HOME",
423
+ state: "HASNA_STATE_HOME",
424
+ cache: "HASNA_CACHE_HOME"
425
+ };
426
+ PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
419
427
  });
420
428
 
421
429
  // src/lib/sync-utils.ts
422
430
  import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
423
431
  import { createHash as createHash2 } from "crypto";
424
- import { homedir as homedir3 } from "os";
425
- import { join as join3 } from "path";
432
+ import { homedir as homedir2 } from "os";
433
+ import { join as join2 } from "path";
426
434
  function getHomeDir() {
427
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir3();
435
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
428
436
  }
429
437
  function getTodosGlobalDir() {
430
438
  return getTodosDir();
@@ -450,14 +458,14 @@ function writeJsonFile(path, data) {
450
458
  `);
451
459
  }
452
460
  function readHighWaterMark(dir) {
453
- const path = join3(dir, ".highwatermark");
461
+ const path = join2(dir, ".highwatermark");
454
462
  if (!existsSync2(path))
455
463
  return 1;
456
464
  const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
457
465
  return isNaN(val) ? 1 : val;
458
466
  }
459
467
  function writeHighWaterMark(dir, value) {
460
- writeFileSync(join3(dir, ".highwatermark"), String(value));
468
+ writeFileSync(join2(dir, ".highwatermark"), String(value));
461
469
  }
462
470
  function getFileMtimeMs(path) {
463
471
  try {
@@ -518,9 +526,9 @@ var init_sync_utils = __esm(() => {
518
526
 
519
527
  // src/lib/config.ts
520
528
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
521
- import { dirname, join as join4 } from "path";
529
+ import { dirname, join as join3 } from "path";
522
530
  function getConfigPath() {
523
- return join4(getTodosGlobalDir(), "config.json");
531
+ return join3(getTodosGlobalDir(), "config.json");
524
532
  }
525
533
  function normalizeAgent(agent) {
526
534
  return agent.trim().toLowerCase();
@@ -571,21 +579,6 @@ function normalizeApiUrl(value) {
571
579
  return null;
572
580
  return trimmed.replace(/\/+$/, "");
573
581
  }
574
- function getLocalApiConfig(env = process.env) {
575
- const config = loadConfig();
576
- const envApiUrl = normalizeApiUrl(env["TODOS_URL"]);
577
- const configApiUrl = normalizeApiUrl(config.apiUrl);
578
- const apiUrl = envApiUrl ?? configApiUrl;
579
- const apiKey = env["TODOS_API_KEY"] || config.apiKey || null;
580
- return {
581
- apiUrl,
582
- apiKey,
583
- source: {
584
- apiUrl: envApiUrl ? "TODOS_URL" : configApiUrl ? "config" : "none",
585
- apiKey: env["TODOS_API_KEY"] ? "TODOS_API_KEY" : config.apiKey ? "config" : "none"
586
- }
587
- };
588
- }
589
582
  function getSyncAgentsFromConfig() {
590
583
  const config = loadConfig();
591
584
  const agents = config.sync_agents;
@@ -6108,6 +6101,171 @@ class SqliteTodosTaskManifestBackend {
6108
6101
  }
6109
6102
  }
6110
6103
 
6104
+ // src/storage/machine-registry.ts
6105
+ import { randomUUID } from "crypto";
6106
+ import { isDeepStrictEqual } from "util";
6107
+ class MachineRegistryError extends Error {
6108
+ status;
6109
+ constructor(message, status = 409) {
6110
+ super(message);
6111
+ this.status = status;
6112
+ }
6113
+ }
6114
+ var fields = new Set(["id", "name", "hostname", "platform", "last_seen_at", "metadata", "created_at", "ssh_address", "is_primary", "archived_at"]);
6115
+ function validateMachines(value) {
6116
+ if (!Array.isArray(value) || value.length > 1e4)
6117
+ throw new MachineRegistryError("machines must be an array of at most 10000 complete records", 400);
6118
+ for (const row of value) {
6119
+ if (!row || typeof row !== "object" || Array.isArray(row) || Object.keys(row).some((key2) => !fields.has(key2)))
6120
+ throw new MachineRegistryError("Invalid machine record fields", 400);
6121
+ for (const key2 of ["id", "name", "last_seen_at", "created_at"])
6122
+ if (typeof row[key2] !== "string" || !row[key2].trim() || row[key2].length > 1024)
6123
+ throw new MachineRegistryError(`Invalid machine ${key2}`, 400);
6124
+ for (const key2 of ["hostname", "platform", "ssh_address", "archived_at"])
6125
+ if (row[key2] !== null && typeof row[key2] !== "string")
6126
+ throw new MachineRegistryError(`Invalid machine ${key2}`, 400);
6127
+ for (const key2 of ["last_seen_at", "created_at", "archived_at"])
6128
+ if (row[key2] !== null && !Number.isFinite(Date.parse(row[key2])))
6129
+ throw new MachineRegistryError(`Invalid machine ${key2}`, 400);
6130
+ if (typeof row.is_primary !== "boolean" || !row.metadata || typeof row.metadata !== "object" || Array.isArray(row.metadata))
6131
+ throw new MachineRegistryError("Invalid machine flags or metadata", 400);
6132
+ if (!isDeepStrictEqual(JSON.parse(JSON.stringify(row.metadata)), row.metadata))
6133
+ throw new MachineRegistryError("Machine metadata must preserve JSON values exactly", 400);
6134
+ if (JSON.stringify(row.metadata).length > 262144)
6135
+ throw new MachineRegistryError("Machine metadata is too large", 400);
6136
+ if (row.is_primary && row.archived_at !== null)
6137
+ throw new MachineRegistryError("An archived machine cannot be primary", 400);
6138
+ }
6139
+ return value;
6140
+ }
6141
+ function planMachineImport(existing, incoming) {
6142
+ const rows = validateMachines(incoming);
6143
+ const byId = new Map(existing.map((row) => [row.id, row]));
6144
+ const byName = new Map(existing.map((row) => [row.name, row.id]));
6145
+ const insert = [];
6146
+ let skipped = 0;
6147
+ for (const row of rows) {
6148
+ const prior = byId.get(row.id);
6149
+ if (prior && !isDeepStrictEqual(prior, row))
6150
+ throw new MachineRegistryError("Machine migration conflicts with an existing identity; reconcile before retrying");
6151
+ if (byName.has(row.name) && byName.get(row.name) !== row.id)
6152
+ throw new MachineRegistryError("Machine name belongs to a different identity; migration was not applied");
6153
+ if (prior)
6154
+ skipped++;
6155
+ else {
6156
+ insert.push(row);
6157
+ byId.set(row.id, row);
6158
+ byName.set(row.name, row.id);
6159
+ }
6160
+ }
6161
+ if ([...byId.values()].filter((row) => row.is_primary).length > 1)
6162
+ throw new MachineRegistryError("Machine migration would create multiple primary machines");
6163
+ return { rows: insert, skipped };
6164
+ }
6165
+ function createPostgresMachineRegistry(client, service, table, ensure) {
6166
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table))
6167
+ throw new Error("Invalid machine registry table");
6168
+ async function list(connection = client) {
6169
+ await ensure();
6170
+ const result = await connection.query(`SELECT payload FROM ${table} WHERE service=$1 AND object_type='machines' AND deleted_at IS NULL ORDER BY object_id`, [service]);
6171
+ return validateMachines(result.rows.map((row) => typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload));
6172
+ }
6173
+ return { list, async execute(input) {
6174
+ if (!input || typeof input !== "object" || Array.isArray(input) || !["register", "heartbeat", "set-primary", "archive", "unarchive", "delete", "import"].includes(input.action))
6175
+ throw new MachineRegistryError("Invalid machine operation", 400);
6176
+ if (Object.keys(input).some((key2) => !["action", "name", "id", "options", "machines"].includes(key2)))
6177
+ throw new MachineRegistryError("Unknown machine operation fields", 400);
6178
+ if (input.options !== undefined && (!input.options || typeof input.options !== "object" || Array.isArray(input.options)))
6179
+ throw new MachineRegistryError("Invalid machine options", 400);
6180
+ await ensure();
6181
+ if (!client.transaction)
6182
+ throw new MachineRegistryError("Machine registry requires a transactional server backend", 501);
6183
+ return client.transaction(async (tx) => {
6184
+ await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${service}:machine-registry`]);
6185
+ const current = await list(tx);
6186
+ let changed = [];
6187
+ let skipped = 0;
6188
+ let machine;
6189
+ let deleted;
6190
+ const stamp = new Date().toISOString();
6191
+ if (input.action === "import") {
6192
+ const plan = planMachineImport(current, input.machines);
6193
+ const ids = plan.rows.map((row) => row.id);
6194
+ if (ids.length) {
6195
+ const retired = await tx.query(`SELECT object_id FROM ${table} WHERE service=$1 AND object_type='machines' AND object_id IN (SELECT jsonb_array_elements_text($2::text::jsonb)) AND deleted_at IS NOT NULL`, [service, JSON.stringify(ids)]);
6196
+ if (retired.rows.length)
6197
+ throw new MachineRegistryError("Machine migration contains a retired identity");
6198
+ }
6199
+ changed = plan.rows;
6200
+ skipped = plan.skipped;
6201
+ } else {
6202
+ if (typeof input.name !== "string" || !input.name.trim() || input.name.length > 1024)
6203
+ throw new MachineRegistryError("Machine name or ID is required", 400);
6204
+ const matches = current.filter((row) => row.name === input.name || row.id === input.name);
6205
+ if (matches.length > 1)
6206
+ throw new MachineRegistryError("Machine selector is ambiguous between a name and another identity; use an unambiguous selector");
6207
+ const prior = matches[0];
6208
+ if (input.action === "register" || input.action === "heartbeat") {
6209
+ const opts = input.options ?? {};
6210
+ const allowed = new Set(["hostname", "platform", "ssh_address", "primary", "tailscale_name", "tailscale_ip", "lan_address", "workspace_path", "git_root", "arch"]);
6211
+ for (const [key2, value] of Object.entries(opts)) {
6212
+ if (!allowed.has(key2) || (key2 === "primary" ? typeof value !== "boolean" : typeof value !== "string" || value.length > 4096))
6213
+ throw new MachineRegistryError("Invalid machine registration options", 400);
6214
+ }
6215
+ if (input.id !== undefined && (typeof input.id !== "string" || !input.id.trim() || input.id.length > 1024))
6216
+ throw new MachineRegistryError("Invalid stable machine ID", 400);
6217
+ if (prior && input.id && input.id !== prior.id)
6218
+ throw new MachineRegistryError("Machine registration ID does not match existing identity");
6219
+ if (!prior && input.id && current.some((row) => row.id === input.id))
6220
+ throw new MachineRegistryError("Machine ID already belongs to another name");
6221
+ const metadata = { ...prior?.metadata };
6222
+ for (const key2 of ["tailscale_name", "tailscale_ip", "lan_address", "workspace_path", "git_root", "arch"])
6223
+ if (opts[key2] !== undefined)
6224
+ metadata[key2] = opts[key2];
6225
+ if (!prior && input.id) {
6226
+ const retired = await tx.query(`SELECT object_id FROM ${table} WHERE service=$1 AND object_type='machines' AND object_id=$2 AND deleted_at IS NOT NULL`, [service, input.id]);
6227
+ if (retired.rows.length)
6228
+ throw new MachineRegistryError("Machine identity has been retired");
6229
+ }
6230
+ machine = { id: prior?.id ?? input.id ?? randomUUID(), name: prior?.name ?? input.name, hostname: opts.hostname ?? prior?.hostname ?? null, platform: opts.platform ?? prior?.platform ?? null, ssh_address: opts.ssh_address ?? prior?.ssh_address ?? null, created_at: prior?.created_at ?? stamp, last_seen_at: stamp, archived_at: prior?.archived_at ?? null, is_primary: opts.primary === true || (prior?.is_primary ?? false), metadata };
6231
+ if (machine.archived_at && machine.is_primary)
6232
+ throw new MachineRegistryError("Cannot set archived machine as primary");
6233
+ changed = [machine];
6234
+ if (opts.primary === true)
6235
+ changed.push(...current.filter((row) => row.is_primary && row.id !== machine.id).map((row) => ({ ...row, is_primary: false })));
6236
+ } else {
6237
+ if (!prior)
6238
+ throw new MachineRegistryError("Machine not found", 404);
6239
+ machine = { ...prior };
6240
+ if (input.action === "set-primary") {
6241
+ if (prior.archived_at)
6242
+ throw new MachineRegistryError("Cannot set archived machine as primary");
6243
+ machine.is_primary = true;
6244
+ changed = current.filter((row) => row.is_primary && row.id !== prior.id).map((row) => ({ ...row, is_primary: false }));
6245
+ } else if (input.action === "archive" || input.action === "delete") {
6246
+ if (prior.is_primary)
6247
+ throw new MachineRegistryError("Cannot archive or delete the primary machine");
6248
+ const refs = await tx.query(`SELECT object_id FROM ${table} WHERE service=$1 AND object_type<>'machines' AND deleted_at IS NULL AND payload->>'machine_id'=$2 AND ($3='delete' OR (object_type='tasks' AND payload->>'status' IN ('pending','in_progress'))) LIMIT 1`, [service, prior.id, input.action]);
6249
+ if (refs.rows.length)
6250
+ throw new MachineRegistryError("Machine still has referenced records or active tasks");
6251
+ machine.archived_at = stamp;
6252
+ deleted = input.action === "delete" ? true : undefined;
6253
+ } else if (input.action === "unarchive")
6254
+ machine.archived_at = null;
6255
+ else
6256
+ throw new MachineRegistryError("Unknown machine action", 400);
6257
+ changed.push(machine);
6258
+ }
6259
+ }
6260
+ for (const row of changed) {
6261
+ await tx.query(`INSERT INTO ${table} (service,object_type,object_id,payload,updated_at,deleted_at,version) VALUES ($1,'machines',$2,$3::text::jsonb,$4,$5,1) ON CONFLICT (service,object_type,object_id) DO UPDATE SET payload=EXCLUDED.payload,updated_at=EXCLUDED.updated_at,deleted_at=EXCLUDED.deleted_at,version=COALESCE(${table}.version,0)+1`, [service, row.id, JSON.stringify(row), stamp, deleted && row.id === machine?.id ? stamp : null]);
6262
+ }
6263
+ const machines = await list(tx);
6264
+ return { schema_version: 2, machines, ...machine ? { machine } : {}, inserted: changed.filter((row) => !current.some((old) => old.id === row.id)).length, skipped, ...deleted ? { deleted } : {} };
6265
+ });
6266
+ } };
6267
+ }
6268
+
6111
6269
  // src/storage/postgres-sync.ts
6112
6270
  init_types();
6113
6271
 
@@ -6330,6 +6488,10 @@ class PostgresTodosSyncStore {
6330
6488
  }
6331
6489
  }
6332
6490
  async pushSnapshot(snapshot, context = {}) {
6491
+ if ((snapshot.tombstones ?? []).some((row) => row.object_type === "machines"))
6492
+ throw new Error("Machine tombstones require explicit registry lifecycle operations");
6493
+ if (snapshot.machines !== undefined)
6494
+ validateMachines(snapshot.machines);
6333
6495
  const routingErrors = validateSnapshotRoutingRecords(snapshot.projects, snapshot.taskLists);
6334
6496
  if (routingErrors.length > 0) {
6335
6497
  throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
@@ -6352,6 +6514,14 @@ class PostgresTodosSyncStore {
6352
6514
  throw new ResourceConflictError("SNAPSHOT_DESTINATION_CONFLICT", `Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
6353
6515
  }
6354
6516
  const result = { records: 0, objectTypes: {} };
6517
+ if (snapshot.machines?.length) {
6518
+ if (!this.client.transaction)
6519
+ throw new Error("Machine snapshot migration requires transaction(callback)");
6520
+ const registry = createPostgresMachineRegistry({ query: client.query.bind(client), transaction: (fn) => fn(client) }, this.service, this.tableName, async () => {});
6521
+ const receipt = await registry.execute({ action: "import", machines: snapshot.machines });
6522
+ result.records += receipt.inserted + receipt.skipped;
6523
+ result.objectTypes.machines = receipt.inserted + receipt.skipped;
6524
+ }
6355
6525
  const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
6356
6526
  for (const entry of snapshotEntries(snapshot)) {
6357
6527
  if (entry.deletedAt === null)
@@ -6515,6 +6685,7 @@ function rowsToSnapshot(rows) {
6515
6685
  tasks: [],
6516
6686
  projects: [],
6517
6687
  projectMachinePaths: [],
6688
+ machines: [],
6518
6689
  plans: [],
6519
6690
  agents: [],
6520
6691
  taskLists: [],
@@ -6526,6 +6697,12 @@ function rowsToSnapshot(rows) {
6526
6697
  for (const row of rows) {
6527
6698
  const payload = payloadRecord(row.payload);
6528
6699
  const deletedAt = stringValue(row.deleted_at);
6700
+ if (row.object_type === "machines") {
6701
+ if (deletedAt)
6702
+ throw new Error("Snapshot contains retired machine identities; use the authoritative registry instead of silently discarding tombstones");
6703
+ snapshot.machines.push(payload);
6704
+ continue;
6705
+ }
6529
6706
  if (deletedAt) {
6530
6707
  snapshot.tombstones ??= [];
6531
6708
  snapshot.tombstones.push({