@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
@@ -0,0 +1,405 @@
1
+ # Native Storage Boundary
2
+
3
+ `@hasna/todos` stays local-first where it is a local product. `todos-serve`, the
4
+ native storage tooling, and the plan-artifact files run on local SQLite and
5
+ local artifact files without network access, hosted credentials, SaaS accounts,
6
+ or a shared cloud runtime package.
7
+
8
+ The MCP server and the CLI are NOT wholly local. Their plan and task-list
9
+ command families — and the CLI's stateful template commands — are served by the
10
+ authenticated shared API, and the MCP tools that still read the on-box store are
11
+ served only under the explicit local opt-in (`HASNA_TODOS_LOCAL=1`) — see "MCP
12
+ tools and the on-box store" below. The plan-artifact surface is the local half
13
+ of a shared record: the Markdown companion file is local, the plan it describes
14
+ is not.
15
+
16
+ The CLI's client transport is NOT implicitly local. Since the fleet fail-closed
17
+ ruling (2026-09-04, hasna/apps#1613), a CLI run with neither API variable
18
+ present exits non-zero with an error naming `HASNA_TODOS_API_URL` and
19
+ `HASNA_TODOS_API_KEY` — it never silently serves the on-box SQLite store.
20
+ Serving local SQLite through the CLI requires the explicit opt-in
21
+ `HASNA_TODOS_LOCAL=1` (alias `TODOS_LOCAL=1`), which is how local-only CLI
22
+ deployments, self-hosters, and tests opt in deliberately.
23
+
24
+ Remote storage is explicit and repo-native. Internal Hasna deployments and
25
+ future SaaS wrappers should configure it through `HASNA_TODOS_*` variables and
26
+ provide the matching Postgres/S3 clients through the public `./storage` package
27
+ export.
28
+
29
+ ## The data-backend switch
30
+
31
+ There is no deployment-mode axis. One switch selects where data lives, with
32
+ exactly two arms on each side of the HTTP boundary:
33
+
34
+ - **Client (CLI/MCP/TUI)** — `sqlite` (the on-box file, reachable only under
35
+ the explicit local opt-in `HASNA_TODOS_LOCAL=1` / `TODOS_LOCAL=1`) or `http`
36
+ (the authenticated Todos `/v1` authority). The client never opens Postgres
37
+ directly.
38
+ - **Server (`todos-serve`) / native storage tooling** — `sqlite` (no database
39
+ URL configured) or `postgres` (a `HASNA_TODOS_DATABASE_URL` is present).
40
+
41
+ `HASNA_TODOS_API_URL` + `HASNA_TODOS_API_KEY` select the authenticated HTTP
42
+ authority (client). `HASNA_TODOS_DATABASE_URL` selects the Postgres backend
43
+ (server/native storage tooling). The storage-mode variables
44
+ (`HASNA_TODOS_STORAGE_MODE` and their aliases) are RETIRED: their mere presence
45
+ is a hard error (owner directive 2026-08-15), and no deployment-mode token is
46
+ accepted. No third arm exists.
47
+
48
+ Legacy hosted API toggles are not storage selectors. They cannot select the
49
+ local store and cannot rescue a run that lacks both the API pair and the local
50
+ opt-in.
51
+
52
+ ## CLI Remote HTTP Authority
53
+
54
+ The open CLI remote route uses only these canonical settings:
55
+
56
+ | Setting | Purpose |
57
+ | --- | --- |
58
+ | `HASNA_TODOS_API_URL` | Todos authority root, the same root ending in `/v1`, or the fleet gateway form `https://api.hasna.com/todos[/v1]`. |
59
+ | `HASNA_TODOS_API_KEY` | API key supplied to the authority as a bearer credential. |
60
+
61
+ Both must be set to select the HTTP authority. URL set without KEY (or KEY set
62
+ without URL) is a hard error naming the missing variable — the CLI never falls
63
+ back to the on-box SQLite store from a partial cloud configuration. With
64
+ NEITHER set, the CLI fails closed (non-zero exit, error naming both variables
65
+ and the opt-in) unless `HASNA_TODOS_LOCAL=1` (alias `TODOS_LOCAL=1`) explicitly
66
+ opts into local SQLite mode. There is no default local mode and no
67
+ `todos-local-fallback` notice path: a false-green rc 0 against the on-box store
68
+ is impossible (the incident 715712 response was tightened by the ruling).
69
+
70
+ The URL must use HTTPS, except for loopback development authorities. Userinfo,
71
+ query strings, fragments, redirects, `/api/v1`, and other non-`<app>[//v1]`
72
+ paths are rejected. The gateway form `https://api.hasna.com/<app>` is accepted
73
+ and `/v1` is appended (the gateway strips the `<app>` segment and forwards to
74
+ the `https://<app>.hasna.xyz` origin); `<app>/v1` is used verbatim. The CLI
75
+ does not infer an authority from `TODOS_URL`, does not use a private SaaS
76
+ route, and does not send the bearer credential across redirects.
77
+
78
+ Remote selection is fail-closed. A missing URL or key, conflicting mode
79
+ selectors, an incompatible collection route, authentication failure, timeout,
80
+ or server failure is reported with a `REMOTE_*` diagnostic before local SQLite
81
+ can open. A resource-level 404 remains a normal not-found result. There is no
82
+ SQLite or Postgres fallback for a remote CLI invocation.
83
+
84
+ The supported coordination surface includes storage/status diagnostics,
85
+ projects, task lists, project-scoped plans, task create/upsert/list/show/update,
86
+ task-list and plan moves, comments, start/complete/delete, and next/claim. A
87
+ command without a safe `/v1` equivalent exits with `REMOTE_COMMAND_UNSUPPORTED`
88
+ before local helpers run.
89
+
90
+ `todos storage status --json` is a configuration-only diagnostic. On the http
91
+ transport it reports the redacted `/v1` base, URL/key presence, HTTP transport, and
92
+ the disabled local fallback without opening a database or making a network
93
+ request. `todos health` and `todos doctor` authenticate against the required
94
+ remote routes.
95
+
96
+ `todos doctor` additionally audits REFERENTIAL INTEGRITY against the authority
97
+ and its exit code is a verdict, not a "the call succeeded" flag: `0` clean, `1`
98
+ findings (orphaned or dangling project / task-list references), `2` incomplete
99
+ (a condition could not be measured, so health was not established). It prefers
100
+ the server-side aggregate `GET /v1/integrity`, which the storage adapter computes
101
+ with one SQL COUNT per condition on Postgres and SQLite alike; an authority that
102
+ does not expose it leaves the task-level conditions `NOT CHECKED` unless
103
+ `--scan-tasks` completes a read-only paged walk of `/v1/tasks`. Findings are
104
+ report-only — `doctor --apply` remains refused outright in remote mode and never
105
+ repairs an integrity finding in any mode.
106
+
107
+ ## Native AWS Configuration
108
+
109
+ - `HASNA_TODOS_DATABASE_URL`: Postgres connection string for RDS-backed task
110
+ state.
111
+ - `HASNA_TODOS_DATABASE_SSL`: boolean, defaults to `true`.
112
+ - `HASNA_TODOS_DATABASE_SCHEMA`: optional schema name for service-owned
113
+ isolation.
114
+ - `HASNA_TODOS_S3_BUCKET`: optional artifact bucket.
115
+ - `HASNA_TODOS_S3_PREFIX`: object prefix, defaults to `todos/`.
116
+ - `HASNA_TODOS_AWS_REGION`: AWS region for S3 and RDS-adjacent operations.
117
+ - `HASNA_TODOS_S3_ENDPOINT`: optional endpoint for tests or compatible object
118
+ stores.
119
+ - `HASNA_TODOS_S3_FORCE_PATH_STYLE`: boolean for local S3-compatible tests.
120
+ - `HASNA_TODOS_SYNC_BATCH_SIZE`: positive integer, defaults to `500`.
121
+ - `HASNA_TODOS_SYNC_DRY_RUN`: boolean sync preview flag.
122
+
123
+ Plain local-development fallbacks are accepted with the same names minus the
124
+ `HASNA_` prefix, for example `TODOS_DATABASE_URL`, and
125
+ `TODOS_S3_BUCKET`. Public docs and wrappers should still prefer the canonical
126
+ `HASNA_TODOS_*` names.
127
+
128
+ Production secrets should follow a consistent namespaced convention, for
129
+ example:
130
+
131
+ - `<org>/<division>/<app>/<env>/env`
132
+ - `<org>/<division>/<app>/<env>/rds`
133
+ - `<org>/<division>/<app>/<env>/s3`
134
+
135
+ Deployment-specific infrastructure identifiers — the managed Postgres cluster
136
+ name and the secrets-manager path that holds the runtime database URL — are
137
+ owned by the private hosting wrapper and supplied at runtime via the optional
138
+ `HASNA_TODOS_RDS_CLUSTER` and `HASNA_TODOS_RDS_RUNTIME_PATH` environment
139
+ variables; this open package ships no real cluster names or secrets-manager
140
+ paths. Runtime wiring should set `HASNA_TODOS_DATABASE_URL` from the resolved
141
+ secret. `TODOS_DATABASE_URL` is only a plain fallback for local development or
142
+ wrappers that have not yet migrated.
143
+
144
+ A SaaS wrapper owns tenant state, billing, accounts, deployment, observability,
145
+ and production secret wiring. The open package owns local storage, the public
146
+ storage contract, local tests, and explicit remote adapter interfaces.
147
+
148
+ ## Public Adapter Exports
149
+
150
+ The public `@hasna/todos/storage` export now includes:
151
+
152
+ - `loadTodosStorageConfig` and `createTodosStorageAdapter` for backend selection.
153
+ - `STORAGE_TABLES`, `TODOS_STORAGE_ENV`, and `TODOS_STORAGE_FALLBACK_ENV` for
154
+ wrapper provenance and explicit env mapping.
155
+ - `exportSqliteTodosStorageSnapshot` and `importSqliteTodosStorageSnapshot`
156
+ for local SQLite state movement without a hosted service.
157
+ - `createHybridTodosStorageAdapter` for local SQLite CRUD with explicit
158
+ Postgres-backed remote snapshot push/pull.
159
+ - `createPostgresTodosStorageAdapter` for pure remote CRUD backed by the same
160
+ Postgres JSONB sync records and a caller-provided query client.
161
+ - `createPostgresTodosSyncStore` for RDS-backed snapshot push/pull and
162
+ cross-machine sync cursors through a caller-provided Postgres query client.
163
+ - `createTodosS3ArtifactStore` for S3 object reads/writes/deletes through
164
+ signed `fetch` requests and caller-provided credentials.
165
+ - `uploadRunArtifactsToS3` and `downloadRunArtifactsFromS3` for syncing
166
+ locally stored `task_run_artifacts` bytes to and from S3 while keeping the
167
+ local artifact metadata rows as the source of truth. Objects are
168
+ content-addressed under `artifacts/<task_id>/<sha256>` inside the configured
169
+ bucket prefix.
170
+ - `uploadRunArtifactAtCreation` for the creation-time hop: when
171
+ `HASNA_TODOS_S3_BUCKET` (or the `TODOS_S3_BUCKET` fallback) is set, the CLI
172
+ `runs artifact` command, the MCP `add_task_run_artifact` tool, and the
173
+ environment-snapshot flows upload the stored bytes to S3 right after the
174
+ artifact row is created and record the remote reference on its metadata, so
175
+ `todos storage artifacts download` restores the bytes on any machine. The
176
+ hop is fail-soft: without a bucket or with missing credentials the artifact
177
+ stays local-only and creation never fails.
178
+
179
+ These exports are dependency-light by design. The open package does not bundle
180
+ platform billing, tenant tables, deployment code, or a cloud SDK. Internal
181
+ deployments and wrappers can provide `pg` clients, credentials, and secret
182
+ loading from their own runtime.
183
+
184
+ ## Plan Markdown Artifacts
185
+
186
+ Plan records are served by the authenticated shared API; a project-scoped plan
187
+ also has an optional local Markdown companion file that this CLI writes on this
188
+ machine. The root is always chosen by the caller with
189
+ `--artifact-root <directory>` — a real local project directory. A path returned
190
+ by the API is never used. The companion lives at:
191
+
192
+ ```text
193
+ <artifact-root>/.hasna/todos/plans/<project-id>/<plan-slug>--<id8>.md
194
+ ```
195
+
196
+ The Markdown file is an offline-readable artifact for agents, reviews, handoffs,
197
+ and branch work. It is not the registry: the shared plan record is the source of
198
+ truth, and the file is a client-side copy.
199
+
200
+ Each file uses the `hasna.todos.plan/v1` schema in frontmatter:
201
+
202
+ ```markdown
203
+ ---
204
+ schema: "hasna.todos.plan/v1"
205
+ plan_id: "<stable plan UUID>"
206
+ plan_slug: "launch-plan"
207
+ project_id: "<stable project UUID>"
208
+ task_list_id: null
209
+ agent_id: null
210
+ stable_id: "<same stable plan UUID>"
211
+ name: "Launch Plan"
212
+ status: "active"
213
+ created_at: "2026-06-30T00:00:00.000Z"
214
+ updated_at: "2026-06-30T00:00:00.000Z"
215
+ artifact_updated_at: "2026-06-30T00:00:00.000Z"
216
+ ---
217
+
218
+ # Launch Plan
219
+
220
+ ## Tasks
221
+
222
+ - [ ] Example task
223
+ <!-- todos: task_id=<task-id> status=pending priority=medium -->
224
+ ```
225
+
226
+ The path resolver only accepts safe project and plan path segments and requires
227
+ a project-scoped plan; an unscoped plan has no artifact because the layout is
228
+ explicitly under `<project-id>`.
229
+
230
+ `todos plans --show <id-or-slug> --artifact-root <directory>` reads the
231
+ companion file when present and includes the parsed artifact metadata and body
232
+ in JSON output. The text view prints the artifact path. If the file is missing,
233
+ the command still shows the shared plan record.
234
+
235
+ For backwards compatibility, artifact readers also check the legacy UUID path:
236
+
237
+ ```text
238
+ <artifact-root>/.hasna/todos/plans/<project-id>/<plan-id>.md
239
+ ```
240
+
241
+ When both files exist, the slugged `<plan-slug>--<id8>.md` artifact wins. A
242
+ later `--write-artifacts` run materializes the slugged artifact while leaving
243
+ legacy files untouched for operator review.
244
+
245
+ For migration and diagnostics:
246
+
247
+ ```bash
248
+ todos plans --write-artifacts --artifact-root <directory>
249
+ todos plans --artifact <id-or-slug> --artifact-root <directory> --json
250
+ ```
251
+
252
+ Before 0.16.0 the artifact root was implicit: the plan was read from local
253
+ SQLite and its recorded project path supplied the root, so the form was
254
+ `todos plans --artifact <id-or-slug> --json`. That invocation now exits
255
+ non-zero with `--artifact-root is required`. The root is always the caller's
256
+ choice, because a path returned by the shared API never authorizes client
257
+ filesystem access.
258
+
259
+ `--write-artifacts` materializes Markdown files for every project-scoped plan in
260
+ the current project scope using readable slug filenames. `--artifact` reports
261
+ the resolved file path, whether the file exists, parse errors, task references,
262
+ and deterministic conflicts between the shared plan record and Markdown
263
+ frontmatter/task comments. The CLI does not silently treat the Markdown file as
264
+ authoritative when conflicts exist; agents should resolve the conflict through
265
+ the CLI or an explicit migration task.
266
+
267
+ ## MCP tools and the on-box store
268
+
269
+ The MCP server no longer opens the on-box SQLite store implicitly. `getDatabase()`
270
+ throws `API_DATABASE_FALLBACK_FORBIDDEN` unless the call supplies an explicit
271
+ path or the environment selects the local store
272
+ (`selectsTodosLocalStore()` — the deliberate `HASNA_TODOS_LOCAL=1` /
273
+ `TODOS_LOCAL=1` opt-in on an environment that configures no authority or
274
+ credential of its own).
275
+
276
+ Tools that read the store through the shared API or read no store at all are
277
+ unaffected. Tools that still call into the local data layer directly answer with
278
+ the typed `{"code":"API_DATABASE_FALLBACK_FORBIDDEN"}` payload on the default
279
+ posture, whose `suggestion` names the opt-in; the server logs the same code. The
280
+ ten plan/task-list tools (`create_plan`, `list_plans`, `get_plan`, `update_plan`,
281
+ `delete_plan`, `create_task_list`, `list_task_lists`, `get_task_list`,
282
+ `update_task_list`, `delete_task_list`) return the typed
283
+ `REMOTE_API_CONFIG_MISSING` refusal that names the missing configuration.
284
+
285
+ Measured at 0.16.0 with `TODOS_PROFILE=full` on the default (no-credential)
286
+ posture (361 tools, 125 of them taking no required arguments), 68 of the 125
287
+ returned `API_DATABASE_FALLBACK_FORBIDDEN`:
288
+
289
+ - templates: `list_templates`, `init_templates` (the rest of the family —
290
+ `create_template`, `preview_template`, `export_template`, `import_template`,
291
+ `create_task_from_template` — takes arguments, so it is outside the
292
+ zero-argument census and reads the same store; `list_template_library` and
293
+ `write_template_library` are bundled-only and unaffected)
294
+ - tags and labels: `list_tags`, `list_labels`
295
+ - agent work: `get_org_chart`, `suggest_agent_name`, `get_usage_ledger`,
296
+ `get_leaderboard`, `list_agent_run_queue`, `run_next_agent_dispatch`,
297
+ `list_task_runs`, `finish_task_run`, `list_task_findings`
298
+ - health and reporting: `run_doctor`, `todos_retro`, `todos_inbox`,
299
+ `build_local_report`, `get_planning_forecast`, `get_activity_timeline`,
300
+ `get_time_report`, `export_agent_reliability_scorecards`
301
+ - stale, blocked and archived work: `get_stale_tasks`, `get_blocked_tasks`,
302
+ `get_blocking_tasks`, `get_critical_path`, `patrol_tasks`,
303
+ `archive_completed`, `get_archived_tasks`, `notify_upcoming_deadlines`,
304
+ `get_sla_breaches`, `rebalance_workload`
305
+ - boards, calendar and review: `list_boards`, `list_calendar_events`,
306
+ `export_calendar_ics`, `get_review_queue`, `list_review_queue`
307
+ - knowledge, risk and search: `list_knowledge_records`,
308
+ `export_knowledge_records`, `list_risks`, `export_risk_register`,
309
+ `list_retrospectives`, `create_retrospective`, `export_retrospectives`,
310
+ `sync_kg`, `find_duplicate_tasks`, `list_search_views`,
311
+ `query_tasks_by_fields`, `migrate_workflow_states`
312
+ - files, locks and handoffs: `list_file_locks`, `list_handoffs`,
313
+ `get_latest_handoff`, `get_file_heat_map`, `list_active_files`,
314
+ `list_inbox_items`, `import_external_issues`
315
+ - backups, integrity and fixtures: `create_local_backup`,
316
+ `check_local_integrity`, `poll_local_snapshots`, `import_onboarding_fixture`
317
+ - focus and audit: `start_focus_session`, `list_focus_sessions`,
318
+ `get_idle_focus_prompts`, `get_audit_ledger`
319
+ - projects, notifications and dispatch: `bootstrap_project`,
320
+ `check_local_notifications`, `list_dispatches`, `run_due_dispatches`,
321
+ `generate_release_notes`
322
+
323
+ The shared-API MCP tools that need a credential — `list_tasks`, `list_projects`,
324
+ `get_status`, `get_my_tasks`, `get_health`, `standup`, `list_agents`,
325
+ `machines_*` — answer the typed `REMOTE_API_CONFIG_MISSING` on the default
326
+ posture: the documented fail-closed behaviour rather than a lost local surface.
327
+ Five more zero-argument tools refuse caller input or local state with the typed
328
+ `INVALID_INPUT` / `ENCRYPTION_KEY_UNAVAILABLE` / `ENCRYPTED_PAYLOAD_INVALID`,
329
+ and five answer a readable text refusal. Measured on the same 125-tool census, no
330
+ zero-argument tool returns an opaque `UNKNOWN_ERROR` — neither on this default
331
+ posture nor under the `HASNA_TODOS_LOCAL=1` opt-in below, where
332
+ `create_retrospective` and `finish_task_run` now answer the typed
333
+ `INVALID_INPUT` for their missing scope / run id instead of the opaque
334
+ `UNKNOWN_ERROR` they returned earlier in 0.16.0.
335
+
336
+ Run the MCP server with `HASNA_TODOS_LOCAL=1` to serve these tools from the
337
+ on-box store. That opt-in is honoured only when the environment configures no
338
+ authority or credential: with `HASNA_TODOS_API_KEY` or `HASNA_TODOS_API_URL`
339
+ set, a configured environment outranks the opt-in and these tools are
340
+ unreachable in 0.16.0. Converting them to the shared API is tracked separately;
341
+ this document records the boundary rather than claiming the surface is
342
+ converted.
343
+
344
+ ## Hybrid Sync Shape (explicit migration machinery)
345
+
346
+ `createHybridTodosStorageAdapter` builds a local-plus-remote adapter when the
347
+ caller passes a Postgres-style query client or sync store. It is migration/sync
348
+ machinery invoked explicitly — it is NOT an arm of the data-backend switch, and
349
+ no environment value selects it:
350
+
351
+ - Local CRUD stays SQLite-backed and works offline.
352
+ - `adapter.sync.exportSnapshot()` and `adapter.sync.importSnapshot()` move the
353
+ storage-level SQLite snapshot.
354
+ - `adapter.remote.pushSnapshot()` writes the local snapshot into Postgres sync
355
+ records.
356
+ - `adapter.remote.pullSnapshot()` reads Postgres sync records and imports them
357
+ into local SQLite.
358
+ - `adapter.remote.syncOnce()` pulls first, then pushes the merged local
359
+ snapshot.
360
+
361
+ This is the open-package boundary. SaaS tenant wrappers can be added on top
362
+ without changing the local default or depending on a shared cloud package.
363
+
364
+ ## Postgres CRUD Shape
365
+
366
+ A present `HASNA_TODOS_DATABASE_URL` builds a pure Postgres adapter when the
367
+ caller passes a Postgres-style query client to
368
+ `createTodosStorageAdapter`:
369
+
370
+ - CRUD uses the repo-owned `todos_sync_records` JSONB table rather than SaaS
371
+ tenant tables.
372
+ - The package does not import `pg`; wrappers or internal deployments provide
373
+ the connected client.
374
+ - `createPostgresTodosStorageAdapter` is also exported directly for callers
375
+ that want to bypass backend selection.
376
+ - Local SQLite remains the default unless `HASNA_TODOS_DATABASE_URL` is set
377
+ explicitly (with the one carve-out that a shadow mirror
378
+ `HASNA_TODOS_SHADOW=1` keeps SQLite as the source of truth).
379
+
380
+ ## S3 Artifact Sync
381
+
382
+ Run artifacts recorded with local stored content can now be pushed to S3 through
383
+ the public storage helpers:
384
+
385
+ - `uploadRunArtifactsToS3({ store, db, filter })` verifies the local
386
+ content-addressed artifact bytes, uploads them using `createTodosS3ArtifactStore`,
387
+ and stores a `remote_artifact_store` reference on the `task_run_artifacts`
388
+ metadata row.
389
+ - `downloadRunArtifactsFromS3({ store, db, filter })` reads that remote
390
+ reference, downloads the object, verifies the checksum, and restores the local
391
+ content-addressed file.
392
+
393
+ The helper only needs a caller-provided S3 store. Credentials, secret loading,
394
+ tenant scoping, and production scheduling remain outside the open package.
395
+
396
+ The CLI exposes the same boundary:
397
+
398
+ - `todos storage artifacts upload --run-id <id> --json` previews uploadable
399
+ local artifact bytes without network access.
400
+ - `todos storage artifacts download --run-id <id> --json` previews remote
401
+ restore work without network access.
402
+ - Add `--apply` to perform the S3 operation. Apply mode requires
403
+ `HASNA_TODOS_S3_BUCKET` plus `HASNA_TODOS_S3_ACCESS_KEY_ID` and
404
+ `HASNA_TODOS_S3_SECRET_ACCESS_KEY`; `HASNA_TODOS_S3_SESSION_TOKEN` is
405
+ optional.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.15.52",
3
+ "version": "0.17.0",
4
4
  "description": "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -53,37 +53,40 @@
53
53
  }
