@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.
- package/CHANGELOG.md +1884 -0
- package/README.md +314 -37
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai.d.ts.map +1 -1
- package/dist/cli/cloud-router.d.ts +99 -51
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/delegate.d.ts.map +1 -1
- package/dist/cli/commands/environment-snapshots.d.ts.map +1 -1
- package/dist/cli/commands/machines-api.d.ts +4 -0
- package/dist/cli/commands/machines-api.d.ts.map +1 -0
- package/dist/cli/commands/machines.d.ts.map +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/plan-commands.d.ts +3 -0
- package/dist/cli/commands/plan-commands.d.ts.map +1 -0
- package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
- package/dist/cli/commands/pr-group-commands.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts +15 -0
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/storage-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-list-commands.d.ts +3 -0
- package/dist/cli/commands/task-list-commands.d.ts.map +1 -0
- package/dist/cli/commands/template-commands.d.ts +3 -0
- package/dist/cli/commands/template-commands.d.ts.map +1 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +15156 -11088
- package/dist/cli/machine-api.d.ts +7 -0
- package/dist/cli/machine-api.d.ts.map +1 -0
- package/dist/cli/plan-read-api.d.ts +6 -0
- package/dist/cli/plan-read-api.d.ts.map +1 -0
- package/dist/cli/project-api.d.ts +119 -0
- package/dist/cli/project-api.d.ts.map +1 -0
- package/dist/cli/stage-a.d.ts +34 -4
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/cli/task-query-api.d.ts +21 -0
- package/dist/cli/task-query-api.d.ts.map +1 -0
- package/dist/cli/template-api.d.ts +7 -0
- package/dist/cli/template-api.d.ts.map +1 -0
- package/dist/contracts.js +836 -266
- package/dist/db/builtin-templates.d.ts +2 -33
- package/dist/db/builtin-templates.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/handoffs.d.ts.map +1 -1
- package/dist/db/machines.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/plans.d.ts.map +1 -1
- package/dist/db/retrospectives.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/db/task-runs.d.ts +2 -0
- package/dist/db/task-runs.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3863 -1708
- package/dist/lib/assignee-validation.d.ts.map +1 -1
- package/dist/lib/builtin-template-library.d.ts +32 -0
- package/dist/lib/builtin-template-library.d.ts.map +1 -0
- package/dist/lib/cli-reference.d.ts.map +1 -1
- package/dist/lib/config.d.ts +10 -13
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/db-backup.d.ts +1 -1
- package/dist/lib/db-backup.d.ts.map +1 -1
- package/dist/lib/env.d.ts +18 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/environment-snapshots.d.ts.map +1 -1
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/headless-boundaries.d.ts +4 -3
- package/dist/lib/headless-boundaries.d.ts.map +1 -1
- package/dist/lib/local-encryption.d.ts +12 -0
- package/dist/lib/local-encryption.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +90 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/paths.d.ts +9 -0
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/plan-artifacts.d.ts +1 -0
- package/dist/lib/plan-artifacts.d.ts.map +1 -1
- package/dist/lib/plan-client-boundary.d.ts +2 -0
- package/dist/lib/plan-client-boundary.d.ts.map +1 -0
- package/dist/lib/plan-schedule.d.ts +6 -0
- package/dist/lib/plan-schedule.d.ts.map +1 -0
- package/dist/lib/project-panel.d.ts +6 -0
- package/dist/lib/project-panel.d.ts.map +1 -1
- package/dist/lib/project-receipt.d.ts +3 -0
- package/dist/lib/project-receipt.d.ts.map +1 -0
- package/dist/lib/sandbox-profiles.d.ts.map +1 -1
- package/dist/lib/shared-plan-artifacts.d.ts +14 -0
- package/dist/lib/shared-plan-artifacts.d.ts.map +1 -0
- package/dist/lib/sync.d.ts.map +1 -1
- package/dist/lib/task-list-client-boundary.d.ts +3 -0
- package/dist/lib/task-list-client-boundary.d.ts.map +1 -0
- package/dist/lib/task-routing.d.ts.map +1 -1
- package/dist/lib/template-client-boundary.d.ts +2 -0
- package/dist/lib/template-client-boundary.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +45195 -40368
- package/dist/mcp/plan-api.d.ts +6 -0
- package/dist/mcp/plan-api.d.ts.map +1 -0
- package/dist/mcp/remote-authority.d.ts +49 -0
- package/dist/mcp/remote-authority.d.ts.map +1 -0
- package/dist/mcp/task-coordination-api.d.ts +15 -0
- package/dist/mcp/task-coordination-api.d.ts.map +1 -0
- package/dist/mcp/task-list-api.d.ts +8 -0
- package/dist/mcp/task-list-api.d.ts.map +1 -0
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp/tools/environment-snapshots.d.ts.map +1 -1
- package/dist/mcp/tools/machines.d.ts.map +1 -1
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-resources.d.ts.map +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +45 -19
- package/dist/project-registration.js +2830 -557
- package/dist/registry.js +837 -267
- package/dist/release-provenance.json +6 -6
- package/dist/sdk/client.d.ts +51 -4
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +1209 -1080
- package/dist/sdk/resolve.d.ts +91 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +219 -3
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/auth-posture.d.ts +66 -7
- package/dist/server/auth-posture.d.ts.map +1 -1
- package/dist/server/client-ip.d.ts +39 -0
- package/dist/server/client-ip.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +2 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +20556 -15533
- package/dist/server/openapi.d.ts +1341 -83
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/port.d.ts +2 -2
- package/dist/server/port.d.ts.map +1 -1
- package/dist/server/routes.d.ts +3 -6
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/serve.d.ts +2 -6
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts +3 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/atomic-project-migration.d.ts +53 -0
- package/dist/storage/atomic-project-migration.d.ts.map +1 -0
- package/dist/storage/index.d.ts +3 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +61 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/machine-registry.d.ts +36 -0
- package/dist/storage/machine-registry.d.ts.map +1 -0
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +2 -2
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage/run-artifact-creation-upload.d.ts +50 -0
- package/dist/storage/run-artifact-creation-upload.d.ts.map +1 -0
- package/dist/storage/s3-artifact-sync.d.ts +18 -0
- package/dist/storage/s3-artifact-sync.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts +2 -0
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +2695 -322
- package/dist/task-manifest/postgres.d.ts +1 -1
- package/dist/task-manifest/sqlite.d.ts +1 -1
- package/dist/task-manifest.js +244 -73
- package/dist/task-subtree-transfer.js +198 -4
- package/dist/testing.d.ts +90 -29
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +34 -10
- package/dist/types/index.d.ts +40 -4
- package/dist/types/index.d.ts.map +1 -1
- package/docs/PLAN_API.md +93 -0
- package/docs/TASK_LIST_API.md +58 -0
- package/docs/TASK_QUERY_API.md +14 -0
- package/docs/TEMPLATE_API.md +18 -0
- package/docs/native-storage.md +405 -0
- package/package.json +19 -16
- package/postinstall.js +52 -1
- package/dashboard/dist/assets/index-BNQ4gJua.js +0 -342
- package/dashboard/dist/assets/index-DjzvHUWt.css +0 -1
- package/dashboard/dist/index.html +0 -13
- 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
|