@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/CHANGELOG.md ADDED
@@ -0,0 +1,1993 @@
1
+ # Changelog
2
+
3
+ ## 0.17.0
4
+
5
+ - Support the repository vault-token release path with the same signed independent review and package-owned checks. Annotated tags select one delivery lane, preventing local and Actions publishers from racing.
6
+
7
+ ### Minor Changes
8
+
9
+ - MCP surface triage, slice 1 (fleet alignment 2026-09-11; follows the fail-closed
10
+ validation in #1942).
11
+
12
+ - `todos-mcp` is stdio-only. `--http`, `--port <n>` and `MCP_HTTP=1` are refused
13
+ with exit code 2 and a stderr line pointing at `todos-serve`, which owns the
14
+ `POST /mcp` Streamable HTTP endpoint and its auth posture. The MCP bin used to
15
+ start the full `todos-serve` HTTP app with `allowAnonymous: true` from a client
16
+ binary that MCP clients spawn with no credential.
17
+ - Removed 50 files under `src/mcp/tools/` that the server never registered
18
+ (nothing imported 48 of them; `todos-md.ts` was reached only by its own test),
19
+ plus `code-tools.ts`: the `extract_todos` and `watch_source_todos` MCP tools are
20
+ gone (a polling filesystem watcher is not an MCP tool call, and the scan reads
21
+ the agent host's checkout, not the hosted fleet). `todos extract` /
22
+ `todos extract-watch` remain as CLI verbs; the CLI↔MCP parity manifest records
23
+ the `source-index` domain as an intentional gap.
24
+ - `todos stream` defaults to the `todos serve` port (19427) instead of 3000, a
25
+ port nothing in this package listens on.
26
+
27
+ - MCP tools now use the hosted `/v1` routes instead of the local store (PORT-TO-API slice A)
28
+
29
+ Thirty-two MCP tools reached `src/db/*` directly and had no hosted arm at all, so on a
30
+ station whose credential points at the hosted authority they answered from that machine's
31
+ private SQLite file — while the matching CLI verbs were already remote-only. The two doors
32
+ disagreed on the same station. Each of these now calls the real `/v1` route through the
33
+ existing todos client:
34
+
35
+ - **Templates (12)** — `create_template`, `list_templates`, `create_task_from_template`,
36
+ `delete_template`, `update_template`, `init_templates`, `preview_template`,
37
+ `export_template`, `import_template`, `template_history` now use
38
+ `GET|POST /v1/templates`, `GET|PATCH|DELETE /v1/templates/{id}`,
39
+ `GET /v1/templates/{id}/history` and `POST /v1/templates/initialize`.
40
+ `list_template_library` and `write_template_library` answer from the bundled static
41
+ library and no longer drag `bun:sqlite` in behind them.
42
+ - **Tasks (7)** — `upsert_task` (`POST /v1/tasks/upsert`), `claim_task`
43
+ (`POST /v1/tasks/{id}/start`), `release_task` and `extend_task`
44
+ (`GET` + `PATCH /v1/tasks/{id}`, revision-checked), `get_comments`
45
+ (`GET /v1/tasks/{id}/comments`), `list_my_tasks` (`GET /v1/tasks`), `standup`
46
+ (the shared recap over `/v1/tasks`, `/v1/dependencies` and `/v1/agents`).
47
+ - **Queue analytics (7)** — `get_my_workload`, `notify_upcoming_deadlines`,
48
+ `get_sla_breaches`, `get_stale_tasks`, `get_blocked_tasks`, `get_blocking_tasks`
49
+ compute from `GET /v1/tasks` + `GET /v1/dependencies`; `run_doctor` reports
50
+ `GET /v1/integrity` and refuses `apply` on the hosted authority rather than
51
+ pretending a client-side repair happened.
52
+ - **Git trail and verification (7)** — `link_task_to_commit`, `get_task_commits`,
53
+ `find_task_by_commit`, `link_task_git_ref`, `get_task_git_refs`,
54
+ `find_tasks_by_git_ref`, `add_task_verification` use
55
+ `/v1/tasks/{id}/{commits,refs,verifications}`, `/v1/commits/{sha}` and `/v1/refs/{ref}`.
56
+ - **Agents (1)** — `get_agent` resolves against the shared roster on
57
+ `GET /v1/agents/{id}` instead of 404ing every cloud-only agent.
58
+
59
+ The local-store arm of each tool is unchanged and still reachable behind the existing
60
+ explicit local opt-in. The remote reusable-template implementation that was private to the
61
+ CLI `template*` commands moved to `src/cli/template-remote.ts` so both surfaces run one
62
+ implementation rather than two. New hosted-path tests drive every ported tool against a
63
+ real in-process `/v1` origin and assert no database file is created.
64
+
65
+ - Route twelve additional MCP tools through the authoritative Todos `/v1` API.
66
+
67
+ Hosted bulk create and bulk delete now use bounded server-owned transactions. Bulk create commits every task and dependency edge together or commits nothing, including parent-guarded and plan-guarded tasks on the already-held PostgreSQL transaction client. Bulk delete snapshots authoritative child relationships, applies order-independent force semantics, requires `todos:*` for force, and returns a complete per-request receipt. Unsupported or contradictory hosted receipts fail closed without local fallback.
68
+
69
+ Archive and workload-rebalance mutation reads exhaust stable authoritative totals through bounded scalar-status pages and refuse before mutation when a selection is incomplete or exceeds 10,000 tasks. `archive_completed` uses `updated_at`, matching the established local age contract. `get_archived_tasks` now requests one server-filtered `archived_only` page, including subtasks, so a tiny archive remains readable inside a fleet-sized live corpus. Agent rosters, recent activity, task history, and dependency analytics are storage-bounded; large legacy unpaged reads fail loudly instead of materializing an unbounded dataset.
70
+
71
+ This release is server-first: deploy and prove the new bulk routes, bounded task-history response, and bounded dependency behavior before publishing a client version that depends on them.
72
+
73
+ ### Patch Changes
74
+
75
+ - Raise the HTTP server's default request allowance from 120 to 12,000 per minute per peer. Preserve canonical and legacy environment overrides, reject invalid or excessive budgets before startup, and retain authentication, proxy trust, and Retry-After behavior.
76
+
77
+ Write the complete CLI JSON manual through the existing output helper so piped output remains parseable.
78
+
79
+ - Harden the hosted MCP slice by preserving template assignees and task-list bindings, binding exact-agent reads to validated response identities, removing placeholder claims, pushing bounded dependency pagination into the storage adapters and generated `/v1` SDK, and refusing unsupported commit and verification timestamps instead of dropping them.
80
+
81
+ ## 0.16.1
82
+
83
+ ### Patch Changes
84
+
85
+ - 012c2ad: Fail closed on every surface (hasna/apps#1720 validation, round 1).
86
+
87
+ - `todos-mcp` (stdio) decides its authority BEFORE it serves. With no
88
+ credential anywhere it exits non-zero with the `REMOTE_API_CONFIG_MISSING`
89
+ line on stderr — naming the Keychain item `hasna.credentials.todos.api-key`,
90
+ `~/.hasna/todos/config/credentials`, `HASNA_TODOS_API_KEY` and the
91
+ `HASNA_TODOS_LOCAL=1` opt-in — before `initialize` is answered, and creates
92
+ no `todos.db`. It used to open (and create) the local store on every start.
93
+ - On the hosted route the MCP server refuses the on-box SQLite store
94
+ altogether: every local-only tool family (machines, dispatch, templates,
95
+ handoffs/boards/time, runs, knowledge, …) and every `todos://` resource
96
+ returns `REMOTE_COMMAND_UNSUPPORTED` naming the opt-in instead of reading an
97
+ empty local file (`resources/read todos://projects` no longer answers `[]`
98
+ while the fleet holds thousands of projects). Agent focus is answered from
99
+ the session on the hosted route, never from the local agents table.
100
+ - `REMOTE_API_*` / `REMOTE_COMMAND_*` refusals reach the MCP client under their
101
+ own code instead of `UNKNOWN_ERROR`; they name sources only, never a value.
102
+ - `@hasna/todos/sdk`: `new TodosClient()` with nothing resolved throws
103
+ `TODOS_CREDENTIAL_MISSING` (the same code `createTodosV1Client` used) instead
104
+ of degrading to `http://localhost:19427`. The local serve is reachable only
105
+ under the explicit `HASNA_TODOS_LOCAL=1` opt-in, which prints its one-line
106
+ notice.
107
+ - The local store honours `HASNA_HOME` (absolute, non-blank) as the `~/.hasna`
108
+ root, the same rule `@hasna/contracts` applies to the credentials file.
109
+ - `hasna.contract.json` declares the CLI and MCP surfaces as `api-key`
110
+ (hosted via the contracts chain; local only under the opt-in).
111
+
112
+ ## 0.16.0
113
+
114
+ ### Migrating from 0.15.52
115
+
116
+ No data migration is required, but four things change under an existing
117
+ install:
118
+
119
+ 1. **A credential is required for the hosted surface, and the retired
120
+ locations are no longer read.** The `./sdk` client used to read a credential
121
+ out of `~/.todos/config.json` and to prefer the unprefixed `TODOS_URL` and
122
+ key names; that chain is gone. Every surface resolves through the
123
+ `@hasna/contracts` client chain — the macOS Keychain item
124
+ `hasna.credentials.todos.api-key`, then `~/.hasna/todos/config/credentials`
125
+ (owner-only `0400`/`0600`), then `HASNA_TODOS_API_KEY` — and `todos storage
126
+ status --json` names the tier that answered. `~/.hasna/fleet-env`,
127
+ `~/.hasna/cloud`, `~/.config/hasna`, `$XDG_CONFIG_HOME` and
128
+ `~/.todos/config.json` are not read. With no credential the CLI exits
129
+ non-zero (`REMOTE_API_CONFIG_MISSING`) instead of serving local rows; the MCP
130
+ server keeps serving and refuses each credential-gated call with a typed
131
+ payload — `REMOTE_API_CONFIG_MISSING` for the shared-API tools,
132
+ `API_DATABASE_FALLBACK_FORBIDDEN` for the on-box ones (item 3).
133
+ 2. **`todos plans`, `todos task-lists` (aliases `lists`, `tl`) and the template
134
+ commands are shared-API only.** They refuse `HASNA_TODOS_DB_PATH`,
135
+ `TODOS_DB_PATH`, `HASNA_TODOS_LOCAL` and `TODOS_LOCAL` before startup — any
136
+ one of them set is enough to refuse the command — and need
137
+ `HASNA_TODOS_API_URL` and `HASNA_TODOS_API_KEY` (or saved account
138
+ credentials). The five MCP plan tools (`create_plan`, `list_plans`,
139
+ `get_plan`, `update_plan`, `delete_plan`) and the five MCP task-list tools
140
+ (`create_task_list`, `list_task_lists`, `get_task_list`, `update_task_list`,
141
+ `delete_task_list`) no longer read the local store either.
142
+ `todos template-library` is unchanged: it renders the library bundled in the
143
+ package and never opens a store.
144
+ 3. **Most MCP tools that read the on-box store now need the local opt-in.**
145
+ The MCP server no longer opens the on-box SQLite store implicitly, so every
146
+ tool that still reads it directly — the template family (`create_template`,
147
+ `list_templates`, `init_templates`, `preview_template`, `export_template`,
148
+ `import_template`, `create_task_from_template`), tags and labels, stale and
149
+ blocked work, `doctor`, the local run ledger, handoffs,
150
+ review queues, retrospectives, risks, knowledge records, backups and
151
+ integrity checks, calendar, boards, focus/time reports and dispatches — fails
152
+ on the default posture. Measured at 0.16.0 with `TODOS_PROFILE=full` on the
153
+ default (no-credential, no local opt-in) posture, 68 of the 125
154
+ zero-required-argument tools log `API_DATABASE_FALLBACK_FORBIDDEN` and answer
155
+ the typed `{"code":"API_DATABASE_FALLBACK_FORBIDDEN"}` payload, whose
156
+ `suggestion` names the opt-in. A further 18 return the typed
157
+ `REMOTE_API_CONFIG_MISSING`: the 16 tools served by the shared API that need
158
+ a credential — `list_tasks`, `list_projects`, `list_agents`, `get_next_task`,
159
+ `get_status`, `bootstrap`, `get_context`, `get_my_tasks`, `get_my_workload`,
160
+ `get_health`, `standup`, `list_my_tasks` and `machines_register` /
161
+ `machines_list` / `machines_heartbeat` / `machines_topology` — plus
162
+ `list_plans` and `list_task_lists`. Five more refuse caller input or local
163
+ state with `INVALID_INPUT` / `ENCRYPTION_KEY_UNAVAILABLE` /
164
+ `ENCRYPTED_PAYLOAD_INVALID`, and five answer a readable text refusal
165
+ ("Provide agent_id, id, or name."). No zero-argument tool returns an opaque
166
+ `UNKNOWN_ERROR` — on this default posture or under the `HASNA_TODOS_LOCAL=1`
167
+ opt-in below. Run the MCP server with
168
+ `HASNA_TODOS_LOCAL=1` to keep using them. That opt-in is honoured only when
169
+ the environment configures no authority or credential of its own — a
170
+ configured environment outranks it, so with `HASNA_TODOS_API_KEY` (or
171
+ `HASNA_TODOS_API_URL`) set, `HASNA_TODOS_LOCAL=1` is ignored and the on-box
172
+ MCP tools are unreachable in 0.16.0. Converting them to the shared API is
173
+ tracked separately; the current boundary is recorded in
174
+ `apps/todos/docs/native-storage.md`.
175
+ 4. **The advertised command list depends on the resolved route.** `todos --help`,
176
+ `todos manual` and the generated completions list what the route exposes: 76
177
+ root commands on the hosted route once a credential resolves, 75 with none
178
+ configured (`stale-lock-handoff` is advertised only when the shared-API
179
+ authority is), and the full 166 with `HASNA_TODOS_LOCAL=1`. No command was
180
+ removed; every verb still resolves once its posture is configured. The
181
+ per-posture counts are in `apps/todos/docs/cli-help.md`.
182
+
183
+ Everything else still runs offline with `HASNA_TODOS_LOCAL=1` (again, only when
184
+ the environment configures no authority or credential of its own). The
185
+ per-surface detail is in `apps/todos/docs/PLAN_API.md`, `TASK_LIST_API.md`,
186
+ `TEMPLATE_API.md`, `TASK_QUERY_API.md` and `native-storage.md`.
187
+
188
+ ### Minor Changes
189
+
190
+ - 96169f8: Resolve credentials through the `@hasna/contracts` client chain (hasna/apps#1720).
191
+
192
+ The CLI, the MCP server and the `./sdk` client no longer carry a credential
193
+ chain of their own. All three call the one resolver in `@hasna/contracts`
194
+ (bumped to 1.0.2), which reads, per call: an explicit `apiKey` argument — the
195
+ resolver's own `--api-key`/`--profile` flag spellings belong to its other
196
+ consumers; the `todos` CLI exposes no hosted-credential flag of its own, so
197
+ tier 1 is reachable only through the SDK/client option —
198
+ then `HASNA_TODOS_API_KEY_OVERRIDE` / `HASNA_PROFILE` /
199
+ `HASNA_TODOS_API_KEY_REF`, then the macOS Keychain item
200
+ `hasna.credentials.todos.api-key`, then `~/.hasna/todos/config/credentials`
201
+ (owner-only 0400/0600), then `HASNA_TODOS_API_KEY`. The authority follows the
202
+ same ladder — `HASNA_TODOS_API_URL`, the Keychain `api-url` item, the
203
+ credentials file — and now DEFAULTS to the fleet gateway
204
+ `https://api.hasna.com/todos` once a credential resolves, so a key alone is a
205
+ complete configuration. Resolving per call is what makes a key rotation heal a
206
+ long-lived shell, MCP server or agent without restarting it: a `TodosClient`
207
+ held for hours re-resolves the credential on every request, so the next request
208
+ after a rotation carries the new key. The two deliberate exceptions are an
209
+ explicit `apiKey` argument (tier 1, a pin the caller owns) and the service
210
+ authority, which is fixed for the life of a client so a credential written for
211
+ one authority is never sent to another.
212
+
213
+ What this removes:
214
+
215
+ - `getLocalApiConfig()` and `LocalApiConfig` (breaking, hence minor), together
216
+ with the `apiUrl` / `apiKey` fields of `TodosConfig`. The SDK used to read a
217
+ credential out of `~/.todos/config.json` — an ordinary-permission file that
218
+ unrelated `todos config` writes rewrite wholesale — and to prefer `TODOS_URL`
219
+ and an unprefixed key name over the canonical `HASNA_TODOS_*` pair, so an
220
+ operator who configured the documented names silently got the localhost
221
+ default with no credential.
222
+ - `requireTodosRemoteAuthorityEnv()` (breaking), the pre-normalisation pass the
223
+ resolver now performs itself.
224
+ - The retired paths, everywhere: nothing reads `~/.hasna/fleet-env`,
225
+ `~/.hasna/cloud`, `~/.config/hasna` or `$XDG_CONFIG_HOME`. `@hasna/todos/testing`
226
+ delivers a fixture key to `~/.hasna/todos/config/credentials` at 0600 instead.
227
+ - The legacy-env DEPRECATED stderr notice, which contracts 1.0.1 drops:
228
+ `HASNA_TODOS_API_KEY` is a legitimate tier, it just sits below disk.
229
+
230
+ What this adds:
231
+
232
+ - `@hasna/todos/sdk` exports `resolveTodosSdkTransport`, `createTodosV1Client`
233
+ and `TODOS_LOCAL_SERVE_URL`, so a consumer can see WHICH tier supplied its
234
+ credential (never the value) and build the hosted `/v1` client without writing
235
+ a private copy of the chain.
236
+ - `todos storage status` reports `api_url_source`, `api_key_source` and
237
+ `api_key_tier`.
238
+ - `@hasna/todos/testing` exports `TODOS_CREDENTIALS_FILE_SEGMENTS` and
239
+ `TODOS_TEST_KEYCHAIN_ACCOUNT`. The scrub list gained the deliberate pointers
240
+ (`HASNA_TODOS_API_KEY_OVERRIDE`, `HASNA_TODOS_API_KEY_REF`, `HASNA_PROFILE`)
241
+ and now REMOVES entries rather than blanking them, because a declared-but-blank
242
+ credential is a refusal rather than an absence. Blanking still means "unset"
243
+ at the Todos seam, so existing consumer fixtures keep working.
244
+
245
+ Behaviour worth knowing about:
246
+
247
+ - Hosted mode with no credential fails closed — non-zero exit, no SQLite
248
+ fallback, no local-fallback event — and the message now names every tier it
249
+ consulted, so the remedy is in the error. This is a change from 0.15.52 for
250
+ the nothing-configured case, which served the on-box store behind a
251
+ local-fallback notice: 0.16.0 requires the deliberate `HASNA_TODOS_LOCAL=1`
252
+ opt-in to serve it. The `./sdk` surface differs for that ONE case by design:
253
+ `new TodosClient()` falls to the local `todos-serve` with a stderr line,
254
+ `createTodosV1Client()` throws. Every other refusal throws on every surface.
255
+ - The bundled `@hasna/todos-sdk` package (`apps/todos/sdk`, published separately
256
+ and NOT a workspace member, so it carries no changeset of its own — see
257
+ hasna/apps#1787) now documents the environment variables it reads and prints
258
+ one line when it falls back to the local `todos-serve`. Its canonical
259
+ `HASNA_TODOS_*` names always win; the unprefixed spellings are legacy and are
260
+ accepted for one release only.
261
+ - A declared-but-blank `*TODOS_*` variable no longer disables the Keychain tier.
262
+ Removing a blank means handing the resolver a COPY of the environment, and
263
+ @hasna/contracts gates its ambient tiers on object identity, so the copy used
264
+ to switch tier 3 off for the whole run — silently dropping a station from its
265
+ Keychain identity to the next one in the chain. The gate is now decided before
266
+ normalising and carried across as `keychain.enabled`.
267
+ - Local mode (`HASNA_TODOS_LOCAL=1`, alias `TODOS_LOCAL=1`) is honoured only
268
+ when the environment configures no authority and no credential, and is
269
+ answered BEFORE the resolver runs, so an unhosted run reads neither the
270
+ Keychain nor the credential file. Every local run now prints one line on
271
+ stderr saying it is local.
272
+ - A credential with no URL used to be refused as a half-configured pair; it now
273
+ resolves the fleet gateway.
274
+ - The command list `todos --help`, `todos manual` and the generated
275
+ completions print is now route-dependent. 0.15.52 advertised all 166
276
+ commands to every caller because the local fallback was implicit; 0.16.0
277
+ advertises the 76 commands the hosted route exposes once a credential
278
+ resolves (75 with none configured — `stale-lock-handoff` is advertised only
279
+ when the shared-API authority is) and the full 166 only
280
+ once the local opt-in is set, because the on-box commands fail closed on the
281
+ default posture. No command was removed —
282
+ every verb still resolves when its posture is configured; run
283
+ `HASNA_TODOS_LOCAL=1 todos --help` to see the on-box families. The
284
+ curation mechanism itself is unchanged from 0.15.52; only its trigger moved
285
+ with the removal of the implicit local fallback. See
286
+ `apps/todos/docs/cli-help.md`.
287
+ - `todos storage status` now reports the resolver's credential-source
288
+ disagreement as a `warnings` array in `--json` (the field already existed
289
+ and was always `[]` on the hosted branch) and prints each entry as a yellow
290
+ stderr line in human mode. The entries name the sources and env key names
291
+ that disagree — never a credential value — and the array is additive, so no
292
+ consumer contract breaks.
293
+ - A 401/403 from the authority no longer echoes the server's response body: the
294
+ transport cancels it unread, because that body is the one place a rejected
295
+ request can reflect credential material back into logs. The refusal still
296
+ names the authority and the credential source.
297
+
298
+ ### Patch Changes
299
+
300
+ - The published tarball now carries the migration notes that were repo-only:
301
+ `CHANGELOG.md` and the API documents the 0.16.0 breaking changes reference
302
+ (`docs/PLAN_API.md`, `docs/TASK_LIST_API.md`, `docs/TEMPLATE_API.md`,
303
+ `docs/TASK_QUERY_API.md`, `docs/native-storage.md`). npm consumers previously
304
+ received only the README's Upgrading section, so the per-API breaking notes
305
+ were unreachable from an installed package.
306
+ - The publish gate's reviewed-file allowlist now admits the six documentation
307
+ paths 0.16.0 packs (`CHANGELOG.md` and the five `docs/*.md` files), so
308
+ `npm publish` no longer aborts at `prepublishOnly` with `package-files-extra`
309
+ for the very files this release added. `src/lib/public-release-gate.test.ts`
310
+ now validates the real `package.json`, not only a fixture, so a `files[]`
311
+ change the allowlist has not reviewed fails in tests instead of at publish
312
+ time.
313
+ - `todos config --set` warns when the key looks like a credential or an
314
+ authority (`apiKey`, `api_url`, `*_token`, …) and no longer echoes the value.
315
+ 0.16.0 removed the last reader of a credential from `config.json`, so a silent
316
+ `Set apiKey = …` told a migrating operator the write took effect while nothing
317
+ would ever consult it — and printed it to the terminal. The value is still
318
+ stored for a caller that reads the file itself; the warning names the tiers
319
+ that are read.
320
+ - The README's account of the published tarball is corrected: the package ships
321
+ `CHANGELOG.md` and the five API documents alongside `dist/`, rather than
322
+ "this README and `dist/` only". The zero-argument MCP census in these 0.16.0
323
+ notes is corrected to sum to 125 (27 typed ok plus two `{"allowed":…}` policy
324
+ verdicts, which are not coded error envelopes) and no longer lists `standup`
325
+ or `status` among the `API_DATABASE_FALLBACK_FORBIDDEN` tools — both answer
326
+ `REMOTE_API_CONFIG_MISSING`, as the same entry already said.
327
+ - The publish gate (`scripts/verify-public-release.ts`, publish mode) now runs
328
+ the package test suite before it packs, so `npm publish` can no longer ship a
329
+ tree whose own suite never ran — the gap left by a CI run that aborts on an
330
+ unrelated package before `@hasna/todos:test` executes. The `prepublishOnly`
331
+ command string is unchanged (the gate itself asserts it byte-for-byte).
332
+ - Every `REMOTE_API_*` refusal now carries the remedy for its own code instead of
333
+ the "configure the shared Todos API" advice for all of them. A 401
334
+ (`REMOTE_API_UNAUTHORIZED`) means the credential is present and was rejected —
335
+ re-save or reissue it, which is a different fix from setting
336
+ `HASNA_TODOS_API_URL`; a timeout, an unreachable authority, a bad URL, a
337
+ rejected redirect and an incompatible route each name their own remedy. The
338
+ map lives in `src/mcp/remote-authority.ts` and is used by both the typed error
339
+ class and the formatter, so the two cannot drift.
340
+ - The two remaining zero-argument MCP tools that still answered an opaque
341
+ `{"code":"UNKNOWN_ERROR"}` under the documented `HASNA_TODOS_LOCAL=1` remedy —
342
+ `create_retrospective` and `finish_task_run` — now return the typed
343
+ `INVALID_INPUT` refusal. Both refuse caller input (a missing scope; a missing
344
+ run id/key), so they are the same class the formatter already types; they were
345
+ throwing plain `Error`s. This makes the "no zero-argument tool returns an
346
+ opaque `UNKNOWN_ERROR`" claim true on BOTH postures: measured at 0.16.0 with
347
+ `TODOS_PROFILE=full`, the default posture census is unchanged
348
+ (68 `API_DATABASE_FALLBACK_FORBIDDEN` / 18 `REMOTE_API_CONFIG_MISSING` /
349
+ 3 `INVALID_INPUT` / 1 `ENCRYPTION_KEY_UNAVAILABLE` /
350
+ 1 `ENCRYPTED_PAYLOAD_INVALID` / 5 readable text / 27 typed ok / 2 policy
351
+ verdicts — `check_workspace_permission` and `check_runner_sandbox` answer a
352
+ `{"allowed":…}` body, not a coded error envelope — which is all 125) and the
353
+ `HASNA_TODOS_LOCAL=1` census is 0 `UNKNOWN_ERROR` with 5 `INVALID_INPUT`.
354
+ - The on-box MCP tools that were not converted to the shared API are documented
355
+ as requiring the deliberate `HASNA_TODOS_LOCAL=1` / `TODOS_LOCAL=1` opt-in,
356
+ and every one of them now refuses with a typed payload instead of an opaque
357
+ `UNKNOWN_ERROR`. They still read the on-box store, which is no longer opened
358
+ implicitly, so on the default posture 68 of the 125 zero-required-argument
359
+ tools at 0.16.0 with `TODOS_PROFILE=full` answer the typed
360
+ `{"code":"API_DATABASE_FALLBACK_FORBIDDEN"}` payload (whose `suggestion` names
361
+ the opt-in) while the server logs the same code. The 16 shared-API tools that
362
+ need a credential (`list_tasks`, `list_projects`, `list_agents`,
363
+ `get_next_task`, `get_status`, `bootstrap`, `get_context`, `get_my_tasks`,
364
+ `get_my_workload`, `get_health`, `standup`, `list_my_tasks`,
365
+ `machines_register`, `machines_list`, `machines_heartbeat`,
366
+ `machines_topology`) plus `list_plans` and `list_task_lists` return the typed
367
+ `REMOTE_API_CONFIG_MISSING`; five more refuse caller input or local state with
368
+ `INVALID_INPUT` / `ENCRYPTION_KEY_UNAVAILABLE` / `ENCRYPTED_PAYLOAD_INVALID`,
369
+ and five answer a readable text refusal; the remaining 29 answer data (27) or
370
+ a `{"allowed":…}` policy verdict (2). That is all 125 accounted for: no
371
+ zero-argument tool returns `UNKNOWN_ERROR` on the default posture, and the
372
+ `HASNA_TODOS_LOCAL=1` posture has none either (see the entry above). This is
373
+ the same defect class the
374
+ ten plan/task-list tools were converted out of, now closed at the formatter
375
+ chokepoint every handler error passes through; converting the on-box tools
376
+ themselves to the shared API is tracked separately. On an environment that
377
+ sets `HASNA_TODOS_API_KEY` or `HASNA_TODOS_API_URL` the opt-in is ignored, so
378
+ these tools are unreachable there. See the 0.16.0 Migrating section and
379
+ `apps/todos/docs/native-storage.md`.
380
+ - 8f8e88871: Align the exact `@hasna/contracts` pin with the 1.0.2 optional secrets peer release.
381
+ - b269abea4: Return the MCP shared-API refusal as a typed, actionable payload instead of
382
+ `UNKNOWN_ERROR`. The plan and task-list MCP tools are served only by the
383
+ authenticated shared API, but the guard threw a plain `Error`, which the MCP
384
+ error formatter sanitizes to `UNKNOWN_ERROR` ("An unexpected error occurred.
385
+ Check server logs for details.") — so a configuration requirement reached
386
+ clients as an opaque server fault and the reason ("Plan tools require the
387
+ authenticated Todos API") was visible only on stderr. The ten tools
388
+ `create_plan`, `list_plans`, `get_plan`, `update_plan`, `delete_plan`,
389
+ `create_task_list`, `list_task_lists`, `get_task_list`, `update_task_list` and
390
+ `delete_task_list` now return
391
+ `{"code":"REMOTE_API_CONFIG_MISSING","message":…,"suggestion":…}` naming the
392
+ missing configuration — the same code the CLI prints — in local mode (the
393
+ deliberate `HASNA_TODOS_LOCAL`/`TODOS_LOCAL` opt-in, which these tools do not
394
+ honour) and on a station with no credential. That change touched only those ten
395
+ tools; the 0.16.0 Patch entry above then extended the same typed refusal to
396
+ every remaining on-box tool at the formatter chokepoint, so this entry's
397
+ "no other payload changed" scope is those ten tools, not the release.
398
+ - 92d9dac: Stop the `./sdk` client sending the station's fleet credential to a
399
+ caller-supplied `baseUrl` (hasna/apps#1781 review follow-up, regression from
400
+ hasna/apps#1788).
401
+
402
+ Making credential resolution per-call gave `TodosClient` a `currentApiKey()`
403
+ that re-ran the resolver on every request — but it re-ran it with no `baseUrl`,
404
+ dropping the tier-1 authority the constructor was given. For
405
+ `new TodosClient({ baseUrl: X })` with no `apiKey`, construction correctly took
406
+ the explicit-authority arm and held no credential, and then every request
407
+ resolved the AMBIENT chain (Keychain, `~/.hasna/todos/config/credentials`,
408
+ `HASNA_TODOS_API_KEY`) and attached that key as `x-api-key` on the way to `X`.
409
+ `baseUrl` is a documented public option, and the shape with no key is what
410
+ local-serve and test-double callers write — including this repo's own tests
411
+ against `http://localhost:19427` — so a hosted credential was going to an
412
+ unauthenticated on-box process. It also contradicted the guarantee the same
413
+ change documented: the service authority is fixed for the life of a client
414
+ because a credential written for one authority must never be sent to another.
415
+
416
+ The authority pin now binds the credential. When tier 1 named the authority the
417
+ chain is not consulted again: the client sends the credential it was
418
+ CONSTRUCTED with — an explicit `apiKey`, or nothing. Per-call re-resolution, and
419
+ the rotation-healing it exists for, still applies to a client that resolved its
420
+ own hosted authority. Regression tests cover `new TodosClient({ baseUrl })` with
421
+ no key, with a key, and the rotating hosted client that must still re-resolve.
422
+
423
+ Also in this change:
424
+
425
+ - `createTodosV1Client()`'s per-request `fetch` wrapper normalises the headers
426
+ init through `Headers` instead of an object spread with a
427
+ `Record<string, string>` cast. The generated client hands it a plain record
428
+ today, but that was asserted only in a comment — a regenerated client passing
429
+ a `Headers` instance or a tuple array would have spread to `{}` and silently
430
+ dropped every header, `Content-Type` included. The wrapper also no longer
431
+ forwards `notice` into its re-resolution, which could print the "LOCAL mode —
432
+ … not the hosted fleet" line while the client was still addressing its
433
+ original hosted authority.
434
+ - `resolveTodosSdkTransport()` walks the credential chain once instead of twice.
435
+ `resolveClientTransport()` resolves the credential but returns only its
436
+ source, so reading the value meant a second full pass — and on macOS each pass
437
+ spawns `/usr/bin/security`, so a per-request surface paid two spawns per
438
+ request for one answer (measured: 3 `security` invocations per resolution, now
439
+ 2). The resolved value is handed down as the chain's tier-1 argument, so the
440
+ second pass short-circuits; the reported `apiKeySource` is still the true tier
441
+ (`keychain:…`, a file path, an env name), never `"explicit apiKey argument"`.
442
+
443
+ Bundled `@hasna/todos-sdk` (`apps/todos/sdk`, published separately and NOT a
444
+ workspace member, so changesets cannot version it — bumped by hand to **0.2.0**,
445
+ which is the release vehicle for the behaviour hasna/apps#1788 documented but
446
+ shipped without one):
447
+
448
+ - A resolved credential with no authority no longer targets `localhost`. The
449
+ local-mode notice was gated on "no URL and no key", but the URL still fell
450
+ back to `http://localhost:19427` whenever no URL was named — so with
451
+ `HASNA_TODOS_API_KEY` set and no `HASNA_TODOS_API_URL`, the client went local,
452
+ said nothing, and forwarded the fleet credential to an unauthenticated
453
+ `todos-serve` on the box. A key now selects the fleet gateway
454
+ `https://api.hasna.com/todos`, which is what the `@hasna/todos` `./sdk`
455
+ surface already answered for that identical environment; the two clients no
456
+ longer disagree about where a key is sent. Local mode is unchanged where the
457
+ ruling puts it: no URL and no key, with one stderr line saying so.
458
+ - The behaviour has tests. `__resetTodosLocalModeNotice`,
459
+ `TODOS_API_URL_ENV_KEYS`, `TODOS_API_KEY_ENV_KEYS` and
460
+ `TODOS_LOCAL_SERVE_URL` were added as a test seam and a public surface but
461
+ exported from nowhere and referenced by nothing; they are now re-exported from
462
+ the package index and exercised, alongside the new `TODOS_DEFAULT_FLEET_URL`.
463
+ - `README.md` documented the hole ("Setting either one turns the notice off and
464
+ uses what you set") and now documents the rule, with the default authority in
465
+ the configuration table.
466
+
467
+ - 49d75dd: Reject an explicitly empty `active --project` filter instead of silently returning unfiltered work.
468
+ - 500f99d: `@hasna/todos/testing`'s `deliverTodosApiKeyViaDisk` stopped delivering fixture keys
469
+ through the legacy `~/.hasna/cloud` fallback: @hasna/contracts 0.14.2 demoted
470
+ that tier to a NOISY deprecated fallback, so a CLI subprocess test delivering
471
+ via cloud would print the DEPRECATED notice to stderr and break stderr-exact
472
+ assertions. The tier this landed on was superseded later in the same release by
473
+ the credential-resolver adoption (96169f8 above): the shipped helper writes
474
+ `$HOME/.hasna/todos/config/credentials` at 0600, and no surface reads
475
+ `~/.hasna/fleet-env`.
476
+ - 6f4238c: Project and task-list slugs are no longer auto-prefixed with `todos-`. A new
477
+ project registered from the name `apps` now derives the bare slug `apps`
478
+ (previously `todos-apps`); task lists follow the same rule, matching the fleet
479
+ convention that a repo project's task-list slug IS the repo short name.
480
+
481
+ - New projects/task lists: `task_list_id`/`slug` = the sanitized kebab-case
482
+ name, verbatim — no prefix is prepended (SQLite, Postgres, and the
483
+ project-registration authority all agree; v1 `/projects` and `/task-lists`
484
+ derive the same way).
485
+ - Explicit user-supplied slugs are stored verbatim, so a deliberate
486
+ `todos-<name>` id is still honored.
487
+ - Legacy stored ids are untouched (no rows renamed) and keep resolving by
488
+ their stored value, including the registration authority's bind of
489
+ pre-normalization `todos-<slug>` rows. Duplicate names still fail with
490
+ `PROJECT_SLUG_CONFLICT`; the collision surface for the hosted fleet's
491
+ existing prefixed rows is planned in `apps/todos/docs/slug-prefix-
492
+ normalization.md` (runner: `apps/todos/scripts/normalize-slug-prefixes.ts`,
493
+ dry-run by default, case-seeded collision resolutions, separate reviewed
494
+ run).
495
+
496
+ - f00607b: Remove the bundled web dashboard (Vite/React/shadcn SPA at `dashboard/`): the tree, the `dashboard` workspace + `files` entry, the `build:dashboard` script, the dashboard step in `build`, the Dockerfile/.dockerignore references, the CI dashboard job, and the server's static-file serving (`resolveDashboardDir`/`serveStaticFile`/SPA fallback) with unknown non-API routes now always 404 JSON. The server startup browser auto-open and the `--no-open` flag / `TODOS_NO_OPEN` env are gone; the headless-boundary manifest drops the dead `local_dashboard` optional surface. REST API, MCP HTTP, OAuth-adjacent auth postures, the CLI, and the SDK are unchanged — `todos serve` / `todos-serve` still serve /api/\* and /mcp.
497
+ - 6d8782c: `todos serve` / `todos-serve` now read their accepted key from the server's own
498
+ variable, `HASNA_TODOS_SERVER_API_KEY`, instead of the client credential names
499
+ (`HASNA_TODOS_API_KEY` / `TODOS_API_KEY`). One name no longer plays both roles
500
+ on opposite sides of the same trust boundary: exporting the fleet client key on
501
+ a workstation can no longer silently become the local server's accepted key,
502
+ and rotating the client key no longer changes what the local server accepts.
503
+
504
+ The client names remain a documented silent fallback for one release, so an env
505
+ written before 2026-09-05 keeps working. `todos serve` prints one line at
506
+ startup naming which variable supplied its accepted key, flagging the
507
+ deprecated spelling when a fallback name was used.
508
+ - 5a20d230a: Restore the admitted-local redaction's delete-not-blank semantics in
509
+ stage-a — the emitted statement deletes the legacy unprefixed authority variable
510
+ from the environment — and align the public-text-boundary exemption (and its
511
+ gate tests) with that emitted delete shape. The #1829 blanking workaround
512
+ contradicted stage-a's documented delete-not-blank law (a declared-but-blank
513
+ authority is refused loudly downstream) and left the gate stripping a shape the
514
+ source no longer emitted; the release-review P1 (0d22a7aa2) requires the
515
+ exemption to match the delete statement exactly, with every other spelling — a
516
+ read, a blanking assignment, any other module — still failing the boundary. The
517
+ SDK README documents the canonical HASNA_TODOS_API_URL / HASNA_TODOS_API_KEY
518
+ names only.
519
+ - 17b09fae3: Route all CLI task-list aliases through authenticated shared storage, with complete task detail, status controls, preserving deletion and database-selector rejection before startup.
520
+
521
+ **Breaking for local-SQLite users** (hasna/apps#2027). `todos task-lists` —
522
+ and its aliases `lists` and `tl` — read and write the authenticated shared API
523
+ only. Selecting an on-box database (`HASNA_TODOS_DB_PATH`, `TODOS_DB_PATH`,
524
+ `HASNA_TODOS_LOCAL`, `TODOS_LOCAL`) is refused before startup with
525
+ `Task-list commands require the authenticated shared API. Unset … and configure
526
+ HASNA_TODOS_API_URL and HASNA_TODOS_API_KEY, or saved account credentials.`; a
527
+ run with no credential fails closed with `REMOTE_API_CONFIG_MISSING`. Task
528
+ lists carry a persisted `status` on the shared record, task detail is read in
529
+ full, and deletion is confirmed by a checked receipt. As with plans, the
530
+ deletion option contract changed with the move: `--delete` removes an empty
531
+ task list, and `--force` (which requires `--delete`) detaches linked tasks and
532
+ plans while preserving their content and history.
533
+ - b29183485: Route CLI plans through shared authenticated storage and preserve Markdown exports with an explicitly chosen local root, checked deletion receipts and accurate artifact-failure reporting.
534
+
535
+ **Breaking for local-SQLite users** (hasna/apps#2034). `todos plans` and every
536
+ action it carries — `--add`, `--show`, `--delete`, `--complete`,
537
+ `--link-project`, `--rollback-project-link`, `--artifact`, `--write-artifacts`
538
+ — read and write the authenticated shared API only. Selecting an on-box
539
+ database is refused before startup with `Plan commands require the
540
+ authenticated shared API. Unset … and configure HASNA_TODOS_API_URL and
541
+ HASNA_TODOS_API_KEY, or saved account credentials.`, and a run with no
542
+ credential fails closed with `REMOTE_API_CONFIG_MISSING`. Two option contracts
543
+ changed with the move: `--artifact` and `--write-artifacts` compare local
544
+ Markdown against shared plan data and now require an explicit
545
+ `--artifact-root <directory>`; `--delete` removes an empty plan, and `--force`
546
+ (which requires `--delete`) detaches linked tasks and lists while preserving
547
+ their content and history. Deletions are confirmed by a checked server receipt,
548
+ so a plan is never reported deleted on an unverified response.
549
+ - aa503b7a4: Add an explicitly privileged, bounded atomic project snapshot reconciliation API with durable receipts and content-preserving historical tombstones.
550
+ - 3e63609f9: Resolve hosted credentials through the shared `@hasna/contracts` client chain by pinning the exact `@hasna/contracts` dependency at 1.0.2: the CLI, the MCP server and the `./sdk` client all call `resolveClientTransport`, which adds the macOS Keychain tier and no longer reads the retired `~/.hasna/fleet-env` and `~/.hasna/cloud` disk tiers. The shipped 0.16.0 tarball carries this pin; this record keeps the change visible to the changeset tooling for the next version bump.
551
+ - 3655d23f5: Preserve historical PR-group rows during SQLite lineage upgrades, stop on statement failures, and restore foreign-key settings after rollback.
552
+ - bc5a45ce1: Keep machine command registration and help store-free while authenticating shared machine actions. Refresh API regression fixtures for machine and task coordination capabilities.
553
+ - bc5a45ce1: Bind machine migration capability, writes and receipts to the same authenticated tenant and key identity. Reject credential changes before importing and ambiguous machine task selectors before reading tasks.
554
+ - 206296587: Keep plan-project linkage and initial receipt readback in one membership transaction, and distinguish later state drift from an already committed operation without rewriting its receipt.
555
+ - fca2fae5d: Create private, integrity-validated standalone SQLite backups that reopen read-only on macOS, preserving committed WAL data and existing backups on validation failure.
556
+ - fa16b463a: Require complete bounded stdout and stderr evidence from a single command execution before the release gate accepts captured output.
557
+ - bc5a45ce1: Preserve machine identities in storage snapshots and add authenticated shared machine registry operations for CLI and MCP. Refuse older APIs before machine imports and prevent implicit SQLite fallback on API clients.
558
+ - 4941cff8a: Route MCP plan workflows through authenticated shared storage, persist plan schedules and additional statuses, preserve linked content on confirmed plan deletion, and enforce deployment tenant authority across V1 routes.
559
+
560
+ **Breaking for local-SQLite users** (hasna/apps#2031). The MCP plan tools
561
+ `create_plan`, `list_plans`, `get_plan`, `update_plan` and `delete_plan`
562
+ require the authenticated shared API and no longer read the local store: an MCP
563
+ client with no credential gets an error instead of local rows.
564
+ - e2217600b: Route project MCP CRUD and the CLI project panel through saved shared API credentials. Preserve project metadata, count complete task pages, and atomically detach linked content when explicitly deleting a nonempty project. Reject stale project references and misleading API receipts.
565
+ - bc5a45ce1: Route MCP task locks, priority updates and dependency operations through the saved account API. Preserve version conflicts and server force-unlock authorization, reject incomplete mutation receipts, and render complete bounded shared dependency graphs without opening SQLite.
566
+ - 07b43db1b: Route task-list MCP operations through authenticated shared storage, persist task-list status, and preserve linked tasks and plans during explicit forced deletion.
567
+
568
+ **Breaking for local-SQLite users** (hasna/apps#2025). The MCP task-list tools
569
+ `create_task_list`, `list_task_lists`, `get_task_list`, `update_task_list` and
570
+ `delete_task_list` require the authenticated shared API and no longer read the
571
+ local store.
572
+ - bc5a45ce1: Enable account-backed mine, blocked, overdue, today and yesterday CLI commands without SQLite fallback. Exhaust task pagination and fail visibly on incomplete result sets or unreadable dependency records.
573
+
574
+ On a hosted run these five commands read the shared authority and exhaust
575
+ pagination instead of falling back to an on-box database. An explicit local
576
+ selection (`HASNA_TODOS_LOCAL=1`) still serves them from the local store, as
577
+ before.
578
+ - 5b2a8d857: Use shared template CLI operations with version history, atomic bundled initialization, and truthful partial application receipts. Keep bundled library files credential-free.
579
+
580
+ **Breaking for local-SQLite users** (hasna/apps#2040). `todos templates`
581
+ (including `--use`), `template-init`, `template-preview`, `template-export`,
582
+ `template-import` and `template-history` are served by the authenticated shared
583
+ API and refuse `HASNA_TODOS_DB_PATH`, `TODOS_DB_PATH`, `HASNA_TODOS_LOCAL` and
584
+ `TODOS_LOCAL` before startup. `todos template-library` is the deliberate
585
+ exception: it renders the library bundled in the package, opens no store, and
586
+ stays credential-free.
587
+
588
+ ## 0.15.52
589
+
590
+ ### Patch Changes
591
+
592
+ - Accept a path-prefixed remote API base URL in the CLI cloud router (fixes #1512): the fleet is fronted by the `api.hasna.com` gateway, which addresses every app as `https://api.hasna.com/<app>/v1`, and every other fleet CLI accepts `HASNA_<APP>_API_URL=https://api.hasna.com/<app>` and appends `/v1` itself — todos was the only CLI that refused both forms with `REMOTE_API_URL_INVALID`. `normalizeRemoteAuthorityUrl` now accepts the authority root, the exact `/v1` root, the bare gateway form `<app>` (appending `/v1`), and `<app>/v1` (used verbatim, with the path prefix preserved through the status object and the contracts client so requests hit `https://api.hasna.com/todos/v1/...`, never a dropped or doubled `/v1`). The generic `/api/v1` shape, deeper paths, double-`/v1` paths, userinfo, query/fragment, and plaintext HTTP on non-loopback hosts all stay rejected. Regression tests cover the gateway URL forms plus the request-level route through the prefix.
593
+
594
+ ## 0.15.51
595
+
596
+ ### Patch Changes
597
+
598
+ - Switch @hasna/todos local path reads/writes through the @hasna/paths resolver (XDG/macOS home layout). The legacy `~/.hasna/todos` default stays the effective data home until the store has actually been migrated to the XDG data home (`todos.db` or `config.json` exists there) or the operator sets the data-kind override `HASNA_DATA_HOME`. The global database, config, training, replays, sandbox profiles, cloud-task-id cache, and the install-time postinstall provisioning all resolve through the same effective root. The dependency is pinned exactly to `@hasna/paths@0.1.0`.
599
+
600
+ ## 0.15.50
601
+
602
+ ### Patch Changes
603
+
604
+ - Updated dependencies [85a5e06]
605
+ - @hasna/contracts@0.14.1
606
+
607
+ ## 0.15.49
608
+
609
+ ### Patch Changes
610
+
611
+ - 11ba577f: Bounded dedup projection (O15-00170): dedup workflows now capture only the fields the dedupe fingerprint consumes instead of credential-bearing whole-task composites (incidents 713001/713022/713043-46/713119), surfaced through the new `todos dedupe project --json` command; the shared redactor also gains the xai-token pattern on every surface.
612
+ - af5e91ef: Sync push's destination preflight now throws a typed `ResourceConflictError` (`SNAPSHOT_DESTINATION_CONFLICT`) so the mirror/outbox parks persistent destination collisions instead of retrying them as transient (duplicate-key retry storm).
613
+ - Updated dependencies [6176948]
614
+ - Updated dependencies [7575de8]
615
+ - @hasna/contracts@0.14.0
616
+
617
+ ## 0.15.48
618
+
619
+ ### Patch Changes
620
+
621
+ - 9fd8163: Sync push classifies scoped-slug unique violations as typed conflicts (ba6e4a19). pushSnapshot's ON CONFLICT arbiter is the table PRIMARY KEY, but the deployed uniqueness invariants are the partial expression indexes todos_sync_records_task_list_scope_slug_uidx and todos_sync_records_project_task_list_slug_uidx, so a slug collision on a different object_id bypassed the upsert and raised a raw 23505 that the mirror (5x) and durable outbox (8x) retried as transient — the duplicate-key retry storm under load. pushSnapshot now maps 23505 to ResourceConflictError codes TASK_LIST_SLUG_CONFLICT / PROJECT_SLUG_CONFLICT (with a metadata-less fallback re-read, mirroring the adapter's renameProjectAtomic), runs the destination-conflict read plus inserts in the client's transaction when available, and the mirror/outbox retry machinery parks typed conflicts immediately instead of re-enqueueing them.
622
+
623
+ - b8ba49f: `GET /v1/tasks` and legacy `GET /api/tasks` validate the pagination bounds (O15-00354). Malformed values — `limit` 0, negative, or non-numeric, and a negative/non-numeric `offset` — now return HTTP 400 instead of silently answering 200 with the entire table (`0` and `NaN` are falsy, so the store's `if (filter.limit)` dropped the LIMIT clause and SQLite `LIMIT -1` means "no limit"). Regression tests cover each malformed bound plus a valid-pagination control.
624
+
625
+ - f8338ff: ensureCloudSchema retries are throttled with a min-interval cooldown (O15-00479 follow-up to PR #931). Sustained schema failure re-ran the idempotent DDL sequence on every /v1 request, saturating the connection pool under lock contention. Calls inside the min-interval window (default 10s, `HASNA_TODOS_SCHEMA_RETRY_MIN_MS`) rethrow the recorded failure without re-running the DDL; the memo stays cleared so the first call after the interval retries.
626
+
627
+ - cd86caf: Sync push's destination preflight now throws a typed `ResourceConflictError` (`SNAPSHOT_DESTINATION_CONFLICT`) instead of a generic `Error`. The preflight (an existing task-list slug or project task_list_id on a different object_id) previously entered the mirror/outbox retry queues as a generic error, so a persistent destination collision kept retrying — feeding the same duplicate-key retry storm the 23505 classifier removes. The typed error is parked immediately by the retry machinery, matching the 23505 path.
628
+
629
+ ## 0.15.47
630
+
631
+ ### Patch Changes
632
+
633
+ - 507e701: Retry transient Postgres lock failures on the guarded task-write path instead of surfacing HTTP 500 (fixes incident 724667 / HP-00083): on 2026-08-22 the hosted authority returned HTTP 500 for PATCH /tasks/{id} in bursts (measured 83 failures at 17:50-17:59Z and 83+ at 19:30-19:32Z), each surfacing `PostgresError: canceling statement due to lock timeout` (SQLSTATE 55P03) — the `todos_app` role's 5s `lock_timeout` canceled the task-parent-integrity advisory-lock statement or the guarded write while another transaction held the lock. The condition is transient (recovery within seconds), and the /v1 route mapped the unhandled error to HTTP 500, indistinguishable from a crash. The postgres storage adapter now retries the whole guarded write transaction (advisory lock + version-CAS statement) on transient Postgres errors (55P03 lock timeout, 40P01 deadlock, 40001 serialization) with a bounded backoff, and the idempotent schema DDL retries in-request; the version CAS is re-evaluated per attempt so optimistic concurrency is preserved. Regression tests: postgres-lock-retry.test.ts (retry on 55P03, bounded attempts, no retry on non-transient errors, CAS preserved across retries), plus updated schema-sync retry tests.
634
+
635
+ ## 0.15.46
636
+
637
+ ### Patch Changes
638
+
639
+ - 4794bda: Release-review P1 remediation (0.15.44 review, findings 1-2): the Postgres sync adapter's changed-since path (`getChangedSince` / `getTasksChangedSince`) and the cloud changed-since/report path (`cloudChangedSince`) compared `updated_at` against the cursor as raw text. Stored stamps mix ISO ("2026-08-05T18:54:55.814Z") with space-form ("2026-06-10 11:24:47", the DDL default `datetime('now')` plus snapshot import/sync), and as text space (0x20) sorts before 'T' (0x54), so space-form rows that are genuinely NEWER than an ISO cursor were silently omitted from changed-since feeds, CLI summaries, and activity reports. Both paths now compare the stamps as instants via a shared `changedSinceStampNewer` helper (apps/todos/src/lib/instant-compare.ts) mirroring the SQL `julianday()` predicate used by the SQLite/Postgres updated_after paths, including its keep-unparseable semantics: a stamp that cannot be parsed is KEPT, because "cannot read the row's timestamp" is not "older than the cursor"; naive (no-offset) stamps are read as UTC, matching SQLite julianday. Regression tests: postgres-adapter changed-since space-form case (storage.test.ts), cloud changed-since space-form/unparseable case (cloud-router.test.ts), helper unit tests (instant-compare.test.ts).
640
+
641
+ ## 0.15.45
642
+
643
+ ### Patch Changes
644
+
645
+ - b8f1f5d: task-subtree-transfer postgres backend: `ensureSchema()` no longer caches a rejected one-time schema sync. The `schemaReady ??=` memo (apps/todos/src/task-subtree-transfer/postgres.ts) previously kept the rejected promise forever, so a single transient Postgres lock timeout (SQLSTATE 55P03) during the boot-time schema statements was replayed on every later transfer operation instantly — with no DB round trip — permanently bricking the subtree-transfer authority (wired into the same process as the storage adapter via the cached singleton in server/cloud.ts). A failed sync is now cleared and retried on the next operation with fresh state, matching the pattern landed for the storage adapter (PR #931) and pr-groups / project-registration (PR #933). Regression test: a failed schema sync is retried on the next operation instead of being cached forever (task-subtree-transfer-schema-retry.test.ts).
646
+
647
+ ## 0.15.44
648
+
649
+ ### Patch Changes
650
+
651
+ - 73f839e: ensureSchema() no longer caches a rejected one-time schema sync (fixes incident 724661): the boot-time schema statements previously ran into a cached `schemaReady` promise that was never cleared on failure, so a single transient Postgres lock timeout (e.g. `canceling statement due to lock timeout`, SQLSTATE 55P03, under the `todos_app` role's 5s `lock_timeout`) was replayed on every later operation instantly — with no DB round trip — permanently bricking the store until the task was replaced. A failed sync is now cleared and retried on the next operation with fresh state. Applies to the postgres storage adapter (task write path) and the pr-groups, task-manifest, and project-registration postgres backends. Regression tests: a failed schema sync is retried on the next operation (postgres-adapter, pr-groups ledger).
652
+
653
+ ## 0.15.43
654
+
655
+ ### Patch Changes
656
+
657
+ - 1c859c2: getTasksChangedSince compares the since-cursor via julianday() instead of raw TEXT (fixes row 23ce88cb): stored stamps mix ISO "2026-08-05T18:54:55.814Z" with space-format "2026-06-10 11:24:47" (the DDL default datetime('now'), plus snapshot import/sync), and as text "T" sorts after " ", so the old `updated_at > ?` comparison silently excluded space-format rows that are genuinely newer than the cursor from changed-since feeds (MCP get_tasks_changed_since, sync adapter, HTTP changed-since and report routes). Mirrors the sibling updated_after predicate in task-crud.ts, including its keep-unparseable semantics: a stamp julianday() cannot parse yields NULL and the row is KEPT, because "cannot read the row's timestamp" is not "older than the cursor".
658
+
659
+ ## 0.15.42
660
+
661
+ ### Patch Changes
662
+
663
+ - Updated dependencies [554a5b9]
664
+ - @hasna/contracts@0.13.4
665
+
666
+ ## 0.15.41
667
+
668
+ ### Patch Changes
669
+
670
+ - e6134c1: Fix the 0.15.38 /v1 503 (row ae34a051, incident 720366): wire `keyStatus: store.keyStatus` instead of the deprecated `isRevoked`-only hook, which @hasna/contracts >= 0.8.7 refuses at verifier construction — the #761 lockfile regeneration moved todos from the stale-locked contracts 0.5.2 to 0.13.1, so every /v1 business route returned HTTP 503 with a valid key while /health and /version stayed 200. Regression tests: `src/server/cloud-auth-wiring.test.ts` (default lane) and `src/server/cloud-auth-wiring.pg.test.ts` (TODOS_TEST_PG_URL lane).
671
+ - 2e3ab38: Fail closed on unsupported or empty project filters (I38-00523): `todos projects --project <ref>`, `todos show --project <ref> <id>`, and `todos list --project-name ""` / `--project ""` previously returned success at rc=0 with unfiltered or full-population output and empty stderr; each now fails closed with a usage error. 7 regression tests (4 negative + 3 positive controls) against a stub /v1 authority.
672
+ - 65647c2: Make `deliverTodosApiKeyViaDisk` refuse the real machine home (todos 1c86abeb): when `HOME` resolves to this machine's real home directory, delivering a fixture credential now throws `TODOS_FIXTURE_HOME_IS_MACHINE_HOME` instead of replacing the machine's configured todos credential with the fixture value; test fixtures must pass a throwaway home.
673
+ - Release path: fix the root build entry layout and the pack lifecycle. The root multi-entry `bun build` command gains `--root src`, restoring `dist/index.js`, `dist/mcp.js`, `dist/registry.js`, `dist/contracts.js`, `dist/storage.js`, `dist/testing.js`, `dist/project-registration.js`, `dist/task-manifest.js`, and `dist/task-subtree-transfer.js` at the `dist` root (with 9 entries bun auto-derived the project root instead of the entries' common `src/` directory and emitted them under `dist/src/`, leaving `package.json` `main`/`exports` pointing at files the tarball did not contain). The `prepack: bun run build` hook is removed: the package-owned release gate (`verify-public-release.ts`) builds from the clean source commit and packs with `--ignore-scripts`, and a prepack rebuild would make the `npm publish` tarball differ from the verified pack (the gate forbids lifecycle pack scripts).
674
+
675
+ ## 0.15.40
676
+
677
+ ### Patch Changes
678
+
679
+ - @hasna/contracts@0.13.3
680
+
681
+ ## 0.15.39
682
+
683
+ ### Patch Changes
684
+
685
+ - Updated dependencies [5e32853]
686
+ - @hasna/contracts@0.13.2
687
+
688
+ ## 0.15.38
689
+
690
+ ### Patch Changes
691
+
692
+ - @hasna/contracts@0.13.1
693
+
694
+ ## 0.15.37
695
+
696
+ ### Patch Changes
697
+
698
+ - 0d4f749: Add `prepack: bun run build` so `npm pack` and `npm publish` ship the built `dist` that each package's `main` points to. Previously only `prepublishOnly` built, so a clean-clone `npm pack` shipped a tarball with no code. Also add a repo-root `.editorconfig` with the member-standard style (2-space indent, LF, final newline).
699
+ - 0d7a2d6: fix(todos): name the local-SQLite fallback instead of serving it silently. When neither HASNA_TODOS_API_URL nor HASNA_TODOS_API_KEY is set, resolveTodosCliTransport now emits one machine-readable JSON notice on stderr per process (`todos-local-fallback`) naming the mode switch before serving local — the same family as the merged secrets fix (PR #681 / incident 715558). Incident 715712: a re-provision dropped the hosted pair and tasks appeared gone at rc=0. Partial pairs keep failing closed; the notice never fires when the hosted pair selects http.
700
+ - Updated dependencies [d5b64f8]
701
+ - Updated dependencies [1da0550]
702
+ - @hasna/contracts@0.13.0
703
+
704
+ ## 0.15.36
705
+
706
+ ### Patch Changes
707
+
708
+ - 058cb86: Fix `todos comment <plan-id>` returning 404 "task not found": plans now have a comment surface end to end — `plan_comments` table (local sqlite) and `plan_comments` records (postgres), `/v1/plans/:id/comments` GET/POST, CLI `comment` task-first/plan-fallback (local + hosted), and `plans --show` listing plan comments. Plan-level outcomes can now be recorded on the plan row (todos task 04ee08fd).
709
+ - 3fa7d73: Remove the deprecated storage-mode env selection (owner deprecation, 2026-07-29 deployment-modes removal). The client now routes on `HASNA_TODOS_API_URL` + `HASNA_TODOS_API_KEY` alone; any storage-mode var now hard-errors instead of silently selecting a backend. Breaking/behavioral change: configurations still setting the deprecated var must drop it before upgrading to 0.15.34.
710
+ - 4c8ab45: Bound every authenticated /v1 request to a single 10s budget in the CLI's cloud router (task 9b050845). `todos count` on a stalled /tasks endpoint previously hung past 120s and then reported REMOTE_API_UNREACHABLE while the authority was reachable; the contracts transport's 30s timer was consumed by two retries of a timeout-shaped failure, an abort-ignoring fetch hung unbounded, and `response.text()` after headers was unbounded. A stalled request now fails within the bound with REMOTE_API_TIMEOUT (slow authority), non-retryable, instead of the multi-minute hang followed by REMOTE_API_UNREACHABLE (down authority).
711
+ - Updated dependencies [b630c48]
712
+ - @hasna/contracts@0.11.2
713
+ - @hasna/events@0.1.16
714
+
715
+ All notable changes to this project will be documented in this file.
716
+
717
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
718
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
719
+
720
+ ## [Unreleased]
721
+
722
+ ## [0.15.35] - 2026-08-17
723
+
724
+ ### Fixed
725
+
726
+ - **Bounded remote request timeout.** `todos count` and every other
727
+ authenticated `/v1` command against a stalled authority now fail within a
728
+ single 10s request bound and report `REMOTE_API_TIMEOUT` — "the authority is
729
+ slow" — instead of hanging for minutes and then reporting
730
+ `REMOTE_API_UNREACHABLE` — "the authority is down". Timeout-shaped failures
731
+ are no longer retried into a multi-attempt hang. (task 9b050845)
732
+
733
+ - **`list --limit N` no longer caps silently.** A bounded read was
734
+ indistinguishable from the full population: `--limit 2000` returned exactly
735
+ 2000 rows of a 7,839-row pending set at rc=0 with no truncation signal. The
736
+ forwarded path now requests limit+1 as a truncation probe and slices to the
737
+ caller's limit; the withheld path (--sort, --overdue/--due-today, creator
738
+ and task-list filters, scalar-status union) compares the pre-window set. A
739
+ truncation is reported on stderr — the same channel as the scan-ceiling
740
+ warning — while `--json` stdout stays a clean parseable array. Applies to
741
+ the HTTP authority and the local SQLite store. (todos 52b0a207)
742
+
743
+ ## [0.15.34] - 2026-08-15
744
+
745
+ ### Changed
746
+
747
+ - **The deprecated storage-mode env selection is removed.** The client now
748
+ routes on `HASNA_TODOS_API_URL` + `HASNA_TODOS_API_KEY` alone; any
749
+ storage-mode var now hard-errors instead of silently selecting a backend.
750
+ Configurations still setting the deprecated var must drop it before
751
+ upgrading.
752
+
753
+ ## [0.15.33] - 2026-08-14
754
+
755
+ ### Fixed
756
+
757
+ - **Historical project-registration receipts can be looked up by their
758
+ immutable stored corpus identity while current create and inverse authority
759
+ guards remain bound to the current capability.**
760
+
761
+ ## [0.15.32] - 2026-08-12
762
+
763
+ ### Fixed
764
+
765
+ - **Task-manifest apply and compensation now bind operation steps, deterministic
766
+ idempotency, terminal nonacceptance receipts, and exact legacy slug
767
+ compatibility across SQLite, PostgreSQL, HTTP, CLI, and OpenAPI surfaces.**
768
+
769
+ ## [0.15.31] - 2026-08-12
770
+
771
+ ### Fixed
772
+
773
+ - **The npm release full suite keeps single-pass failure semantics while
774
+ allowing finite GitHub-runner scheduling headroom.** The root release gate
775
+ now uses a 30-second per-test and hook ceiling instead of Bun's inherited
776
+ five-second default, while continuing to forbid retries so deterministic
777
+ assertion failures and genuine hangs still stop publication.
778
+
779
+ ## [0.15.30] - 2026-08-12
780
+
781
+ ### Added
782
+
783
+ - **The Todos project-resource producer exposes stable, complete project and
784
+ task-list bindings.** `project-registration project-resources` now enumerates
785
+ deterministic resources for a canonical source project across SQLite,
786
+ PostgreSQL, authenticated HTTP, OpenAPI, and the generated SDK, with bounded
787
+ paging, mutation restarts, duplicate and cursor guards, and exact UUID
788
+ readback.
789
+ - **Prior Projects registration adoption is explicitly verifiable.**
790
+ `validatePriorRegistrationAdoption` and the matching CLI, HTTP, OpenAPI, and
791
+ generated-SDK surfaces validate the stored authority, target identity, and
792
+ receipt digest before a caller adopts an existing project or task list.
793
+
794
+ ### Fixed
795
+
796
+ - **Historical pre-adoption receipts remain compatible without weakening
797
+ integrity.** Bind-existing digests are anchored to the immutable resource
798
+ incarnation, PostgreSQL serializes adoption against concurrent renames, and
799
+ hosted validation rejects false, malformed, or forged response envelopes.
800
+ - **npm release review receipts stay out of GitHub Actions step preambles.**
801
+ Both receipt-consuming steps now source the capability-bearing receipt from
802
+ the `npm-release` environment secret surface while preserving the existing
803
+ exact package, version, tag, commit, workflow, reviewer, and signature gates.
804
+ - **The next optional AI companion candidate is `@hasna/todos-ai@0.1.2`.** The
805
+ unpublished candidate carries the masked receipt-delivery regression needed
806
+ for the real negative/positive release-workflow verification.
807
+
808
+ ## [0.15.29] - 2026-08-11
809
+
810
+ ### Fixed
811
+
812
+ - **Historical project-registration receipts remain addressable after package upgrades.**
813
+ Exact source identity now includes the stored authority route and package
814
+ version across SQLite, PostgreSQL, authenticated HTTP, and the
815
+ `project-registration receipt-lookup` CLI without weakening current-version
816
+ checks on create or inverse operations.
817
+
818
+ ## [0.15.28] - 2026-08-11
819
+
820
+ ### Fixed
821
+
822
+ - **Todos AI preserves host-created clarification and approval control signals
823
+ across the bundled companion boundary.** This includes zero-trap containment
824
+ for outer and nested `Proxy` payloads.
825
+
826
+ ## [0.15.27] - 2026-08-11
827
+
828
+ ### Fixed
829
+
830
+ - **Hosted task-manifest lookup derives the authenticated authority tenant.**
831
+ The package-owned OpenAPI now advertises the tenant-bearing capability route,
832
+ the generated SDK exposes that contract, and the built CLI verifies both
833
+ omitted capability-derived and explicit tenant lookup without local fallback.
834
+ - **Hosted task creation advertises explicit creator compatibility.** The same
835
+ generated server contract continues to expose `created_by` on both
836
+ `CreateTaskInput` and `Task`, so compatible clients can prove the field before
837
+ mutation and verify it on authoritative readback.
838
+
839
+ ## [0.15.26] - 2026-08-10
840
+
841
+ ### Fixed
842
+
843
+ - **Task-manifest CLI execution is supported across local, SQLite, and
844
+ PostgreSQL authorities.** The CLI now preserves deterministic plan slugs
845
+ across backends and the test fixtures share one bounded projection for the
846
+ unrelated external Bun ancestor-package warning.
847
+
848
+ ## [0.15.25] - 2026-08-10
849
+
850
+ ### Fixed
851
+
852
+ - **Default Todos AI sessions retain their safe task read tool.** An
853
+ unconfigured workspace now keeps the minimal profile's bounded `get_task`
854
+ tool while list and mutation tools remain subject to explicit workspace
855
+ trust.
856
+
857
+ ## [0.15.24] - 2026-08-10
858
+
859
+ ### Fixed
860
+
861
+ - **Release CI installs the independently locked AI companion before recursive
862
+ tests.** The npm workflow now installs `ai/bun.lock` dependencies before the
863
+ root `bun test`, retaining the Todos AI content from the unpublished
864
+ `0.15.23` release while advancing the package to `0.15.24`.
865
+
866
+ ## [0.15.23] - 2026-08-10
867
+
868
+ ### Added
869
+
870
+ - **Todos gains an optional provider-neutral AI companion.** `@hasna/todos-ai`
871
+ adds the `todos ai` CLI contract, a Groq adapter, typed and streaming output,
872
+ bounded read/write tools with guarded mutations, and deterministic evaluation
873
+ and redaction without placing provider dependencies in the root package.
874
+
875
+ ### Changed
876
+
877
+ - **CI validates the independently locked AI companion.** Pull requests install
878
+ `ai/bun.lock` with Bun 1.3.14 and run its typecheck, full test suite, and build
879
+ in addition to the existing root and dashboard gates.
880
+
881
+ ## [0.15.22] - 2026-08-10
882
+
883
+ ### Fixed
884
+
885
+ - **Remote stale-lock recovery follows the deployed authority contract.** The
886
+ CLI advertises and invokes `stale-lock-handoff` only when the authority's
887
+ OpenAPI document exposes the exact route, sends no mutation to older
888
+ authorities, and permits a new authenticated session with the same agent name
889
+ to refresh an exact stale lock through holder/version CAS plus an audit
890
+ receipt ([#258](https://github.com/hasna/todos/pull/258)).
891
+ - **PostgreSQL stale-lock recovery no longer depends on a schema helper.** The
892
+ exact stored `locked_at` token is compared as text before PostgreSQL casts only
893
+ the already-validated, PostgreSQL-compatible request token for the stale
894
+ cutoff, so missing helper functions, ISO year zero, and malformed stored
895
+ timestamps fail closed without mutation
896
+ ([#261](https://github.com/hasna/todos/pull/261)).
897
+
898
+ ## [0.15.20] - 2026-08-09
899
+
900
+ ### Added
901
+
902
+ - **npm publishing requires an Ed25519-signed independent-agent `GO`.** The
903
+ package-owned workflow and prepublish gates reject missing, unsigned,
904
+ `NO_GO`, mismatched, tampered, wrong-key, self-review, and replayed receipts
905
+ unless they bind the exact repository, commit, package version, tag,
906
+ workflow revision, registry, reviewer, and publisher.
907
+
908
+ ### Fixed
909
+
910
+ - **Exact stale task locks can be handed off atomically.** `todos
911
+ stale-lock-handoff <id>` fences on the expected holder and lock version,
912
+ rejects live or mismatched locks, transfers only the named task across local
913
+ and authenticated `/v1` authorities, and returns an immutable audit receipt.
914
+
915
+ ## [0.15.19] - 2026-08-09
916
+
917
+ ### Fixed
918
+
919
+ - **Hosted plan completion stays on the authenticated authority path.** When
920
+ the direct completed-status route is unavailable, the CLI uses the supported
921
+ revision-guarded completion operation and verifies authoritative readback
922
+ without broad snapshot replacement ([#243](https://github.com/hasna/todos/pull/243)).
923
+ - **Remote task retry requires an advertised capability.** The CLI checks the
924
+ authority's boolean retry capability before mutation and fails closed for
925
+ missing or incompatible remote schemas while preserving the local retry path.
926
+ - **Comments accept explicit file input.** Comment creation can read the body
927
+ from a requested file while preserving inline input and authenticated cloud
928
+ creator attribution ([#242](https://github.com/hasna/todos/pull/242)).
929
+
930
+ ## [0.15.18] - 2026-08-09
931
+
932
+ ### Fixed
933
+
934
+ - **Hosted exact-list reads reuse the resolved list's project scope.** UUID and
935
+ slug reads now bound legacy compatibility scans by the owning project, apply
936
+ output limits after exact local filtering, and reject conflicting explicit
937
+ project/list scopes before reading tasks.
938
+
939
+ ## [0.15.17] - 2026-08-09
940
+
941
+ ### Fixed
942
+
943
+ - **Task failure now routes through the selected authority.** `todos fail
944
+ --reason [--retry]` uses the supported local or authenticated `/v1` lifecycle
945
+ path, preserves failure reasons in readback, exposes the operation through
946
+ OpenAPI and the generated SDK, and rejects unsupported lifecycle status
947
+ values at the server boundary ([#237](https://github.com/hasna/todos/pull/237)).
948
+
949
+ ## [0.15.16] - 2026-08-09
950
+
951
+ ### Fixed
952
+
953
+ - **Takumi MCP registration uses Takumi's first-class CLI.** Registering or
954
+ removing Todos now calls scoped `takumi mcp add/remove` commands instead of
955
+ the previously rejected generic path ([#233](https://github.com/hasna/todos/pull/233)).
956
+ - **Remote task creation is single-attempt and authority-verified.** Every
957
+ create is read back by exact ID from the configured authority before success
958
+ is printed, preventing duplicate or reported-but-unreadable task success
959
+ ([#235](https://github.com/hasna/todos/pull/235)).
960
+ - **P1: partial task-manifest outbox delivery is retry-safe.** When a client
961
+ acknowledged row 1, crashed or lost the response, then resumed the two-row
962
+ sequence, the repeated row-1 acknowledgement was rejected as a graph
963
+ conflict and row 2 could never be acknowledged. SQLite and PostgreSQL now
964
+ treat an exact tenant-scoped row already in `delivered` state as success
965
+ without changing its first delivery timestamp or attempt count; missing,
966
+ wrong-tenant, cancelled, or otherwise incompatible rows still fail closed.
967
+ The unchanged `todos.task-manifest.v1` capability now advertises
968
+ `idempotent_outbox_delivery: true`, with a runtime guard that distinguishes
969
+ older authorities where the field is absent or false.
970
+ - **Hosted exact task-list reads are complete or explicit.** Legacy unfiltered
971
+ pages use bounded, stable `total`/`offset` pagination with matching-only
972
+ output; incomplete or unsupported authority shapes fail closed instead of
973
+ silently omitting matches ([#238](https://github.com/hasna/todos/pull/238)).
974
+ - **PostgreSQL outbox delivery and compensation no longer race.**
975
+ Operation-scoped locking serializes delivery with compensation, and graph
976
+ rollback proceeds only after every expected outbox row is atomically
977
+ cancelled ([#240](https://github.com/hasna/todos/pull/240)).
978
+
979
+ ## [0.15.12] - 2026-08-08
980
+
981
+ ### Fixed
982
+
983
+ - **Remote `todos list` no longer reports an empty queue when `--status` is
984
+ omitted.** The default pending-plus-in-progress filter was serialized as one
985
+ comma-separated query; a contract-incompatible authority answered it with HTTP
986
+ 200, valid empty JSON, and no stderr even though both scalar status reads were
987
+ populated. Multi-status cloud reads now issue bounded scalar queries, deduplicate
988
+ their union by task ID, restore the global task order, and apply offset/limit last.
989
+
990
+ ## [0.15.11] - 2026-08-08
991
+
992
+ ### Fixed
993
+
994
+ - **Hosted guarded plan/project linkage now fails closed on invalid success
995
+ payloads.** The cloud client validates complete nested plan, project, task,
996
+ count, revision, receipt, digest, and rollback invariants before accepting an
997
+ HTTP 2xx response, and normalizes idempotency keys before both the remote write
998
+ and receipt validation so a successful mutation cannot be reported as a
999
+ client-side failure ([#220](https://github.com/hasna/todos/pull/220)).
1000
+
1001
+ ## [0.15.10] - 2026-08-07
1002
+
1003
+ ### Added
1004
+
1005
+ - **Existing plans can now be linked atomically to one authoritative project.**
1006
+ Linkage validates exact plan, project, and member-task revisions; records
1007
+ immutable apply and rollback receipts with every prior task link; and enforces
1008
+ the chosen project for future plan members across SQLite, PostgreSQL, the v1
1009
+ API, SDK, and CLI ([#215](https://github.com/hasna/todos/pull/215)).
1010
+
1011
+ ## [0.15.9] - 2026-08-07
1012
+
1013
+ ### Fixed
1014
+
1015
+ - **SQLite project registration no longer rolls back unrelated successful task
1016
+ writes.** The 0.15.7 authority path awaited project/list discovery and digest
1017
+ work while a transaction remained open on the shared SQLite connection, so a
1018
+ later authority failure could roll back an ordinary supported task write that
1019
+ had already returned success. Registration now stages all asynchronous work
1020
+ outside the transaction, revalidates the exact read set, and applies the staged
1021
+ mutations atomically under `BEGIN IMMEDIATE`; forward and inverse fault
1022
+ regressions prove unrelated writes survive while concurrent authority calls
1023
+ serialize or retry safely ([#208](https://github.com/hasna/todos/pull/208)).
1024
+ - **Ambiguous `todos list --assigned <name>` reads now disclose that the returned
1025
+ queue is partial.** Literal-only fallback remains non-fatal and preserves JSON
1026
+ stdout, but stderr now names the ambiguity instead of making a populated partial
1027
+ result indistinguishable from a complete queue
1028
+ ([#206](https://github.com/hasna/todos/pull/206)).
1029
+
1030
+ Containment: `0.15.7` remains deprecated and was removed from `latest`; the
1031
+ registry intentionally stays on known-good `0.15.6` until this fix is released.
1032
+ Version `0.15.8` was already reserved by a separate release lane and is skipped,
1033
+ so `0.15.9` is the first releasable package containing the SQLite repair.
1034
+
1035
+ ## [0.15.7] - 2026-08-07
1036
+
1037
+ ### Added
1038
+
1039
+ - **Protected conditional project and task-list registration.** The package-owned
1040
+ `@hasna/todos/project-registration` SDK and authenticated `/v1/project-registration`
1041
+ routes create or bind an exact project/list pair under immutable request digests,
1042
+ deterministic retries, exact receipt readback, and bounded ambiguity reconciliation
1043
+ across SQLite and hosted PostgreSQL.
1044
+
1045
+ ### Fixed
1046
+
1047
+ - **Registration compensation now refuses unsafe rollback.** Receipt-scoped
1048
+ compensation verifies ownership, parent state, and dependent records before removing
1049
+ an accepted registration, and refuses without mutation when foreign or user-created
1050
+ data would otherwise be detached or deleted.
1051
+
1052
+ ## [0.15.6] - 2026-08-06
1053
+
1054
+ ### Fixed
1055
+
1056
+ - **`show` and `inspect` advertised a comment cursor that no verb could spend, so every
1057
+ comment older than the newest page was unreachable from the CLI.** Both commands emit
1058
+ `comments_page` with `has_more: true`, a `next_cursor`, and `pagination_supported: true`,
1059
+ and both called `cloudListComments(cloud, id)` with no options — while that reader has
1060
+ accepted `{ limit, cursor }` all along and is unit-tested for it. `show --help` listed
1061
+ no options beyond `-h`, `--cursor`/`--comments-cursor` were rejected as unknown, and no
1062
+ `comments` verb exists (`comment` is write-only), so the advertised cursor had no
1063
+ consumer anywhere in the CLI. Measured on a live 125-comment task: the newest 100 were
1064
+ returned and the remaining 25 could not be read by any command. `show` and `inspect` now
1065
+ take `--comments-limit <n>` (1-500) and `--comments-cursor <cursor>`, so the cursor a
1066
+ page hands you is spendable by the command that produced it; walking that live task now
1067
+ yields 100 + 25 = 125 distinct comments with zero overlap and terminates at
1068
+ `has_more: false`. The local (SQLite) read path accepts the same flags with the same
1069
+ semantics through a shared pager; without a flag its output is unchanged — the complete
1070
+ history and no `comments_page` — so existing local consumers are unaffected.
1071
+
1072
+ Note on the ordering, because the reported symptom pointed the other way: a page is the
1073
+ **newest** `limit` comments in **ascending** display order, so the newest comment is the
1074
+ **last** array element and was always reachable. `next_cursor` walks toward **older**
1075
+ history, which is the direction that was blocked. A `--comments-limit 1` read returns the
1076
+ single newest comment, which is the probe that distinguishes the two readings.
1077
+
1078
+ - **The comment cursor codec was private to the `/v1` server**, so the CLI could not decode
1079
+ a cursor the server had minted. `encodeCommentCursor`/`decodeCommentCursor` moved to
1080
+ `src/lib/comment-cursor.ts` alongside the pure pager both read paths now share; the
1081
+ server imports them and its behaviour is unchanged. A second copy of keyset logic is how
1082
+ the two ends drift into disagreeing about what a cursor means.
1083
+
1084
+ ## [0.15.5] - 2026-08-05
1085
+
1086
+ ### Added
1087
+
1088
+ - **`todos delegate` — one atomic verb for handing a task to a worker.** A delegation
1089
+ previously took four to six separate commands (`update --assign`, a comment carrying
1090
+ the brief, an `agents register` for the worker, a channel post), each of which could
1091
+ half-happen. Nothing tied them together, so a row could end up assigned with no brief,
1092
+ briefed with no assignee, or dispatched with no record anyone could grep. `delegate`
1093
+ performs the ordered effects in one call and refuses before the first write when the
1094
+ handover would be incomplete.
1095
+ - **A brief is mandatory and is gated on both sides.** `--brief <path>`,
1096
+ `--brief-text <text>`, or `--brief -` for stdin. A missing, unreadable, zero-byte or
1097
+ whitespace-only brief is refused with the offending path named and the row left
1098
+ byte-identical; the accepted content is stored untrimmed with its sha256, so a brief
1099
+ cannot be silently substituted later.
1100
+ - **Handover lineage is persisted on the row** — `assigned_by`, `delegated_from` and
1101
+ `delegation_depth`, written in a single patch. The write branches test
1102
+ `!== undefined` rather than truthiness, so `delegation_depth: 0` and an explicit
1103
+ `delegated_from: null` are honoured instead of being dropped. Depth increments from
1104
+ the parent row, so a re-delegated task records its chain.
1105
+ - **The row is left claimable.** `started_at` and `locked_by` stay `NULL` and status
1106
+ stays `pending`: delegation assigns and briefs, it does not claim. The worker's own
1107
+ `todos start` is still what takes the lock, which keeps the dispatched-but-unclaimed
1108
+ population countable.
1109
+ - **A greppable `[DISPATCH]` comment** carrying worker, dispatcher, brief source and
1110
+ digest, lineage, and the claim deadline — the marker is the first thing on the first
1111
+ line so counters can anchor on it. The deadline also lands in task metadata as a
1112
+ queryable field, merged read-modify-write so a concurrent writer's keys survive.
1113
+ - **Read-back verification.** After the patch the row is re-read and every field the
1114
+ delegation claims to have written is checked. If the authority accepted the request
1115
+ but did not persist the lineage, the command refuses and names the missing fields
1116
+ _before_ the `[DISPATCH]` comment is written, so a partial delegation can never be
1117
+ reported as a complete one. This matters most where `assigned_by` still holds the
1118
+ filer — a plausible value, which is what would otherwise make the no-op invisible.
1119
+ - **Depth threshold and embargo are data, not constants.** The threshold ships unset:
1120
+ the seat-queue count and any recorded override are unconditional, while parking is
1121
+ opt-in via flag, environment or config. The embargo is an owner-editable file that
1122
+ self-disables when absent, and it is enforced against both the worker's name and its
1123
+ agent id, since resolving an id to a name would otherwise bypass a name-only check.
1124
+ - Registered in both the canonical command list and the remote command list.
1125
+ Canonical-only would have left the verb local-only, which the `/v1` route refuses —
1126
+ a state that is invisible from `--help`.
1127
+ - `dispatch` and `dispatches` are unchanged and still registered. No schema change, no
1128
+ migration, and no change to any server route.
1129
+
1130
+ ## [0.15.4] - 2026-08-04
1131
+
1132
+ ### Fixed
1133
+
1134
+ - **`todos comment` ignored the per-session identity environment variable, so every
1135
+ unflagged comment was silently unattributable.** `todos comment <id> <text>` (alias
1136
+ `log-progress`) read `agent_id: globalOpts.agent` directly at both the cloud and the
1137
+ local `addComment` call site — a bare read of the `--agent` flag that never called
1138
+ `resolveWritableIdentity`. `add`, `start`, and `done` all already resolved through that
1139
+ helper, which checks the explicit flag first and then the two supported per-session
1140
+ variables `TODOS_AGENT_ID` and `HASNA_TODOS_AGENT_ID` (never the station-shared
1141
+ `identity.json`, which is not process-bound). Because `comment` never called it, that
1142
+ documented escape hatch was invisible on this one command: a comment written with the
1143
+ variable exported and no flag landed with `agent_id` null on the local store and
1144
+ attributed to the shared `fleet` principal on the cloud path — rc=0, printing
1145
+ `Comment added.`, with no warning that the attribution had been dropped. Both call
1146
+ sites now resolve through `resolveWritableIdentity(globalOpts.agent)`, matching the
1147
+ pattern `add` already uses: an explicit `--agent` still wins and keeps its original
1148
+ casing, and the resolver's canonicalised value is used only when no flag was passed.
1149
+ Regression coverage in `creator-attribution.test.ts` mirrors the existing `add` cases —
1150
+ attributes from the environment variable (this case fails without the fix), `--agent`
1151
+ still wins over the variable, stays null with neither, and never attributes from the
1152
+ persisted identity file (todos task `39b4255b`, PR #196).
1153
+
1154
+ ## [0.15.3] - 2026-08-04
1155
+
1156
+ ### Fixed
1157
+
1158
+ - **`todos add --assign <agent>` silenced the attribution warning while `created_by`
1159
+ went null.** After 0.15.2 routed `created_by` through the guarded
1160
+ `resolveWritableIdentity`, the ownerless-warning gate in `todos add` still keyed
1161
+ only on `assignee` — a check that used to imply attribution but no longer does. An
1162
+ anonymous filer that passed `--assign <agent>` got a real owner and a silently null
1163
+ `created_by`, because giving the row an assignee suppressed the one warning that
1164
+ would have said so. The warning now fires independently on whichever condition is
1165
+ true — no assignee, or no writable identity — so an assigned-but-unattributed row
1166
+ now says so on stderr instead of filing in silence (todos task `a3f4bb1a`).
1167
+ - **`todos init`'s success message still promised automatic attribution that 0.15.2
1168
+ removed.** The line printed on every successful `init` — "Identity saved — later
1169
+ commands attribute to this agent automatically" — became false on every column
1170
+ (`created_by`, `agent_id`, `assigned_to`) once the persisted identity file was
1171
+ narrowed to a display-only diagnostic. The collision path already named the correct
1172
+ escape hatch (`export TODOS_AGENT_ID=<name>`); the success path — the one every
1173
+ fresh session hits — now prints the same instruction instead of the opposite one
1174
+ (todos task `a3f4bb1a`).
1175
+
1176
+ ## [0.15.2] - 2026-08-04
1177
+
1178
+ ### Fixed
1179
+
1180
+ - **`created_by` no longer inherits the station-shared identity `todos init` persists.**
1181
+ `todos add` and the MCP `create_task` tool resolved `created_by` through
1182
+ `resolveCreatorIdentity`, which falls back to `~/.hasna/todos/identity.json` — a file
1183
+ keyed on `$HOME` and shared by every agent session on a station, so it names the box
1184
+ rather than the caller. `agent_id`/`assigned_to` were already narrowed to the guarded
1185
+ `resolveWritableIdentity` in `0.14.x` (#142); `created_by` was deliberately left on the
1186
+ wider resolver on the premise that the change was inert on the hosted path because the
1187
+ deployed server dropped the column outright. That premise no longer holds — the server
1188
+ now persists and serves `created_by` — and the residual produced 489 real rows on one
1189
+ station misattributed to whichever agent last ran `todos init` there (todos task
1190
+ `9090972e`). `created_by` now resolves the same way as `agent_id`: unattributable
1191
+ (`null`) unless a process-bound identity (`--agent`, `--created-by`, `TODOS_AGENT_ID`,
1192
+ or `HASNA_TODOS_AGENT_ID`) is given.
1193
+
1194
+ ## [0.15.1] - 2026-08-03
1195
+
1196
+ ### Fixed
1197
+
1198
+ - **Cloud `link-ref` / `find-ref` round-trips now support refs containing `/` and
1199
+ `#`.** The v1 server decodes the opaque ref segment after route matching and returns
1200
+ a stable 400 for malformed percent encoding.
1201
+ - **The npm release workflow no longer dirties its checkout during the full suite.**
1202
+ The model-config test restores the runner's original `HOME` instead of deleting it,
1203
+ preventing later config writes from creating a literal `~/.hasna/todos/config.json`
1204
+ inside the repository. A pre-publish cleanliness assertion now reports the path and
1205
+ stops before npm lifecycle hooks if this regresses.
1206
+
1207
+ ## [0.15.0] - 2026-08-03
1208
+
1209
+ Numbered as a minor for the same reason `0.13.13` was renumbered to `0.14.0` the day
1210
+ before: this release rejects CLI input that `0.14.0` accepted, and under 0.x semver the
1211
+ minor is the field that signals that. A patch would have let a `^0.14` range absorb it
1212
+ silently, which is the outcome the previous renumber existed to prevent.
1213
+
1214
+ The scope is narrower than `0.14.0`'s. That release rejected out-of-vocabulary enums on
1215
+ `todos list` and on three HTTP endpoints, and it named those surfaces explicitly. It did
1216
+ not cover `todos watch`, and it did not treat an empty or blank enum element as invalid.
1217
+ Both gaps are closed here, so input that survived `0.14.0` can now fail.
1218
+
1219
+ ### Added
1220
+
1221
+ - **`todos bulk tag|untag <ids...> --tag <comma-separated>`.** Adds or removes tags
1222
+ across many tasks in one invocation, on both the `/v1` and local SQLite paths.
1223
+ Previously `bulk` could reassign a plan across many tasks but could not tag them,
1224
+ and `todos tag` / `todos untag` take one id and one tag — so stamping a tag across a
1225
+ backlog cost one process per task.
1226
+
1227
+ Semantics chosen so a large backfill is safe to run and safe to re-run:
1228
+
1229
+ - **Merges, never replaces.** `todos update --tags` replaces the tag list; reusing
1230
+ that here would strip every unrelated tag from every row the run touched.
1231
+ - **Idempotent.** A row that already satisfies the request is skipped with no write,
1232
+ so a re-run after a partial failure does not bump row versions or emit audit noise.
1233
+ - **Fails closed** when `--tag` is absent, and when `--tag` and `--tags` are both
1234
+ given but name different sets — a silently dropped tag argument would report
1235
+ success while applying the wrong tags.
1236
+ - Tags split on commas only; `:` and `/` are preserved, which namespaced tags such as
1237
+ `repo:secrets` and `gh:hasna/todos` depend on.
1238
+
1239
+ ### Changed
1240
+
1241
+ - **BREAKING (CLI): `todos watch --status` now validates its value against the status
1242
+ vocabulary.** `watch` shared `list`'s closed vocabulary and failed the way `list` used
1243
+ to: an out-of-vocabulary status matched no rows, so `todos watch --status open` painted
1244
+ a permanently empty dashboard that reads as "there is no work". This is worse than the
1245
+ `list` case it mirrors, because a live view invites an operator to sit and watch it.
1246
+ `watch` now exits non-zero and names the accepted vocabulary. Documented aliases
1247
+ (`done` -> `completed`) still resolve, so every previously _valid_ input behaves
1248
+ identically; only previously _invalid_ input changes, from a silent empty view at exit
1249
+ 0 to a named rejection.
1250
+ - **BREAKING (CLI): an empty or blank enum element is now rejected instead of being
1251
+ dropped.** `--status=` was parsed by Commander as an explicit empty string and fell
1252
+ back to the default filter at exit 0, and a blank list member meant `--status pending,`
1253
+ and `--priority high,,critical` were accepted as though a clean list had been typed.
1254
+ Both are almost always a stray comma or a shell expanding `--status "$A,$B"` with one
1255
+ variable unset, and in both cases the filter did not deliver what was asked for.
1256
+ Surrounding whitespace on a non-empty element (` pending , high`) stays tolerated,
1257
+ because that is a shape operators legitimately type.
1258
+
1259
+ ### Fixed
1260
+
1261
+ - **`todos list` now applies its window after sorting rather than before**, so a bounded
1262
+ list returns the first N of the sorted result instead of an arbitrary N that was then
1263
+ sorted among itself. The remote scan is bounded by an explicit ceiling sent to the
1264
+ cloud API; the ceiling applies to the remote path by construction and never to local
1265
+ SQLite.
1266
+ - **`todos list` no longer warns about an assignee it never queried.** The empty-result
1267
+ warning resolved the assignee separately from the query, so `--assigned <bogus>
1268
+ --inbox` warned about a value that was never used as a filter, while
1269
+ `--agent-name "" --assigned <bogus>` stayed silent about the value that actually was.
1270
+ The effective assignee is now resolved once and read for both the filter and the
1271
+ warning.
1272
+ - **`todos add` warns instead of silently filing a task with no project.** The local
1273
+ branch fell back to project auto-detection when `--project` was absent; the cloud
1274
+ branch did not, and the fleet runs cloud, so every fleet create stored a NULL project
1275
+ silently. Such a row appears in no per-seat list and no drain reaches it. Measured
1276
+ against the hosted store on 2026-08-03: 578 of 3231 pending rows (17.9%) carried a NULL
1277
+ project, and 93 of the 283 rows created in the preceding 24 hours (32.9%) did. It warns
1278
+ rather than rejects, deliberately — a third of live creations omit the project, so
1279
+ rejecting would take the CLI offline for that traffic. `--no-project` silences the
1280
+ warning, mirroring the `--unassigned` flag this command already ships for the same
1281
+ shape of problem on the assignee field. The cloud create also sends `working_dir`
1282
+ again, which the local branch had always sent.
1283
+ - **`todos workflows`, `template-library`, `onboarding` and `sdk-fixtures` now serve
1284
+ their bundled static content on the `/v1` route** instead of exiting 1 with
1285
+ `REMOTE_COMMAND_UNSUPPORTED`, which is what `manual` — also bundled static — already
1286
+ did. The shipped manual documents `todos workflows` in its own examples. The four are
1287
+ admitted per invocation rather than wholesale: `onboarding --import` and
1288
+ `sdk-fixtures --show/--write` reach a store-backed import path, so those invocations
1289
+ are still refused rather than opening a route to SQLite where the local fallback is
1290
+ deliberately disabled.
1291
+
1292
+ ## [0.14.0] - 2026-08-02
1293
+
1294
+ This release was prepared as `0.13.13` and renumbered before publishing. It carries a
1295
+ breaking CLI change, and under 0.x semver the minor is the field that signals one — a
1296
+ patch number would have let a `^0.13` range pick it up silently. No `0.13.13` was ever
1297
+ published to npm; `0.13.12` is the version this supersedes.
1298
+
1299
+ ### Changed
1300
+
1301
+ - **BREAKING (CLI and HTTP API): an out-of-vocabulary enum value is now rejected
1302
+ instead of returning an empty result.** Passing an unsupported value — the common
1303
+ case is a `--status` word that is not one of `pending`, `in_progress`, `completed`,
1304
+ `failed`, `cancelled` — previously produced a clean empty list that was
1305
+ indistinguishable from "no tasks matched", so a typo read as a true negative. The
1306
+ CLI now exits non-zero and names the accepted vocabulary, and three HTTP endpoints
1307
+ now answer **400** where they previously answered 200 with an empty list:
1308
+ `GET /v1/tasks` validates `status` and `priority`, and `GET /api/tasks` and
1309
+ `GET /api/tasks/export` validate `status`. `?status=open` is the canonical
1310
+ example. Any script or API client that depended on the silent-empty behaviour
1311
+ will start failing loudly; that is the intent.
1312
+
1313
+ ### Fixed
1314
+
1315
+ - **Reaching any terminal status through the generic update path now releases the
1316
+ task lock.** Only `completed` did before, so `--status failed` and
1317
+ `--status cancelled` left `locked_by` and `locked_at` set permanently — a terminal
1318
+ task cannot be started again, so nothing could ever re-acquire the lock and clear
1319
+ it, and lazy repair-on-reacquisition could never reach the row. Both storage
1320
+ adapters are corrected; on the hosted adapter the hole was wider than first
1321
+ reported, having never released a lock for any terminal status including
1322
+ `completed`.
1323
+ - The CLI now surfaces the server's own reason on a remote 400/4xx failure instead
1324
+ of swallowing it, so a rejected request explains itself rather than presenting as
1325
+ a generic failure.
1326
+ - Registering an agent through the hosted path no longer mints a case variant of an
1327
+ existing name. This closes the remaining write path that could recreate the
1328
+ split-identity condition 0.13.12 fixed on the read side.
1329
+ - **The MCP `rename_agent` and `rebalance_workload` tools now leave an ambiguous
1330
+ case-variant assignee alone instead of resolving it arbitrarily.** Where a database
1331
+ holds two distinct agents whose names differ only by case — the same split-identity
1332
+ condition the entry above closes at its source — `rename_agent` matched `assigned_to`
1333
+ case-insensitively and therefore moved _both_ agents' tasks onto the renamed one, and
1334
+ `rebalance_workload` indexed both roster rows under one lower-cased key, attributing
1335
+ those tasks to whichever row happened to be indexed last. `rename_agent` now widens to
1336
+ a case-insensitive match only when the old name uniquely identifies the agent being
1337
+ renamed, and uses an exact match when it does not; `rebalance_workload` marks a
1338
+ colliding alias ambiguous and skips those assignments rather than guessing. The
1339
+ observable difference on a database carrying such a collision is that `rename_agent`
1340
+ reports fewer updated tasks and `rebalance_workload` reports fewer moves — that is the
1341
+ wrong work no longer being done. This is not marked breaking because what it replaces
1342
+ was not a contract: in the colliding case the outcome was arbitrary — whichever roster
1343
+ row was indexed last — and no caller can have depended on it. It does carry one
1344
+ regression, named here rather than left to be found: `rename_agent`'s uniqueness guard
1345
+ resolves the old name through `resolvePartialId`, which treats any string of 36
1346
+ characters or more as a full UUID and looks it up on the `id` column, so it reports
1347
+ "not unique" for every agent name that long even when no collision exists. Such an
1348
+ agent's differently-cased task rows are now left on the stale name after a rename.
1349
+ Tracked as a follow-up.
1350
+ - Multi-value task filters are now modelled correctly in the generated API schema
1351
+ (`style: form`, `explode: false`), so generated clients emit the **comma-separated
1352
+ single parameter** the server actually reads — `?status=pending,in_progress`. The
1353
+ server reads each filter with `searchParams.get()` and splits on `,`; it never calls
1354
+ `getAll`, so a client emitting a repeated parameter has only its first value honoured.
1355
+
1356
+ ### Note
1357
+
1358
+ - **This release does not retro-clear task locks that already leaked.** That sweep is
1359
+ deliberately excluded rather than overlooked: clearing a lock bumps the row version
1360
+ and rewrites `updated_at`, and the change feeds page on `updated_at`, so sweeping
1361
+ the existing terminal rows would push every one of them through those feeds to
1362
+ repair something the reporting layer already handles. This release stops new leaks;
1363
+ the accumulated rows remain a separate, explicit decision.
1364
+
1365
+ ## [0.13.12] - 2026-08-02
1366
+
1367
+ ### Fixed
1368
+
1369
+ - Alias-resolve `assigned_to` on every remaining sibling exact-match call site
1370
+ (~20 sites behind one shared resolver), including a rebalance-load path where
1371
+ an overloaded agent's queue was silently undercounted. `assigned_to` has held
1372
+ an agent ID from one write path and a resolved name from another, plus case
1373
+ variants, so exact-match reads returned a silent subset. `#160` fixed the
1374
+ list filter; this closes the remaining call sites.
1375
+ - **This is a local `bun:sqlite` fix.** Agents on the hosted API (`/v1`) are
1376
+ unaffected: the hosted service lacks Postgres equivalents for most of these
1377
+ code paths and runs an older client version regardless. This release changes
1378
+ nothing for hosted-mode agents.
1379
+
1380
+ ## [0.13.11] - 2026-08-02
1381
+
1382
+ ### Fixed
1383
+
1384
+ - `todos update --assign <seat>` now points callers at a `--assign-seat` invocation
1385
+ that actually runs. The prior refusal message named the flag in a shape the verb
1386
+ rejects, so an agent following it hit a second error and could conclude the CLI
1387
+ was recommending a broken flag. The corrected hint is client-side, so it reaches
1388
+ every installed agent immediately rather than waiting on the hosted service.
1389
+
1390
+ ## [0.13.10] - 2026-08-02
1391
+
1392
+ ### Fixed
1393
+
1394
+ - Starting a failed task through the versioned remote API now returns a deterministic
1395
+ `409 TASK_NOT_STARTABLE` response that explains the required reset to `pending`, and
1396
+ the CLI preserves that domain error instead of reporting the Todos authority as
1397
+ unavailable. Pending tasks continue to start normally on both SQLite and PostgreSQL.
1398
+
1399
+ ## [0.13.3] - 2026-07-30
1400
+
1401
+ ### Added
1402
+
1403
+ - **`@hasna/todos/testing` — a shipped test-isolation helper, so a consumer's test suite
1404
+ cannot write into the shared hosted store.** Measured on 2026-07-30, three repositories
1405
+ had between them left **2,094 rows** in the live authority purely from tests:
1406
+ 1,151 from a consumer test suite (`Short ID resolution test`, `Scoped getTask resolution
1407
+ test`, `seed-task-<epoch>` under `test-resolution-*` projects) and 943 `Merge the release
1408
+ PR` rows from `hasna/loops`' `drain.test.ts`, which shelled out to the real `todos` CLI
1409
+ with an unmodified `process.env`. None was ever assigned, commented on, or actioned.
1410
+ The cause is the same in every case: the client resolves its transport from the
1411
+ environment, and every shell on a fleet machine exports the shared-store pointers.
1412
+
1413
+ The export is `SHARED_TODOS_STORE_ENV_KEYS` (the routing variables, held as **one
1414
+ constant shared with the resolver that reads them** — including the legacy unprefixed
1415
+ API URL and key aliases a hand-rolled consumer copy reliably misses),
1416
+ plus `localTodosTestEnv()`, `applyLocalTodosTestEnv()` (with exact restore) and
1417
+ `assertLocalTodosTestEnv()`. It is published as a subpath so the list lives next to the
1418
+ resolver: a consumer-side reimplementation stops protecting anything the day this package
1419
+ adds a routing variable, and that failure mode is a green suite silently writing to
1420
+ production.
1421
+
1422
+ Guarded by two tests that fail rather than drift: one regex-scans the resolver for every
1423
+ `TODOS_*` variable it reads and fails if any is neither scrubbed nor explicitly declared
1424
+ local-only; the other fails if the subpath ships without its declaration file.
1425
+
1426
+ ### Fixed
1427
+
1428
+ - **`todos doctor` no longer reports healthy on a dataset full of orphaned rows.** In remote
1429
+ mode it returned a HARDCODED `ok: true` after checking only authentication and route
1430
+ availability, printed three green check marks and exited `0` — on the live authority that
1431
+ meant 10,176 orphaned tasks (4,735 of them still open) and 45 unbound task lists passed as
1432
+ healthy, which is why nothing else in that dataset was caught either. It already fetched
1433
+ every task list and every project and reduced them to `.length`, so the rows it needed were
1434
+ in hand and discarded. Neither mode ever counted a null or dangling `project_id` /
1435
+ `task_list_id`, and the local path never set a non-zero exit code even for an
1436
+ error-severity check.
1437
+ - **The verdict is now derived from the counts that are printed.** Doctor counts six
1438
+ referential conditions (`tasks_without_project`, `tasks_without_task_list`,
1439
+ `tasks_with_unregistered_project`, `tasks_with_unregistered_task_list`,
1440
+ `task_lists_without_project`, `task_lists_with_unregistered_project`), one aggregate query
1441
+ each, and `ok` / the exit code are a pure function of those rows. A dangling reference is
1442
+ always an error; a null one escalates from warning to error once it hides OPEN work.
1443
+ - **A condition that cannot be measured is never folded into "all clear".** It is reported as
1444
+ `NOT CHECKED` with the reason, and the report is INCOMPLETE.
1445
+
1446
+ ### Added
1447
+
1448
+ - **Direct unit coverage for previously untested local task-runner and agent-task sync modules.**
1449
+ The tests exercise successful execution and synchronization as well as missing tasks,
1450
+ failed handlers, aborted runs, empty queues, malformed files, and unavailable paths.
1451
+ - **`GET /v1/integrity`** — per-condition referential-integrity counts computed by the backing
1452
+ storage engine, for **both** SQLite and the Postgres JSONB record store (which has no
1453
+ foreign keys and is therefore where these rows actually accumulate). A backend that cannot
1454
+ answer returns `501` rather than a false clean. `TodosStorageAdapter.integrity` is
1455
+ implemented by both adapters.
1456
+ - **`todos doctor --scan-tasks`** — remote-only, read-only paged walk of `/v1/tasks` that
1457
+ derives the task-level conditions when the authority predates the aggregate route
1458
+ (`/v1/tasks` filters cannot express `IS NULL`). A walk that cannot complete marks the
1459
+ conditions unverified instead of reporting a partial count as truth.
1460
+
1461
+ ### Changed
1462
+
1463
+ - **BREAKING (exit code): `todos doctor` exit codes are now a verdict.** `0` clean · `1`
1464
+ findings (any orphan/dangling reference, or an error-severity check) · `2` incomplete (no
1465
+ findings, but a condition could not be measured). Advisory warnings (stale `in_progress`
1466
+ tasks, project paths missing on this machine, duplicate indexes) do not change the exit
1467
+ code. `--no-fail-on-findings` is the explicit opt-out for a consumer that gates on exit `0`;
1468
+ findings are still reported, and the printed exit code is always the one the process
1469
+ returns — a suppressed run prints `(exit 0 — findings gate suppressed by
1470
+ --no-fail-on-findings; the verdict is 1)` rather than a `(exit 1)` the process never
1471
+ used. `doctor --json` gains an `integrity` block (`schema_version:
1472
+ "todos.integrity.v1"`) plus `exit_code` (the status the process RETURNS),
1473
+ `verdict_exit_code` (the status the rows IMPLY) and `fail_on_findings`; `ok` keeps its
1474
+ name and finally means what it says.
1475
+ - **Integrity findings are report-only.** `doctor --apply` repairs schema/hygiene only and
1476
+ never rewrites, deletes or re-points an orphaned row; `--apply` remains refused outright
1477
+ against a remote authority.
1478
+
1479
+ ## [0.13.2] - 2026-07-28
1480
+
1481
+ ### Fixed
1482
+
1483
+ - **`blocked_by` now means "tasks that block me" on every machine-readable surface —
1484
+ the inverted orientation deadlocked dependency chains** (task 4599ef37). `todos deps
1485
+ <id> --json` (schema `todos.task_dependency_edges.v1`), `todos show/inspect --json`,
1486
+ `getTaskWithRelations`, and the self-hosted hydration all placed this task's
1487
+ DEPENDENTS in the field named `blocked_by`, while the human `Depends on:`/`Blocks:`
1488
+ output read the same data correctly. Schedulers consuming the JSON by name
1489
+ (@hasnaxyz/factory) therefore refused to dispatch the UPSTREAM task of every
1490
+ dependency chain with `dependency_unmet` — the chain deadlocked its own blocker.
1491
+ Now: `dependencies` = all prerequisites (upstream), `blocked_by` = the incomplete
1492
+ prerequisites currently blocking the task (empty ⇒ dispatchable; completed and
1493
+ cancelled prerequisites do not block, matching `getBlockedTasks`), and the
1494
+ dependents moved to a new `blocks` field (matching the human `Blocks:` label). The
1495
+ schema version stays `v1`: consumers pin it fail-closed, and the payload now finally
1496
+ matches what v1's field names always claimed. The `/v1/tasks/:id/dependencies` wire
1497
+ payload additionally carries the incoming edges under a new `blocks` key while
1498
+ keeping the deprecated `blocked_by` alias (same contents) so pre-0.13.2 fleet
1499
+ clients keep rendering `Blocks:` correctly; the CLI prefers `blocks` and falls back
1500
+ to the legacy name against older servers.
1501
+
1502
+ - **Secret redaction no longer destroys the clean value under a redacted key.** Once a
1503
+ secret-shaped object key was reduced to a pattern-specific placeholder
1504
+ (`[REDACTED_GITHUB_TOKEN]`, `[REDACTED_NPM_TOKEN]`), key-based redaction matched the word
1505
+ `TOKEN` inside the placeholder itself and replaced the untouched value beneath it with
1506
+ `[REDACTED]` — silent loss of non-secret data. Redaction is now idempotent for keys that are
1507
+ _entirely_ a placeholder. Keys of the form `NAME=[REDACTED]` are deliberately **not** exempt:
1508
+ that shape is what env-assignment redaction produces, and the value beneath it is opaque, so
1509
+ key-based redaction must still apply there. **Accepted trade-off:** a key named _literally_
1510
+ `[REDACTED_GITHUB_TOKEN]` / `[REDACTED_TOKEN]` / `[REDACTED_PASSWORD]` no longer has its value
1511
+ redacted by key name — previously it did, because the placeholder text contains `TOKEN` /
1512
+ `PASSWORD`. Such a key is indistinguishable from this module's own output, and keeping it
1513
+ exempt is what makes redaction idempotent.
1514
+
1515
+ ### Known issues
1516
+
1517
+ - **Secret-bearing metadata _keys_ are not redacted on every write path.** `redactValue()`
1518
+ redacts metadata values but leaves key text intact; only `sanitizePreWriteValue()` rewrites
1519
+ keys. Task metadata is covered (via `sanitizeUpdateTaskInput`), but `task_findings.metadata`
1520
+ is built with `redactValue()` alone and persists a credential placed in key position — and
1521
+ `metadata_keys` is emitted in compact finding output, whose contract states that "metadata
1522
+ values are intentionally omitted", treating keys as the safe half. Pre-existing, not
1523
+ introduced here; tracked separately.
1524
+
1525
+ ### Documentation
1526
+
1527
+ - **Documents pre-write secret sanitation, which has been active and undocumented since
1528
+ 0.12.0.** Credential-shaped text in task `title`, `description`, `tags`, `metadata`, `reason`,
1529
+ comments, verification, dispatch, inbox, run and artifact payloads is redacted **before it is
1530
+ persisted**, not at display time. Consequences worth knowing: the original text is **not
1531
+ recoverable** — `show`/`inspect` return the stored, redacted value, and there is currently no
1532
+ `--raw` / `--no-redact` escape hatch. This supersedes the earlier contract (0.11.x) in which
1533
+ only broad `list`/`search` output was redacted while explicit detail output returned the raw
1534
+ value; the `redactBroadTask`/`redactBroadTasks` display layer still exists but can no longer
1535
+ observe an unredacted stored value. Legitimate text that merely resembles a credential (for
1536
+ example a note containing `Bearer <12+ chars>`) is redacted on write and cannot be restored.
1537
+
1538
+ ## [0.13.1] - 2026-07-27
1539
+
1540
+ ### Added
1541
+
1542
+ - **Machine-readable dependency reads.** `todos deps <id> --json` now returns a versioned,
1543
+ status-bearing edge payload (`schema_version: "todos.task_dependency_edges.v1"`) with
1544
+ `dependencies` (upstream prerequisites) and `blocked_by` (downstream dependents) as compact
1545
+ `{ id, short_id, title, status, priority, plan_id, project_id }` nodes — identical in local
1546
+ and self-hosted mode. New whole-project graph read `todos deps --project <ref> --json`
1547
+ (`schema_version: "todos.project_dependency_graph.v1"`) returns `nodes` + adjacency `edges` +
1548
+ `cycles` in a single call, so a scheduler can order a batch of tasks without one `deps <id>`
1549
+ call per task.
1550
+
1551
+ ### Changed
1552
+
1553
+ - **BREAKING (CLI JSON contract): `todos deps <id> --json` output shape changed.** It previously
1554
+ emitted divergent, unversioned shapes — local returned full task rows under
1555
+ `dependencies`/`blocked_by`, while self-hosted returned bare `{ task_id, depends_on }` edges
1556
+ with no status. Both now emit the unified `todos.task_dependency_edges.v1` shape above (compact
1557
+ nodes; self-hosted gains id + status parity). Scripts that read fields only present on the full
1558
+ task row (e.g. `description`, `tags`, `created_at`, `metadata`) from `deps --json` must migrate to
1559
+ `show --json` / `inspect --json`. Human (non-JSON) output and `deps <id> --graph --json` are
1560
+ unchanged.
1561
+
1562
+ ## [0.13.0] - 2026-07-25
1563
+
1564
+ ### Security
1565
+
1566
+ - **`/api/*` and `/mcp` no longer fail open when no API key is configured.** `checkAuth`
1567
+ began with `if (!apiKey && !generatedKeysEnabled) return null; // no key configured,
1568
+ skip auth`, so a server started without `TODOS_API_KEY` and without a stored key
1569
+ treated **every** request as authorized. On any deployment that binds a non-loopback
1570
+ host (e.g. `HOST=0.0.0.0` behind a load balancer) that published, to anonymous
1571
+ callers: `POST /mcp` (the full MCP tool catalog plus `tools/call` — create/start/
1572
+ complete/fail task, register agent, findings, run transactions), the entire `/api/*`
1573
+ REST CRUD surface (tasks, projects, agents, plans, orgs, templates, webhooks,
1574
+ pr-groups), the information-disclosing `/api/doctor` (internal database path) and
1575
+ `/api/headless` (boundary manifest), and the unbounded SSE streams `/api/events` and
1576
+ `/api/tasks/stream`. The MCP mount was already inside the auth choke point — the
1577
+ choke point itself returned "authorized".
1578
+ The unconfigured case now **denies**. A single startup decision
1579
+ (`resolveAuthPosture`, `src/server/auth-posture.ts`) resolves one of:
1580
+ - `enforce` — a credential source exists (`TODOS_API_KEY`/`--api-key`, or ≥1 stored
1581
+ key): every `/api/*` and `/mcp` request must present it;
1582
+ - `local-plane-disabled` — a hosted deployment (cloud `DATABASE_URL` configured, so
1583
+ the self-authenticating `/v1` plane works) with no local credential: `/api/*` and
1584
+ `/mcp` are not served at all (`404 LOCAL_PLANE_DISABLED`), while `/v1` and the
1585
+ health probes keep working, so closing the hole cannot cause an outage;
1586
+ - `anonymous-loopback` — explicitly requested via `--allow-anonymous` /
1587
+ `TODOS_ALLOW_ANONYMOUS=1` **and** a loopback bind host; anonymous requests are
1588
+ additionally required to come from a loopback transport peer (the check ignores
1589
+ `x-forwarded-for`, so `TODOS_TRUST_PROXY=1` cannot be used to spoof one). The
1590
+ stored-key check is re-evaluated per request under this posture, so
1591
+ `todos api-keys create` closes an already-open anonymous window without a restart;
1592
+ - otherwise the server **refuses to start**, exiting non-zero with an actionable
1593
+ error naming `TODOS_API_KEY` — starting wide open is never an option.
1594
+ `/v1` was not affected (it authenticates itself against the cloud API-key store) and
1595
+ `/health`, `/ready`, `/version`, `/openapi.json` remain public by design, so
1596
+ load-balancer and container health checks are unchanged.
1597
+ Regression coverage: `src/server/auth-fail-closed.test.ts` (unconfigured server exits
1598
+ non-zero and nothing listens; every `/api/*` read and write route, `POST`/`GET`/
1599
+ `DELETE /mcp`, and `/v1` reject a credential-less request; probes stay public) and
1600
+ `src/server/auth-posture.test.ts` (full posture matrix, including "no input ever
1601
+ yields an anonymous plane on an off-box bind").
1602
+
1603
+ ### Changed
1604
+
1605
+ - **BREAKING (local):** `todos serve` / `todos-serve` with no `TODOS_API_KEY` and no
1606
+ stored API key now exits non-zero instead of serving `/api/*` anonymously. Migrate
1607
+ with `todos api-keys create "<name>"` (then send `x-api-key`), or, for loopback-only
1608
+ local development and the bundled dashboard, `todos serve --allow-anonymous`. The
1609
+ flag is refused for a non-loopback `--host`. `todos-mcp --http` is unchanged: it is
1610
+ loopback-pinned by contract, so it opts into the anonymous local plane implicitly
1611
+ (set `TODOS_API_KEY` to enforce auth there too).
1612
+ - `src/test/local-routing-env.fixture.test.ts` also clears `DATABASE_URL` /
1613
+ `TODOS_DATABASE_URL` / `HASNA_TODOS_DATABASE_URL` / `TODOS_ALLOW_ANONYMOUS` so a
1614
+ live DSN or opt-in in a developer's environment cannot flip a local-intent test's
1615
+ auth posture.
1616
+
1617
+ ### Docs
1618
+
1619
+ - `docs/hosted-auth-runbook.md` — posture matrix, the per-caller migration table, the
1620
+ owner-gated ECS redeploy steps (including that the deployed task definition sets no
1621
+ `TODOS_API_KEY`, so no new secret is required to close the hole), and post-deploy
1622
+ verification commands.
1623
+
1624
+ ## [0.12.3] - 2026-07-25
1625
+
1626
+ ### Fixed
1627
+
1628
+ - Remote `show`/`inspect` no longer report empty `dependencies`/`blocked_by` while `deps <id>` lists persisted edges (#58). The `/v1` task row endpoint returns no relation graphs, and the cloud detail assembly hard-coded both arrays to `[]`, so a blocked remote task looked unblocked and `inspect` never printed its `BLOCKED by N unfinished dep(s)` warning. `show`/`inspect` now read `GET /v1/tasks/:id/dependencies` and hydrate each referenced id into a full task row (deduplicated across both directions, bounded concurrency), matching local-mode output. An edge whose target task cannot be read degrades to an explicit `(unavailable task …)` placeholder instead of erasing the readable relations, and a dependency-endpoint failure warns on stderr rather than sinking the whole detail view (a `404`/`501` from a server without the route is silently treated as "no edges").
1629
+
1630
+ ## [0.12.2] - 2026-07-25
1631
+
1632
+ ### Fixed
1633
+
1634
+ - `todos bulk plan|move-plan --plan <id>` / `--clear-plan` now works under a remote (`self_hosted`/cloud) authority. Stage A rejected the whole invocation with `REMOTE_COMMAND_UNSUPPORTED` because the bulk handler resolved the plan reference through the local SQLite `resolvePlanId`, which is unavailable in remote mode. The cloud path now resolves the plan against the shared dataset (`cloudResolvePlan`) once, up front, so an unknown plan still fails closed before any task is mutated, and the Stage-A gate admits the two plan actions. Single-task `todos update <id> --plan` was already remote-capable; bulk reassignment is now at parity (#31).
1635
+
1636
+ ## [0.12.1] - 2026-07-24
1637
+
1638
+ ### Fixed
1639
+
1640
+ - SQLite task search no longer silently degrades or fails on queries containing punctuation. The `shouldUseFts` gate rejected any query with punctuation (e.g. `login: urgent`), falling back to a literal substring LIKE that matched nothing. A real FTS5 query parser now handles punctuation safely: AND-by-default terms, quoted phrases, and prefix matching, with FTS5 operator characters stripped/quoted instead of rejected.
1641
+ - Task search is now bounded. `searchTasks` built its SQL with no `LIMIT`, so a broad query scanned/returned the whole table. It now applies a bounded default (1000) and honors an explicit `SearchOptions.limit`.
1642
+
1643
+ ### Changed
1644
+
1645
+ - SQLite FTS ranking now uses `bm25()` column weighting (title >> description > tags), mirroring the Postgres `ts_rank_cd` A/B/C weights so both backends rank equivalently. The FTS path is unioned with the id/short_id/working_dir/metadata LIKE fallback so identifier/fingerprint/path pastes still resolve, with full-text hits ranked first.
1646
+
1647
+ ## [0.12.0] - 2026-07-24
1648
+
1649
+ ### Fixed
1650
+
1651
+ - Postgres full-text search parity (cloud/self-hosted returned nothing). `searchTasks` only ever queried the local SQLite FTS5 index, which is empty on a Postgres deployment. Task search now runs through the storage abstraction (`store.tasks.list({ query })`) so cloud/self-hosted executes a real Postgres query.
1652
+
1653
+ ### Added
1654
+
1655
+ - `migrations/0006_task_fulltext_search.sql`: a weighted (`title`>`description`>`tags`) `tsvector` generated column on `todos_sync_records`, a GIN index for ranked full-text search, and a `pg_trgm` trigram GIN index for typo/fuzzy matching, all diacritics-insensitive via an immutable `unaccent` wrapper. Idempotent with automatic backfill; mirrored into `postgresTodosSyncSchemaSql` so fresh cloud bootstraps get it too.
1656
+ - `TaskFilter.query` full-text field, honored by BOTH storage adapters so `GET /v1/tasks?q=` searches whether the server is Postgres- or SQLite-backed. The Postgres adapter emits a `websearch_to_tsquery` predicate (AND-by-default, quoted phrases, punctuation-tolerant) with a single-term `pg_trgm` word-similarity fuzzy fallback, ranked by `ts_rank_cd`; the local SQLite adapter routes the query through the FTS5 `searchTasks` path and applies the remaining filters. Exposed over `GET /v1/tasks?q=`; the `todos search` CLI routes through it under a self-hosted authority.
1657
+
1658
+ ## [0.11.96] - 2026-07-24
1659
+
1660
+ ### Security
1661
+
1662
+ - Remove internal production-infrastructure identifiers from the published open-source package. The managed database cluster name and the AWS Secrets Manager runtime path are no longer hardcoded in `src/storage/config.ts`; they are now supplied at runtime by the private hosting wrapper via `HASNA_TODOS_RDS_CLUSTER` and `HASNA_TODOS_RDS_RUNTIME_PATH`, and resolve to `null` when unset (no baked-in defaults).
1663
+ - Scrub the internal cloud domain (`*.hasna.xyz`) from source comments, the `union-backfill` script default endpoint, and test fixtures; compose the private billing host in the headless outbound-boundary allowlist from parts so it is not shipped as a plaintext literal (the outbound guard still blocks it).
1664
+ - Replace the real fleet machine identifier and private Tailscale/LAN addresses in the README machine-topology example with neutral placeholders.
1665
+
1666
+ ### Changed
1667
+
1668
+ - **Breaking (public API):** `getCanonicalTodosRdsConfig()` now accepts an optional `env` argument and returns `cluster` / `runtimeSecretPath` as `string | null`. The exported constants `CANONICAL_TODOS_RDS_CLUSTER` and `CANONICAL_TODOS_RDS_RUNTIME_PATH` are replaced by `CANONICAL_TODOS_RDS_CLUSTER_ENV` and `CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV`.
1669
+
1670
+ ## [0.11.92] - 2026-07-18
1671
+
1672
+ ### Fixed
1673
+
1674
+ - Route supported ordinary CLI coordination commands through the configured authenticated self-hosted `/v1` authority when remote storage mode is selected, before any local database adapter or ID helper can run.
1675
+ - Fail closed on missing credentials, invalid or conflicting storage selectors, unsafe or incompatible authority URLs/routes, redirects, authentication errors, timeouts, and server failures; remote commands never fall back to SQLite.
1676
+ - Make remote storage/status diagnostics HTTP-aware and harden release verification so a candidate must come from a clean tracked tree with commit/tree provenance and verified tarball integrity.
1677
+ - Gate evidence-bearing remote completion on the authority's advertised OpenAPI contract, resolve short task references only from a stats-stable exhaustive snapshot, and preserve existing completion evidence atomically.
1678
+ - Split public release verification into non-authoritative review and strict prepublish modes, pin Bun 1.3.14, verify tracked bytes/modes and packed binary provenance, and require two identical npm tarballs and payload manifests.
1679
+
1680
+ ## [0.11.76] - 2026-07-06
1681
+
1682
+ ### Fixed
1683
+
1684
+ - Auto project detection now skips disposable git roots under the system temp directory, preventing accidental registration of transient `/tmp` project shells.
1685
+ - Added guarded `todos projects --deregister` support that preserves tasks and refuses to deregister any project with incomplete tasks.
1686
+
1687
+ ## [0.11.75] - 2026-07-05
1688
+
1689
+ ### Added
1690
+
1691
+ - `todos doctor routing` — deterministic routing-metadata drift detection (wrong/null `working_dir`, null/unresolvable `task_list_id`, invalid project paths, cross-repo intent, no-auto conflicts) with per-finding `repair_class` (`safe_auto` | `blocker_*` | `unsupported`), project-stable `--shard i/N` scoping, and a machine-consumable `--json` contract (`todos.routing_doctor.v1`) with documented exit codes (0 clean / 1 findings / 2 invalid invocation).
1692
+ - `todos doctor routing --apply` — safe repairs for `safe_auto` findings only, via supported update paths (no raw DB edits), with a DB backup, per-task evidence comments, and an undo record carrying real per-repair undo commands.
1693
+ - `todos update --working-dir <path>`, `--clear-working-dir`, and `--clear-list` — first-class routing-metadata repair and null-reset flags for existing tasks; `--list` now resolves exact UUID → partial UUID → project-scoped slug (UUID authoritative) and errors on unresolvable references instead of silently succeeding.
1694
+ - `scripts/routing-health-scan.mjs` — deterministic OpenLoops command-loop consumer of the doctor JSON (deduped per-scope task upserts), and `scripts/routing-remediation.workflow.json` — validated planner → worker → adversarial-reviewer remediation workflow spec.
1695
+
1696
+ ### Fixed
1697
+
1698
+ - `doctor routing --apply`/`--fix` reached the subcommand as a silent dry-run (Commander actionable-parent option shadowing); the flags now apply and are guarded by a CLI-level end-to-end regression test.
1699
+ - Multi-megabyte `doctor routing --json` reports intermittently truncated when stdout was a pipe; now emitted through the flush-safe writer.
1700
+
1701
+ ## [0.11.74] - 2026-07-05
1702
+
1703
+ ### Fixed
1704
+
1705
+ - Task `route_state` made authoritative and aligned with the OpenLoops drain: the `auto:route`/`route:enabled` tag authorizes routing when `route_enabled` is unset; explicit denies and `no-auto` still deny. Added `route_class`, route evidence, and optional project-root verification (backfilled entry for the #37/#38 release).
1706
+
1707
+ ## [0.11.73] - 2026-07-03
1708
+
1709
+ ### Fixed
1710
+
1711
+ - **Security:** authenticate the `/mcp` endpoint and rate-limit it (and `/health`) — previously an unauthenticated write surface even with `--api-key` set. Preserves the 127.0.0.1-no-key default.
1712
+ - **Security:** key the HTTP rate limiter on `server.requestIP`; honor `X-Forwarded-For` only under `TODOS_TRUST_PROXY` (was spoofable / shared "unknown" bucket).
1713
+ - Task completion lifecycle: `completeTask` returns the correct post-commit version (was stale → follow-up updates conflicted) and is idempotent; completion via `updateTask`/PATCH/CLI now spawns recurrence exactly once and clears the lock; reopening clears `completed_at`; confidence preserved when omitted.
1714
+ - `PATCH /api/tasks/:id` accepts a client version and maps conflicts→409 / not-found→404 (was last-write-wins / 500).
1715
+ - `todos mcp` now starts a stdio server and bare `todos-mcp` defaults to stdio (register writers pass `--stdio`) — clients were booting HTTP and never speaking stdio.
1716
+ - `--json` now works on `next`/`claim`/`status`/`fail`/`active`/`stale`/`redistribute` including empty results; added `log-progress` alias; typed not-found errors; CLI input validation.
1717
+ - Removed phantom MCP tools from the registry, CLI/MCP parity manifest, and golden fixture; exposed `upsert_task` and metadata tools in profiles.
1718
+ - `ensureSchema` backfills migration-48 columns; WAL-safe `restoreDatabase`; stable cursor ordering; `countTasks` mirrors `listTasks` filters; SDK no longer drops 4-byte bodies and authenticates `subscribe()`.
1719
+
1720
+ ## [0.11.72] - 2026-07-02
1721
+
1722
+ ### Added
1723
+
1724
+ - Multi-store route source discovery.
1725
+
1726
+ ## [0.11.71] - 2026-07-01
1727
+
1728
+ ### Changed
1729
+
1730
+ - Plan slug compatibility alignment and related maintenance.
1731
+
1732
+ ## [0.11.70] - 2026-06-30
1733
+
1734
+ ### Changed
1735
+
1736
+ - Maintenance and internal improvements (backfilled entry).
1737
+
1738
+ ## [0.11.69] - 2026-06-29
1739
+
1740
+ ### Fixed
1741
+
1742
+ - Keep large CLI JSON output complete when stdout is piped, including `todos list --json` and `todos list --format json` for large or status-filtered task lists.
1743
+
1744
+ ## [0.11.59] - 2026-06-27
1745
+
1746
+ ### Added
1747
+
1748
+ - Add deterministic task upsert support for loop/workflow automation.
1749
+ - Emit richer task event metadata for task-created routing workflows.
1750
+ - Expose task upsert through CLI, SDK, MCP, and HTTP API surfaces.
1751
+
1752
+ ## [0.9.29] - 2026-03-12
1753
+
1754
+ ### Performance
1755
+
1756
+ - Eliminate redundant `getTask()` re-fetches in `updateTask`, `startTask`, `completeTask` — saves 1 SELECT per mutation (33% fewer DB queries)
1757
+
1758
+ ## [0.9.28] - 2026-03-12
1759
+
1760
+ ### Performance
1761
+
1762
+ - Strip all 119 `.describe()` strings from MCP tool params (lean stubs pattern) — 90% cold start token reduction
1763
+
1764
+ ## [0.9.27] - 2026-03-11
1765
+
1766
+ ### Added
1767
+
1768
+ - CLI `--format=compact|csv|json|table` on `todos list` — compact is 95% fewer tokens than JSON
1769
+
1770
+ ## [0.9.26] - 2026-03-11
1771
+
1772
+ ### Changed
1773
+
1774
+ - MCP mutation responses (create/update/start/complete) now return compact 1-line format instead of 10-line detail — 80% smaller
1775
+
1776
+ ## [0.9.25] - 2026-03-11
1777
+
1778
+ ### Added
1779
+
1780
+ - REST API field filtering: `GET /api/tasks?fields=id,title,status` returns only requested fields — 60-80% smaller responses
1781
+
1782
+ ## [0.9.24] - 2026-03-11
1783
+
1784
+ ### Added
1785
+
1786
+ - `search_tools` and `describe_tools` MCP meta-tools for dynamic tool discovery (90-96% input token reduction)
1787
+ - Trimmed 14 MCP tool descriptions to ≤60 chars
1788
+
1789
+ ## [0.9.23] - 2026-03-11
1790
+
1791
+ ### Added
1792
+
1793
+ - `@hasna/todos-sdk` — universal agent SDK package (TodosClient, OpenAI-compatible schemas)
1794
+ - Agent discovery: `GET /api/agents/me` with auto-register, stats, assigned tasks
1795
+ - Agent task queue: `GET /api/agents/:id/queue` sorted by priority
1796
+ - Smart task claiming: `POST /api/tasks/claim` — atomically claim next available task
1797
+ - Blocking dependency checks: `startTask` rejects tasks with unmet deps
1798
+ - Completion evidence: `completeTask` accepts `{ files_changed, test_results, commit_hash, notes }`
1799
+ - SSE event stream: `GET /api/events` for real-time task change notifications
1800
+ - Auto-assignment: `findBestAgent()` assigns to least-loaded agent with role=agent
1801
+ - `get_my_tasks` MCP tool for agent self-discovery
1802
+
1803
+ ## [0.9.22] - 2026-03-11
1804
+
1805
+ ### Changed
1806
+
1807
+ - README: comprehensive REST API docs (30+ endpoints), MCP tools reference (40 tools), CLI reference
1808
+
1809
+ ## [0.9.21] - 2026-03-10
1810
+
1811
+ ### Added
1812
+
1813
+ - Server API endpoints for audit log, webhooks, templates
1814
+ - Dashboard activity feed showing audit log entries
1815
+ - 61 new tests for audit, webhooks, templates, auto-audit
1816
+
1817
+ ## [0.9.20] - 2026-03-10
1818
+
1819
+ ### Added
1820
+
1821
+ - 11 new MCP tools: `get_task_history`, `get_recent_activity`, `create_webhook`, `list_webhooks`, `delete_webhook`, `create_template`, `list_templates`, `create_task_from_template`, `delete_template`, `approve_task`
1822
+ - Auto-audit: task mutations (start/complete/update) automatically log to task_history
1823
+ - CLI commands: `todos history`, `todos approve`, `todos templates`
1824
+ - `--estimated` and `--approval` flags on `todos add` and `todos update`
1825
+
1826
+ ## [0.9.19] - 2026-03-10
1827
+
1828
+ ### Fixed
1829
+
1830
+ - Bulletproof migration system: `ensureSchema()` individually checks every table, column, and index on startup — handles fresh install, any upgrade path, partial migration recovery
1831
+
1832
+ ## [0.9.18] - 2026-03-10
1833
+
1834
+ ### Added
1835
+
1836
+ - Migration 10: audit log (`task_history`), webhooks, task templates, estimated time, approval workflow, agent permissions
1837
+ - `logTaskChange`, `getTaskHistory`, `getRecentActivity` for audit trail
1838
+ - `createWebhook`, `listWebhooks`, `deleteWebhook`, `dispatchWebhook` with HMAC signatures
1839
+ - `createTemplate`, `listTemplates`, `deleteTemplate`, `taskFromTemplate`
1840
+ - `estimated_minutes`, `requires_approval`, `approved_by`, `approved_at` on tasks
1841
+ - `permissions` on agents (default `["*"]`)
1842
+
1843
+ ## [0.9.17] - 2026-03-10
1844
+
1845
+ ### Added
1846
+
1847
+ - Plans page in web dashboard with data table, markdown description, create/edit dialogs
1848
+ - Plans can be attached to projects, task lists, or be free-standing
1849
+ - Plans have owner agent (`agent_id`)
1850
+ - Full REST API for plans: GET/POST/PATCH/DELETE /api/plans
1851
+
1852
+ ## [0.9.16] - 2026-03-10
1853
+
1854
+ ### Changed
1855
+
1856
+ - npm package published with public access
1857
+ - Added `publishConfig.access: "public"` to package.json
1858
+
1859
+ ## [0.9.15] - 2026-03-10
1860
+
1861
+ ### Changed
1862
+
1863
+ - Open-source release polish: badges, dashboard/API docs in README
1864
+ - Fix git clone URL, MCP server version, SECURITY.md versions
1865
+ - Add repository, homepage, bugs, engines to package.json
1866
+ - Remove self-dependency and postinstall
1867
+
1868
+ ## [0.9.14] - 2026-03-09
1869
+
1870
+ ### Added
1871
+
1872
+ - 27 new tests: lock expiry, partial ID resolution, updateAgent, getTaskListBySlug, ensureTaskList, server CRUD, export, bulk ops
1873
+
1874
+ ## [0.9.13] - 2026-03-09
1875
+
1876
+ ### Added
1877
+
1878
+ - Agent role field (migration 8) with admin/agent/observer roles
1879
+ - `updateAgent()` function and `PATCH /api/agents/:id` endpoint
1880
+ - Agents page: online/offline status, detail dialog, edit mode, role badges, last task, merge duplicates, comparison
1881
+ - shadcn NavigationMenu for header navigation
1882
+ - Help page moved to top-right as `?` icon button
1883
+
1884
+ ## [0.9.12] - 2026-03-09
1885
+
1886
+ ### Added
1887
+
1888
+ - Kanban view QoL: drag-and-drop, collapse/expand columns, priority filter, sort within columns, group by project, cancelled toggle, compact/detailed mode, inline actions, assignee avatars, hover preview, show more pagination
1889
+
1890
+ ## [0.9.11] - 2026-03-09
1891
+
1892
+ ### Added
1893
+
1894
+ - Auto-find free port when default 19427 is in use (scans up to 100 ports)
1895
+
1896
+ ## [0.9.10] - 2026-03-09
1897
+
1898
+ ### Added
1899
+
1900
+ - CLI: `todos count`, `todos bulk`, `todos watch`, `todos config`
1901
+ - CLI: `--project-name`, `--agent-name`, `--sort` on `todos list`
1902
+ - Better error messages with "Did you mean?" suggestions
1903
+ - JSON error output when `--json` is active
1904
+ - Kanban board view with table/kanban toggle
1905
+
1906
+ ## [0.9.9] - 2026-03-09
1907
+
1908
+ ### Added
1909
+
1910
+ - Card shadows removed across dashboard
1911
+ - shadcn Select, Dialog, DatePicker components
1912
+ - Task detail opens in dialog instead of inline
1913
+ - Delete confirmation dialog
1914
+ - CRUD endpoints for agents and projects in server
1915
+ - Bulk delete for agents and projects
1916
+ - Projects and agents pages: data tables with checkboxes, create dialogs, dropdown menus
1917
+
1918
+ ## [0.9.8] - 2026-03-09
1919
+
1920
+ ### Added
1921
+
1922
+ - Web dashboard (React/Vite/Tailwind/shadcn) served by Bun HTTP server
1923
+ - Dashboard page with stats cards, completion rate, recent activity
1924
+ - Tasks data table with search, filters, sorting, pagination
1925
+ - Projects and agents data tables
1926
+ - Task detail with markdown rendering
1927
+ - Create/edit task dialogs
1928
+ - Dark/light/system theme toggle
1929
+ - Auto-refresh every 30 seconds
1930
+ - Keyboard shortcuts (n, /, 0-4, r, Esc)
1931
+ - Export CSV/JSON
1932
+ - `todos-serve` binary and `todos serve` CLI command
1933
+
1934
+ ### Changed
1935
+
1936
+ - Removed default LIMIT 100 from `listTasks()` — returns all by default
1937
+
1938
+ ## [0.9.7] - 2026-03-08
1939
+
1940
+ ### Added
1941
+
1942
+ - Completion guard: configurable throttling to prevent AI agents from faking task completions
1943
+ - 4 guards: status check, min work duration, rate limit, cooldown
1944
+ - Per-project overrides via config
1945
+ - `CompletionGuardError` with `retryAfterSeconds`
1946
+
1947
+ ## [0.9.0] - 2026-02-28
1948
+
1949
+ ### Added
1950
+
1951
+ - Agents with 8-char UUID identity system (migration 5)
1952
+ - Task lists as named containers (migration 5)
1953
+ - Task prefixes with auto-incrementing short IDs per project (migration 6)
1954
+ - Comprehensive test coverage (295 tests across 14 files)
1955
+
1956
+ ### Changed
1957
+
1958
+ - Integrated agents and task lists across CLI, MCP, and library surfaces
1959
+
1960
+ ## [0.5.1] - 2026-02-15
1961
+
1962
+ ### Added
1963
+
1964
+ - Full detail pages for tasks, plans, and projects
1965
+ - Breadcrumb navigation on detail pages
1966
+ - Tabbed editing interface
1967
+
1968
+ ## [0.5.0] - 2026-02-15
1969
+
1970
+ ### Added
1971
+
1972
+ - API key authentication with SHA-256 hashed keys
1973
+ - Dashboard redesign with docs page, about/contact/legal pages
1974
+ - Combobox, Tabs components
1975
+ - Task detail dialog with tabs
1976
+
1977
+ ## [0.4.0] - 2026-02-15
1978
+
1979
+ ### Added
1980
+
1981
+ - Plans as first-class entity with CRUD across all surfaces
1982
+ - URL-based routing in dashboard
1983
+ - Dashboard home page with stats cards
1984
+
1985
+ ## [0.3.7] - 2026-02-14
1986
+
1987
+ ### Added
1988
+
1989
+ - Initial release with CLI, MCP server, and web dashboard
1990
+ - Task management with optimistic locking
1991
+ - Project management with auto-detection
1992
+ - Full-text search, SQLite WAL mode
1993
+ - Bidirectional sync with Claude Code, Codex, Gemini