54
54
  },
55
55
  "workspaces": [
56
- "dashboard",
57
56
  "ai"
58
57
  ],
59
58
  "files": [
60
59
  "dist",
61
- "dashboard/dist",
62
60
  "postinstall.js",
63
61
  "LICENSE",
64
- "README.md"
62
+ "README.md",
63
+ "CHANGELOG.md",
64
+ "docs/PLAN_API.md",
65
+ "docs/TASK_LIST_API.md",
66
+ "docs/TEMPLATE_API.md",
67
+ "docs/TASK_QUERY_API.md",
68
+ "docs/native-storage.md"
65
69
  ],
66
70
  "scripts": {
67
- "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",
71
+ "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",
68
72
  "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",
69
73
  "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/*'",
70
74
  "migrate": "bun run src/server/index.ts migrate",
71
75
  "backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
72
76
  "generate:sdk": "bun run scripts/generate-sdk.ts",
73
- "build:dashboard": "cd dashboard && bun install --frozen-lockfile && bun run build",
74
77
  "typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
75
- "test": "bun test",
76
- "test:no-cloud": "bun test src/no-cloud-boundary.test.ts src/local-first.test.ts src/lib/public-release-gate.test.ts",
78
+ "test": "bun test --timeout 120000",
79
+ "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",
77
80
  "dev:cli": "bun run src/cli/index.tsx",
78
81
  "dev:mcp": "bun run src/mcp/index.ts",
79
82
  "dev:serve": "bun run src/server/index.ts",
80
83
  "verify:release": "bun run scripts/verify-public-release.ts --mode=review",
81
84
  "verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
82
85
  "verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
83
- "test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
86
+ "test:attested-container-candidate": "bun test --timeout 120000 scripts/attested-container-candidate.test.ts",
84
87
  "emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
85
88
  "verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
86
- "test:attested-container-compatibility-vector": "bun test scripts/attested-container-compatibility-vector.test.ts",
89
+ "test:attested-container-compatibility-vector": "bun test --timeout 120000 scripts/attested-container-compatibility-vector.test.ts",
87
90
  "issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
88
91
  "prepublishOnly": "bun run scripts/verify-public-release.ts --mode=publish",
89
92
  "postinstall": "node postinstall.js"
@@ -107,11 +110,12 @@
107
110
  },
