@hasna/todos 0.15.51 → 0.16.0

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