108
111
  "repository": {
109
112
  "type": "git",
110
- "url": "https://github.com/hasna/todos.git"
113
+ "url": "https://github.com/hasna/apps.git",
114
+ "directory": "apps/todos"
111
115
  },
112
- "homepage": "https://github.com/hasna/todos",
116
+ "homepage": "https://github.com/hasna/apps/tree/main/apps/todos#readme",
113
117
  "bugs": {
114
- "url": "https://github.com/hasna/todos/issues"
118
+ "url": "https://github.com/hasna/apps/issues"
115
119
  },
116
120
  "engines": {
117
121
  "bun": ">=1.0.0"
@@ -120,9 +124,8 @@
120
124
  "author": "Andrei Hasna <andrei@hasna.com>",
121
125
  "license": "Apache-2.0",
122
126
  "dependencies": {
123
- "@hasna/contracts": "0.14.2",
127
+ "@hasna/contracts": "1.0.2",
124
128
  "@hasna/events": "^0.1.11",
125
- "@hasna/paths": "0.1.0",
126
129
  "@modelcontextprotocol/sdk": "^1.12.1",
127
130
  "chalk": "^5.4.1",
128
131
  "commander": "^13.1.0",
package/postinstall.js CHANGED
@@ -5,13 +5,64 @@
5
5
  // otherwise the legacy ~/.hasna/todos default. Failures are non-fatal: the
6
6
  // runtime creates the same directories on first use.
7
7
  import { existsSync, mkdirSync } from "node:fs";
8
+ // --- Local path resolver -------------------------------------------------
9
+ // @hasna/paths was deleted (hasna/apps#1535, 2026-09-03); this in-package
10
+ // implementation preserves the resolver contract (XDG / macOS home layout
11
+ // honoring HASNA_{CONFIG,DATA,STATE,CACHE}_HOME, with the same env-override
12
+ // and home-override semantics the deleted package had).
13
+ import { homedir as pathsResolverHomedir } from "node:os";
14
+ import { join as pathsResolverJoin } from "node:path";
15
+
16
+ const PATHS_RESOLVER_KIND_ENV = {
17
+ config: "HASNA_CONFIG_HOME",
18
+ data: "HASNA_DATA_HOME",
19
+ state: "HASNA_STATE_HOME",
20
+ cache: "HASNA_CACHE_HOME",
21
+ };
22
+
23
+ function pathsResolverBaseDir(kind, options) {
24
+ const env = options.env ?? process.env;
25
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
26
+ if (typeof override === "string" && override.length > 0) return override;
27
+ const home = options.home ?? pathsResolverHomedir();
28
+ const platform = options.platform ?? process.platform;
29
+ if (platform === "darwin") {
30
+ switch (kind) {
31
+ case "config":
32
+ case "data":
33
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
34
+ case "cache":
35
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
36
+ case "state":
37
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
38
+ }
39
+ }
40
+ switch (kind) {
41
+ case "config":
42
+ return pathsResolverJoin(home, ".config", "hasna");
43
+ case "data":
44
+ return pathsResolverJoin(home, ".local", "share", "hasna");
45
+ case "state":
46
+ return pathsResolverJoin(home, ".local", "state", "hasna");
47
+ case "cache":
48
+ return pathsResolverJoin(home, ".cache", "hasna");
49
+ }
50
+ }
51
+
52
+ function pathsResolverResolve(kind, options) {
53
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
54
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
55
+ }
56
+ function dataDir(options) {
57
+ return pathsResolverResolve("data", options);
58
+ }
8
59
  import { homedir } from "node:os";
9
60
  import { join } from "node:path";
10
61
 
11
62
  const DATA_HOME_OVERRIDE = (process.env["HASNA_DATA_HOME"] || "").trim();
12
63
 
13
64
  try {
14
- const { dataDir } = await import("@hasna/paths");
65
+ // (local resolver @hasna/paths deleted, hasna/apps#1535)
15
66
  const resolved = dataDir({
16
67
  app: "todos",
17
68
  home: process.env["HOME"] || process.env["USERPROFILE"] || homedir(),