# @hasna/todos

Universal task management for AI coding agents - CLI + MCP server + interactive TUI

[![npm](https://img.shields.io/npm/v/@hasna/todos)](https://www.npmjs.com/package/@hasna/todos)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)

## Install

```bash
bun install -g @hasna/todos
```

## Upgrading From 0.15.52

0.16.0 puts every surface on one credential resolver and moves three command
families to the shared API. Upgrading from 0.15.52 needs no data migration, but
four things change:

- **Configure a credential.** The `./sdk` client no longer reads
  `~/.todos/config.json`, and the unprefixed `TODOS_URL` / key spellings are
  legacy. Put the key in the macOS Keychain, in
  `~/.hasna/todos/config/credentials` (owner-only `0400`/`0600`), or in
  `HASNA_TODOS_API_KEY`, then confirm with `todos storage status --json`. With no
  credential the CLI exits non-zero (`REMOTE_API_CONFIG_MISSING`) instead of
  quietly serving local rows, and MCP tool calls fail closed rather than falling
  back to the on-box store (see the third bullet below). The
  retired locations — `~/.hasna/fleet-env`, `~/.hasna/cloud`, `~/.config/hasna`,
  `$XDG_CONFIG_HOME` — are not read.
- **`todos plans`, `todos task-lists` (`lists`, `tl`) and the template commands
  are shared-API only.** They refuse `HASNA_TODOS_DB_PATH`, `TODOS_DB_PATH`,
  `HASNA_TODOS_LOCAL` and `TODOS_LOCAL` before startup — any one of them set is
  enough to refuse the command, and the refusal names the ones that are set.
  `todos template-library` is unchanged and still credential-free. The MCP plan
  tools (`create_plan`, `list_plans`, `get_plan`, `update_plan`, `delete_plan`)
  and task-list tools (`create_task_list`, `list_task_lists`, `get_task_list`,
  `update_task_list`, `delete_task_list`) are shared-API only as well.
- **Most MCP tools that read the on-box store need `HASNA_TODOS_LOCAL=1`, and
  every refusal is typed.** The MCP server no longer opens the on-box SQLite
  store implicitly, so tools that still read it directly — the template family,
  tags and labels, stale/blocked work, `doctor`, run ledger,
  handoffs, review queues, retrospectives, risks, knowledge records, backups,
  calendar, boards, focus/time reports and dispatches — fail on the default
  posture with the typed `{"code":"API_DATABASE_FALLBACK_FORBIDDEN"}` payload,
  whose `suggestion` names the opt-in; the server also logs the same code.
  Measured at 0.16.0 with `TODOS_PROFILE=full`, that is 68 of the 125
  zero-required-argument tools. A further 18 return the typed
  `REMOTE_API_CONFIG_MISSING` — the 16 shared-API tools that need a credential
  (`list_tasks`, `list_projects`, `list_agents`, `get_next_task`, `get_status`,
  `bootstrap`, `get_context`, `get_my_tasks`, `get_my_workload`, `get_health`,
  `standup`, `list_my_tasks`, `machines_register`, `machines_list`,
  `machines_heartbeat`, `machines_topology`) plus `list_plans` and
  `list_task_lists`. Five more refuse caller input or local state with the typed
  `INVALID_INPUT` / `ENCRYPTION_KEY_UNAVAILABLE` / `ENCRYPTED_PAYLOAD_INVALID`,
  and five answer a readable text refusal ("Provide agent_id, id, or name.").
  No zero-argument tool returns an opaque `UNKNOWN_ERROR` on either posture —
  the default one measured above, or the `HASNA_TODOS_LOCAL=1` opt-in these
  tools are served under. Run the server with
  `HASNA_TODOS_LOCAL=1` to keep using the on-box tools; that opt-in is ignored
  when `HASNA_TODOS_API_KEY` or `HASNA_TODOS_API_URL` is set, because a
  configured environment outranks it.
- **The advertised command list is route-dependent.** 0.15.52 listed all 166
  commands to every caller because the local fallback was implicit. 0.16.0
  lists only what the resolved route exposes: 76 root commands on the hosted
  route once a credential resolves, 75 with none configured (the extra verb,
  `stale-lock-handoff`, is advertised only when the shared-API authority is),
  and the full 166 only with `HASNA_TODOS_LOCAL=1` set, because the on-box
  families fail closed on the default posture. No command was removed —
  `todos --help`, `todos manual` and
  the generated completions all honour the same rule, and every verb still
  resolves once its posture is configured. `todos storage status` also reports
  credential-source disagreements as a `warnings` array (`--json`) and a yellow
  stderr line (human mode); the entries name sources and env key names, never a
  value.

Everything else still runs offline with `HASNA_TODOS_LOCAL=1` (when the
environment configures no authority or credential of its own). The per-surface
detail lives under `apps/todos/docs/` (`PLAN_API.md`,
`TASK_LIST_API.md`, `TEMPLATE_API.md`, `TASK_QUERY_API.md`,
`native-storage.md`). The npm tarball ships this README, `CHANGELOG.md` and those
five documents alongside `dist/`, so they are readable from an installed
package; `docs/cli-help.md` is repo-only.

## HTTP request budget

`todos-serve` defaults to **12,000 requests per minute per network peer**, per
server process. Set `HASNA_TODOS_RATE_LIMIT_MAX` to an integer from `1` to
`1000000` to choose a different finite budget. The legacy `TODOS_RATE_LIMIT_MAX`
variable still works; the canonical variable takes precedence when both are set.
An absent or blank value uses the default. Invalid values fail startup before
the database is opened or the server listens.

The fixed one-minute window covers all routes, including health checks and
unauthenticated requests, except CORS `OPTIONS`. Exceeding the budget returns
HTTP `429` with `Retry-After` in seconds. Authentication requirements stay the
same. This setting is a request allowance, not a throughput guarantee; size a
deployment using observed latency, database load, and its gateway limits.

Behind a reverse proxy, callers share the proxy's bucket unless the operator
has explicitly configured trusted forwarding. Keep proxy trust off for direct
traffic. Only enable `TODOS_TRUST_PROXY=1` when network rules restrict access to
proxies that overwrite forwarding headers; `TODOS_TRUSTED_PROXIES` helps resolve
the forwarded chain and does not itself restrict which sockets may connect.

## Credentials and Service Authority

The CLI, the MCP server and the `./sdk` client all resolve their credential
through one resolver — the client chain in `@hasna/contracts` — so there is
exactly one place to put a key and exactly one precedence to learn. Resolution
happens **on every call**, never once at process start, so rotating a key heals
a long-running shell, MCP server or agent without restarting it: a `TodosClient`
built at startup and held for hours re-reads the Keychain and the credentials
file on each request and picks up the new key on the next one.

Two things are deliberately *not* re-resolved. An explicit `apiKey` argument is
tier 1 — a pin the caller owns — and the **service authority is fixed for the
life of a client**, because a credential written for one authority must never be
sent to another; build a new client to point somewhere else.

That second rule binds the credential too. `new TodosClient({ baseUrl })` with
no `apiKey` pins the authority *and* sends no credential at all: the key in your
Keychain or `~/.hasna/todos/config/credentials` was written for the fleet, so it
is never re-resolved onto a URL you named — a local `todos-serve`, a staging
box, a test double. Pass `apiKey` alongside `baseUrl` when that authority is
supposed to get a credential. Per-call re-resolution applies to a client that
resolved its own hosted authority, which is where rotation matters.

**Credential, highest to lowest:**

| # | Tier | Where |
| --- | --- | --- |
| 1 | explicit argument | `apiKey` on `new TodosClient({...})` — the `todos` CLI exposes no hosted-credential flag of its own (`todos ai --profile` is a runtime profile, not a credential tier) |
| 2 | deliberate env pointer | `HASNA_TODOS_API_KEY_OVERRIDE`, `HASNA_PROFILE`, `HASNA_TODOS_API_KEY_REF` (a secrets-vault item key, never a value) |
| 3 | macOS Keychain | generic password `hasna.credentials.todos.api-key`, account `HASNA_STATION` → `hostname -s` → `USER` |
| 4 | disk | `~/.hasna/todos/config/credentials`, owner-only `0400`/`0600` |
| 5 | environment | `HASNA_TODOS_API_KEY` |

Tier 3 is account-scoped, not host-scoped: the item is looked up under the
station name, so a key stored for one station is invisible on another. Set
`HASNA_STATION` explicitly (a wrapper, a launchd plist, a CI job) when the
account name is not the first of `hostname -s` / `USER`; with none of the three
matching the item's account, the lookup misses and the run fails closed with
`REMOTE_API_CONFIG_MISSING` rather than silently resolving something else.

Tiers 1 and 2 are *deliberate*: if one is set and cannot be honoured, the run
fails — it never quietly authenticates as a different principal. Tier 5 is a
first-class tier, not a deprecated one (a wrapper that injects the key per
process is correct), and it sits **below** disk so a rotated on-disk key beats a
stale `export` in an old shell. Nothing is read from `~/.hasna/fleet-env`,
`~/.hasna/cloud`, `~/.config/hasna`, `$XDG_CONFIG_HOME`, or `~/.todos/config.json`.

**Service authority**, same shape:

`HASNA_TODOS_API_URL` → the Keychain `hasna.credentials.todos.api-url` item →
`~/.hasna/todos/config/credentials` → the fleet gateway
`https://api.hasna.com/todos`. Configure the URL only to point somewhere else;
the client appends `/v1` itself, and sources that disagree are refused rather
than silently preferred. `HASNA_HOME` and `HASNA_CONFIG_HOME` move the disk
root; the older unprefixed spellings of these two variables (without the
`HASNA_` prefix) still work as a silent fallback for one release, but the
`HASNA_TODOS_*` names are canonical and are what every diagnostic names.

The credentials file is a plain env file, and its permissions are enforced — a
group- or world-readable one is refused rather than read:

```bash
install -m 700 -d ~/.hasna/todos/config
printf 'HASNA_TODOS_API_KEY=%s\n' "$KEY" > ~/.hasna/todos/config/credentials
chmod 600 ~/.hasna/todos/config/credentials
```

**Hosted mode fails closed.** With no credential the CLI exits non-zero
(`REMOTE_API_CONFIG_MISSING`) and names the tiers it consulted; the stdio MCP
server exits the same way before answering `initialize`. Once a hosted
authority resolves, each call fails instead of serving local rows — the
credential-gated tools with the typed `REMOTE_API_CONFIG_MISSING` and the
on-box tools with the typed `REMOTE_COMMAND_UNSUPPORTED` (or
`API_DATABASE_FALLBACK_FORBIDDEN` on surfaces without a startup refusal), both
described under
[Upgrading From 0.15.52](#upgrading-from-01552). Neither surface falls back to
the local SQLite store, because serving local rows while authentication is broken
prints healthy output for a broken system.

The `./sdk` surface fails closed the same way: with nothing resolved,
`new TodosClient()` and `createTodosV1Client()` both throw
`TODOS_CREDENTIAL_MISSING` naming every tier they consulted. `TodosClient` can
speak to the on-box `todos-serve` at `http://localhost:19427` — that is a real
mode for it — but only under the explicit opt-in below, never as a fallback.
Every other refusal is a throw on all three surfaces too — aliases that
disagree, an unreadable credential file, a URL with no key — because those are
misconfigurations, not an absence of configuration.

A **declared-but-blank** authority variable is deliberately *not* one of those
refusals. At the Todos seam a blank has always meant "unset" — helpers in the
wild blank rather than delete — so every authority variable that is declared
but empty is removed before the resolver runs. `HASNA_TODOS_API_KEY=` therefore
resolves the machine's ambient Keychain item exactly as an unset variable does:
it neither configures a credential nor withholds one. To force the on-box store
from a wrapper, set `HASNA_TODOS_LOCAL=1` — a blank authority variable counts as
absent for the opt-in too, so the opt-in is still honoured when
`HASNA_TODOS_API_KEY` is present but empty — instead of blanking a credential
variable.

On the hosted route the MCP server never opens the local store either: the
tools and `todos://` resources that only exist for the on-box SQLite file
(dispatch, templates, handoffs, boards, runs, `todos://projects`, …) answer
`REMOTE_COMMAND_UNSUPPORTED` naming the opt-in, instead of reading an empty
local file and reporting it as the fleet. Machine and task tools are
shared-API routed on that route and refuse under their own `REMOTE_API_*`
code when the authority cannot serve them.

**Local mode is deliberate, and it says so.** `@hasna/todos` is usable entirely
offline against an on-box SQLite store — set `HASNA_TODOS_LOCAL=1` (alias
`TODOS_LOCAL=1`). It is honoured only when the environment configures no
authority and no credential, it is answered *before* the resolver runs (so no
Keychain item and no credential file is read), and every local run prints one
line on stderr saying it is local.

Three command families are exempt from local mode and are shared-API only:
`todos plans`, `todos task-lists` (aliases `lists`, `tl`) and the template
commands (`templates`, `template-init`, `template-preview`, `template-export`,
`template-import`, `template-history`). They refuse `HASNA_TODOS_DB_PATH`,
`TODOS_DB_PATH`, `HASNA_TODOS_LOCAL` and `TODOS_LOCAL` before startup and need a
credential. `todos template-library` is the exception on the other side: it
renders the library bundled in the package and stays credential-free.

```bash
todos storage status --json    # which tier supplied the key, and which URL applied
```

## CLI Usage

```bash
todos --help
```

Generate shell completions directly from the registered CLI command tree:

```bash
todos completions bash > ~/.local/share/bash-completion/completions/todos
todos completions zsh > ~/.zsh/completions/_todos
todos completions fish > ~/.config/fish/completions/todos.fish
```

`--help`, `manual` and the completions all derive from the same tree, and that
tree is filtered by the resolved route: the default posture advertises the
commands that can run against the hosted surface, and `HASNA_TODOS_LOCAL=1`
advertises the full on-box set. See
[Upgrading From 0.15.52](#upgrading-from-01552) and
`apps/todos/docs/cli-help.md`.

Print the local CLI manual when you need install/update commands, examples,
JSON output contracts, error behavior, and the command catalog:

```bash
todos manual
todos manual --json
```

Deterministic loops can upsert a task by stable fingerprint without creating
duplicates. The fingerprint is stored as `metadata.fingerprint`, and later
upserts shallow-merge metadata so expectation fields can be refreshed safely:

```bash
todos --json task upsert \
  --fingerprint "loop:expectation:key" \
  --title "Expectation failed" \
  --metadata-json '{"expectation_id":"exp-1"}' \
  --evidence-paths "logs/loop.txt" \
  --expected '{"status":"ok"}' \
  --observed '{"status":"failed"}'
```

### Current-state summaries

A row's title and description are what an agent reads first, while the decision
that changes them usually lands in a comment. `todos realign` applies an
operator-written current state to the row under an exact version guard, anchored
to the comments that carry the decisions:

```bash
todos realign L8-00010 \
  --title "SOU HASNA — recovery approved; $700.00 wire received and credited" \
  --state "Owner approved the recovery path; the $700.00 wire was received and is a confirmed credit. Nothing is filed or paid before the final invoice." \
  --owner legal \
  --blocker "RL final invoice" \
  --next "Wait for the final invoice" \
  --source comment:d9af9e97-8539-4919-bb5b-767152986613 \
  --source comment:83c77867-86f2-4bc9-b579-3d5f4e1cc853 \
  --receipt-comment
```

It never summarises. At least one `--source comment:<uuid>` naming a comment on
that row is required, the comment id is validated against the row's comments,
and the stored text is exactly what the caller supplied. The write carries the
version that was read (or `--expect-version`), so a concurrent writer is refused
rather than clobbered; an ambiguous transport outcome is settled by reading the
row back and the receipt says which of applied / not applied / unknown it is;
comments are never touched, so retractions stay in history. The previous summary
is retained in the row's change log where the authority records description
changes (`todos history`) and, with `--receipt-comment`, verbatim in a comment on
any authority. `--dry-run` composes and validates without writing.

## Terminal Dashboard

`todos dashboard` launches a local Ink TUI with keyboard tabs for overview,
projects, tasks, plans, runs, dependencies, inbox, and search. It reads only the
local SQLite database and can also print deterministic snapshots for scripts or
tests:

```bash
todos dashboard
todos dashboard --snapshot --view tasks --search "release" --json
```

Keyboard hints are shown in the interface: `h`/left and `l`/right move between
tabs, `1`-`8` jumps to a tab, `/` opens local search, `r` refreshes, and `q`
quits.

## Local Project Bootstrap

Bootstrap discovers the current local workspace, registers a project identity,
creates the default task list, records local source metadata, and works for
monorepo package roots without contacting hosted services:

```bash
todos project-bootstrap .
todos project-bootstrap packages/cli --name todos-cli --task-list todos-cli
todos project-bootstrap . --dry-run --json
```

MCP clients can use `bootstrap_project` for the same local-only workflow. The
command is idempotent, so running it again refreshes machine-local paths without
duplicating projects, task lists, or source records.

## Local Machine Topology

Machine registry state stays in local SQLite. Machines can record identity,
last-seen heartbeats, workspace paths, git roots, and user-provided Tailscale
or LAN addresses without probing the network:

```bash
todos machines register my-machine --ssh user@my-machine --tailscale-name my-machine.tailnet --tailscale-ip 100.100.100.100 --lan-address 192.0.2.10 --workspace ~/workspace
todos machines heartbeat my-machine --workspace ~/workspace
todos machines topology --json
todos machines sync --machine my-machine --dry-run --json
todos machines sync --machine my-machine --push
todos machines sync --ssh user@my-machine --dry-run
todos projects-path set <project-id> ~/workspace/my-project
```

`todos machines topology` reports stale machines, missing local path overrides,
missing local paths, and projects whose machine-local paths differ across
registered machines. MCP clients can use `machines_register`,
`machines_heartbeat`, `machines_topology`, and `machines_list` for the same
offline diagnostics.

`todos machines sync` exchanges the same local bridge bundles used by
`todos export --format bridge` over SSH. Pulls import remote projects, task
lists, plans, tasks, comments, run evidence, boards, calendar items, and stored
artifact contents with dry-run-first reporting and safe conflict recording.
`--push` sends the local bundle back to the peer and asks the peer's installed
`todos bridge-import` to apply or preview it. No hosted service is contacted;
use `--ssh` for a one-off bootstrap peer before it has a registered machine
address.

## Local Workspace Trust

Workspace trust profiles live in `~/.hasna/todos/config.json` and keep agent
permissions local. Profiles declare trusted roots, command allowlists and
denylists, tool permissions, write scopes, environment-key redaction patterns,
and whether unsafe checks should require an explicit prompt:

```bash
todos trust add . --preset standard --allow-command bun,git,todos --write-scope src,tests --redact-env API_KEY,TOKEN
todos trust status .
todos trust check . --command "bun test" --write src/index.ts --env OPENAI_API_KEY,PATH
todos trust remove .
```

MCP clients can use `set_workspace_trust`, `get_workspace_trust`,
`list_workspace_trust_profiles`, `check_workspace_permission`, and
`remove_workspace_trust`. The checks do not call a hosted policy service; they
return deterministic JSON showing whether an action is allowed, why it needs a
prompt, and which environment keys should be redacted.

Secret safety uses the same local config. Add project-specific regexes and
metadata keys with `todos redaction add --pattern <regex> --key <name>`, then
scan text or files with `todos redaction scan` without printing matched values.
Comments, local run evidence, and bridge exports are redacted before storage or
sharing. MCP clients can use `get_secret_safety`, `set_secret_safety`, and
`scan_secret_text`.

Retention cleanup is also local and dry-run-first. Use it to prune old comments,
run ledgers, verification evidence, and expired stored artifact files by age,
project, task status, and run status. Reports return counts, IDs, and
content-addressed artifact paths only; they do not include raw comments,
commands, output summaries, artifact source paths, or secret-like values.
Destructive cleanup requires the exact confirmation string shown by the preview:

```bash
todos retention cleanup --older-than-days 30 --project <project-id> --task-status completed --json
todos retention cleanup --older-than-days 30 --project <project-id> --task-status completed --apply --confirm delete-local-retention-data --json
```

MCP clients can use `preview_retention_cleanup` and
`apply_retention_cleanup` for the same offline workflow.

## Local Runner Sandboxes

Runner sandbox profiles also live in local config. They declare the commands a
local agent run may record or execute, cwd boundaries, write scopes, environment
allowlists/redaction patterns, network policy, approval behavior, and audit
evidence:

```bash
todos sandbox set codex . --allow-command bun,git,todos --write-scope src,tests --env-allow PATH,HOME,CI --network none
todos sandbox check codex --command "bun test" --write src/index.ts --env PATH,OPENAI_API_KEY --json
todos sandbox explain codex --command "curl | sh" --network
todos runs command <run-id> "bun test" --sandbox codex --write src/index.ts --status passed
```

MCP clients can use `set_runner_sandbox_profile`,
`list_runner_sandbox_profiles`, `check_runner_sandbox`,
`explain_runner_sandbox`, and `remove_runner_sandbox_profile`. Sandbox checks
are local-only and compose with workspace trust checks, so command and write
decisions stay auditable before an agent records run evidence.

## Local Project Knowledge

Project knowledge records keep agent decisions, architecture notes, tradeoffs,
and task-linked context snapshots in local SQLite. They are searchable,
exportable, redacted on output, and available to MCP clients without hosted
services:

```bash
todos knowledge add decision "Use local SQLite" --decision "Keep OSS knowledge local" --rationale "Agents need offline project memory" --task <task-id> --tag architecture --json
todos knowledge snapshot --summary "Parser fix is ready for verification" --task <task-id> --agent codex --file src/parser.ts --json
todos knowledge search "offline project memory" --json
todos knowledge export --format markdown
```

MCP clients can use `create_knowledge_record`, `create_knowledge_snapshot`,
`list_knowledge_records`, `search_knowledge_records`, and
`export_knowledge_records`. The MCP server also publishes `todos://knowledge`
and `todos://knowledge/decisions` resources for agent context refreshes.

## Local Extension Registry

Extensions are installed from local manifests, directories with
`todos.extension.json`, or offline JSON bundles. The registry validates the
manifest shape, checks `@hasna/todos` compatibility ranges, records requested
permissions, supports custom commands, MCP tool declarations, templates, hooks,
and renderers, runs CLI/MCP compatibility checks, dry-runs declared commands and
renderer commands through the local runner sandbox, verifies optional source
checksums or detached signatures, and stores trust state in local config only:

```bash
todos extensions discover . --json
todos extensions inspect ./todos.extension.json --json
todos extensions compat ./todos.extension.json --json
todos extensions install ./todos.extension.json --checksum sha256:... --trust --json
todos extensions verify ./bundle.todos-extension.json --signature <signature> --public-key "$PUBLIC_KEY"
todos extensions list
todos extensions remove my-extension
```

Unsigned extensions are allowed but installed as local records with warnings.
Without `--trust`, installs remain in `needs_review` so agents can discover
custom commands, MCP tools, hooks, and permissions without treating them as
approved. MCP clients can use `inspect_local_extension`,
`test_local_extension_compatibility`, `install_local_extension`,
`list_local_extensions`, and `remove_local_extension` for the same offline
workflow.

## Local Workflow Prompts

The package includes bundled MCP prompts for common agent workflows:
`goal_planning`, `task_claiming`, `review`, `verification`, `handoff`,
`release_prep`, `import_triage`, and `incident_response`. They are static,
local-only prompt resources that can be listed or rendered without a model call:

```bash
todos workflows list
todos workflows show goal_planning --objective "Ship release" --task 1234abcd --json
todos workflows export --format markdown
```

MCP clients can discover the same catalog at `todos://workflow-prompts` and call
the matching prompt by ID. Prompt output is deterministic and is intended for
Codex, Claude Code, Takumi, and other agent-native clients that need reusable
local guidance for planning, claiming, review, verification, handoff, release,
triage, and incident workflows.

Agent setup recipes for MCP registration, `/goal` planning, task
claim/update/complete loops, evidence comments, and no-cloud verification live
in [docs/agent-adapters.md](docs/agent-adapters.md).

## Local Policy Packs

Policy packs are project-local done gates for agents. They validate task status,
passed verification commands, prohibited commands, linked commits and pull
requests, approvals, branch names, run ledgers, artifacts, changed paths, and
minimum evidence counts from the local SQLite database and config only:

```bash
todos policies set release . \
  --required-status completed \
  --required-command "bun test,bun run typecheck" \
  --prohibited-command "npm install -g,git reset --hard" \
  --require-passed-verification \
  --require-commit \
  --require-pr \
  --require-run \
  --require-artifact
todos policies validate release <task-id> --json
todos policies explain release <task-id>
```

MCP clients can use `set_policy_pack`, `list_policy_packs`,
`validate_policy_pack`, `explain_policy_pack`, and `remove_policy_pack`.
Validation is a dry local read of recorded task evidence; it never calls a
hosted enforcement service.

## Local Source TODO Index

Source extraction scans local code for `TODO`, `FIXME`, `HACK`, `BUG`, `XXX`,
and `NOTE` comments, respects `.gitignore` plus explicit excludes, records
source files, line anchors, nearby symbols, and stable dedupe fingerprints, and
can run as a finite local watcher:

```bash
todos extract . --dry-run --index --json
todos extract . --exclude fixtures/** --tags tech-debt
todos extract-watch . --dry-run --max-runs 1 --json
```

Created tasks are tagged with `extracted` and linked back to the source file.
The source index is CLI-only: the former `extract_todos` / `watch_source_todos`
MCP tools were removed (a filesystem watcher is not an MCP tool call, and the
scan runs against the local checkout, not the hosted fleet). No hosted code
search, hosted sync, or telemetry is used.

## Local Editor Integrations

Editor recipes live in `docs/editor-integrations.md` and
`examples/editor-integrations/`. They include VS Code task definitions,
JetBrains external tool recipes, Neovim Lua helpers, a shell statusline snippet,
and a Bun task picker. Every example uses only `todos` CLI JSON output or MCP
tool names, so editors can claim tasks, inspect local queues, build context
packs, and link source files without importing private modules or hosted code.

## Task Contracts and Reviews

Task contracts make acceptance criteria, required verification, expected
artifacts, relevant files, risk, and review state machine-readable for agents:

```bash
todos contracts set <task-id> \
  --criteria "Parser handles quotes;Parser rejects malformed checkboxes" \
  --verify "bun test src/parser.test.ts" \
  --artifact logs/parser.txt \
  --file src/parser.ts \
  --risk medium \
  --done "review approved" \
  --json
todos contracts request-review <task-id> --requester codex --reviewer reviewer
todos record-verification <task-id> "bun test src/parser.test.ts" --status passed --artifact logs/parser.txt
todos contracts review <task-id> --state approved --reviewer reviewer
todos contracts check <task-id> --json
```

Contracts are stored in local task metadata, mirror acceptance criteria for
context packs, and are checked only against local status, review state, and
recorded verification evidence. MCP clients can use `set_task_contract`,
`get_task_contract`, `request_task_review`, `record_task_review`, and
`check_task_done_contract`.

## Local Approval Gates

Approval gates are manual checkpoints stored in the local task database. Agents
can require, approve, reject, expire, list, and check gates before risky plan or
run work. Blocked checks exit nonzero, including JSON mode, so local automation
cannot silently bypass a missing or denied checkpoint:

```bash
todos approvals require <task-id> deploy --requester codex --reviewer reviewer --run <run-id> --reason "production-affecting action"
todos approvals check <task-id> deploy --json
todos approvals approve <task-id> deploy --reviewer reviewer --note "safe to proceed"
todos approvals list <task-id> --json
```

MCP clients can use `require_approval_gate`, `approve_approval_gate`,
`reject_approval_gate`, `expire_approval_gate`, `check_approval_gate`, and
`list_approval_gates`. Gate events are written to task audit history and, when
a run is linked, to the local run ledger.

### OpenAutomations Approval And Evidence Handoff

OpenAutomations approval gates can be mirrored to Todos approval gates when an
automation action affects external systems, spends money, mutates production
state, or needs human consent. Use the automation run id as the linked run or
metadata reference, and keep the approval decision id in task/run evidence:

```bash
todos approvals require <task-id> automation:<action-id> \
  --requester automations \
  --reviewer operator \
  --reason "OpenAutomations action requires consent"
todos approvals approve <task-id> automation:<action-id> \
  --reviewer operator \
  --note "Approved action execution"
todos record-verification <task-id> "automations queue complete <action-id>" \
  --status passed \
  --summary "Action completed by runner open-loops:<worker-id>"
```

Todos owns task approvals, task comments, verification evidence, audit-ledger
checkpoints, and release evidence. OpenAutomations owns automation specs,
action queue leases, DLQ/replay, and action approval state. Store only redacted
summaries, action ids, run ids, decision ids, artifact paths, and verification
commands in Todos; do not store raw event payloads, secret values, connector
tokens, or full action inputs unless the task explicitly requires them and they
have been redacted.

## Local Review Queues

Review queues turn local task review into an explicit agent workflow: request a
review, route it to a queue, claim it, return it with changes, reopen it, or
approve it. Routing rules live in local config and can match tags, priorities,
and projects without hosted users, orgs, or cloud services:

```bash
todos reviews rules set security --queue security-review --reviewers reviewer --tags security --priorities high
todos reviews request <task-id> --requester codex --reason "security-sensitive change"
todos reviews claim <task-id> --reviewer reviewer
todos reviews return <task-id> --reviewer reviewer --changes "Add tests;Record verification"
todos reviews approve <task-id> --reviewer reviewer
todos reviews list --queue security-review --json
```

MCP clients can use `list_review_queue`, `request_review_queue`,
`claim_review_item`, `return_review_item`, `approve_review_item`,
`reopen_review_item`, `set_review_routing_rule`, `list_review_routing_rules`,
and `remove_review_routing_rule`. Queue transitions are written to audit
history and emitted to local event hooks as `review.requested`,
`review.claimed`, `review.returned`, `review.approved`, and `review.reopened`.

## Local Roadmaps

Roadmaps group local tasks, plans, runs, milestones, and release labels into a
portable planning view. They live in local config, summarize dependency
readiness from the task graph, and export deterministic JSON or Markdown
bundles:

```bash
todos roadmaps create "Public package launch" --release v1 --json
todos roadmaps milestones add <roadmap-id> "Docs and examples" --tasks <task-id> --due 2026-06-01 --release v1 --json
todos roadmaps releases set <roadmap-id> v1 --milestones <milestone-id> --release-version 1.0.0 --json
todos roadmaps show <roadmap-id> --format markdown
todos roadmaps export <roadmap-id> --out roadmap.json
todos roadmaps import roadmap.json --apply --json
```

MCP clients can use `create_roadmap`, `list_roadmaps`,
`get_roadmap_summary`, `update_roadmap`, `delete_roadmap`,
`create_milestone`, `update_milestone`, `delete_milestone`,
`set_release_group`, `export_roadmap`, and `import_roadmap`.

## Local Event Hooks

Event hooks are local subscriptions for task, plan, run, approval, import, and
export events. They can append redacted JSONL to a file, deliver to a Unix
socket, expose a stdout test payload, or run a sandbox-checked local script with
retry/backoff and SHA-256 integrity metadata:

```bash
todos event-hooks set audit --event task.completed,run.failed --target file --file .todos/events.jsonl
todos event-hooks set notify --event task.blocked --target script --command "notify-send \"$TODOS_EVENT_TYPE\"" --sandbox codex --attempts 2
todos event-hooks test audit --event task.completed --payload '{"id":"demo"}' --json
todos event-hooks list --json
```

MCP clients can use `set_local_event_hook`, `list_local_event_hooks`,
`test_local_event_hook`, and `remove_local_event_hook`. Hook delivery is
local-only; it does not call hosted webhooks or cloud automation services.

## Shared Task Events And Route State

Task lifecycle changes emit shared `@hasna/events` events with source `todos`.
Todos is the human-visible task ledger: it emits task intent, routing gates, and
workflow artifact pointers. OpenEvents owns durable channel delivery/replay, and
OpenLoops owns workflow admission, work-item leases, agent execution, evaluator
cycles, and run manifests.

Use `project-bootstrap --route-enabled` to opt a project task list into
task-created routing, then inspect route state with deterministic JSON before a
route or workflow starts:

```bash
todos project-bootstrap . --route-enabled --json
todos ready --json
todos task route-state <task-id> --json
```

`todos ready --json` includes a `route_state` object for each pending unblocked
task. `route_state.schema_version` is `todos.task_route_state.v1`. It exposes
`eligible`, denial `reasons`, dependency blockers, route context, boolean gates,
and compact OpenLoops pointers:

- `current_workflow_invocation_id`
- `current_run_id`
- `latest_manifest_path`
- `latest_evaluation_path`
- `workflow_state`

OpenLoops updates those pointers after admission or evaluator progress:

```bash
todos task workflow-pointers <task-id> \
  --invocation <workflow-invocation-id> \
  --run <workflow-run-id> \
  --manifest ~/.hasna/loops/runs/<project>/<subject>/<run>/manifest.json \
  --state working \
  --json
```

Shared task event data includes task identity, title, description, project/list
ids, working directory, tags, metadata, status, priority, approval state, and
timestamps. Event metadata includes routing-safe project/list/path fields,
`route_enabled` when task or task-list metadata opts in, route-state schema
version, compact workflow pointers, and an `automation` object containing only
boolean routing gates such as `no_auto`, `manual_required`,
`requires_approval`, and `approval_required`.

When running route smokes or tests with an explicit temp/in-memory task database
through `TODOS_DB_PATH` or `HASNA_TODOS_DB_PATH`, also set `HASNA_EVENTS_DIR` or
`HASNA_EVENTS_HOME` to a temp event store. Without an explicit event directory,
Todos suppresses shared event emission for temp task databases so route-enabled
scratch tasks cannot trigger global OpenLoops workers. Set
`HASNA_TODOS_ALLOW_GLOBAL_EVENTS_FROM_TEMP_DB=1` only for deliberate integration
tests that must exercise the global event bus.

Production task-created routes should fail closed:

- Require one explicit opt-in, either task metadata `route_enabled=true` or an
  approved routing tag such as `auto:route`.
- Add negative automation predicates so `no_auto`, manual, and approval-gated
  tasks do not invoke the route.
- Scope by project path, task list, tags, or repo metadata before invoking an
  OpenLoops route.
- Avoid overlapping opt-in channels for the same task family unless the target
  handler is idempotent. OpenLoops task-event admission dedupes by task id and
  event type, but a narrower subscription still avoids wasted invocations.

For tag opt-in, use a second route with the same deny predicates and
`tags=auto:route` instead of `route_enabled=true`.
Tasks without one of those opt-ins are intentionally no-route. Local event hooks
remain available for local-only JSONL/socket/script integrations.

## Local Terminal Notifications

Terminal notification rules are local watch rules for agents that want concise
event signals in a shell, tmux pane, or editor terminal. Rules match task, run,
plan, approval, import, and export events by severity, agent, project, priority,
status, and payload text, then render deterministic line or JSON notifications:

```bash
todos terminal-notifications set blocked --event task.blocked,task.failed --min-severity warning --agent codex --priority high --contains deploy --bell
todos terminal-notifications set due --event task.due,task.sla_breached --min-severity warning --quiet-hours 22:00-07:00
todos notifications check --emit-hooks --terminal --quiet-hours 22:00-07:00 --json
todos terminal-notifications test blocked --event task.failed --payload '{"id":"demo","title":"Deploy failed","agent_id":"codex","priority":"high"}' --json
todos terminal-notifications list --json
```

MCP clients can use `set_terminal_notification_rule`,
`list_terminal_notification_rules`, `test_terminal_notification_rule`,
`evaluate_terminal_watch_rules`, `check_local_notifications`, and
`remove_terminal_notification_rule`. Notifications are evaluated from local
event payloads, can respect quiet hours, and do not require a desktop
notification daemon, hosted queue, or cloud webhook service.

## Local Encryption Profiles

Encryption profiles are optional local config entries for sensitive fields and
secure bridge exports. Profiles store algorithm metadata, a nonsecret salt, and
the name of the environment variable that contains key material. The key itself
is never written to config, bundles, artifacts, or logs:

```bash
export TODOS_ENCRYPTION_KEY="use a strong local passphrase from your secret manager"
todos encryption set default --key-env TODOS_ENCRYPTION_KEY
todos encryption status default --json
todos encryption test default --json
todos export --format bridge --encrypt --output todos-bridge.enc.json
todos bridge-import todos-bridge.enc.json --decrypt --json
todos bridge-import todos-bridge.enc.json --decrypt --apply
```

Plain bridge exports are still supported for compatibility, but the CLI prints
a warning because bridge bundles may contain task metadata, evidence summaries,
comments, and stored artifact content. MCP clients can use
`set_encryption_profile`, `list_encryption_profiles`,
`get_encryption_status`, `encrypt_local_value`, `decrypt_local_value`, and
`remove_encryption_profile` for local-only encrypted field workflows.

## Local Agent Run Queue

Agent run adapters and queue entries are local. Queueing a task creates a run
ledger immediately, then `run-next` launches the configured command template
with `{task_id}`, `{run_id}`, and `{agent_id}` placeholders. Dry-runs show the
command without execution, and cancellation/retry are recorded in the same local
run ledger:

```bash
todos agent-runs adapter-set codex --command "codex exec --task {task_id}" --sandbox codex
todos agent-runs queue <task-id> --adapter codex --agent codex --claim --json
todos agent-runs run-next --dry-run --json
todos agent-runs run-next --json
todos agent-runs retry <run-id>
```

MCP clients can use `set_agent_run_adapter`, `queue_agent_run`,
`list_agent_run_queue`, `run_next_agent_dispatch`,
`cancel_agent_run_dispatch`, and `retry_agent_run_dispatch`. These commands
launch only local processes and do not call hosted runners.

## Local Agent Replay Simulation

Replay simulation turns a recorded context pack or run fixture into a
deterministic dry-run snapshot without opening the project database or mutating
tasks. Use it to debug agent plans, verification commands, task transitions,
failures, touched files, artifacts, and approval decisions offline:

```bash
todos context-pack <task-id> --format json > replay.json
todos runs simulate replay.json --agent codex --scenario parser-failure --json
todos runs simulate replay.json --format markdown
```

MCP clients can use `simulate_agent_replay` with a fixture object and optional
`agent_id` or `scenario`. The simulator redacts fixture values before hashing
or rendering, reports `mutates_database: false`, and emits stable command,
approval, failure, file, artifact, and warning summaries for local debugging.

## Local Dependency Workflows

Dependencies are stored in the local SQLite database and never require hosted
services. Use them to keep agents from starting blocked work:

```bash
todos deps <task-id> --needs <blocking-task-id>
todos deps <task-id> --graph
todos deps <task-id> --json            # machine-readable edges (ids + status) for this task
todos deps --project <ref> --json      # the whole-project graph (nodes + edges + cycles) in one read
todos blocked
todos ready
```

`--json` returns versioned, machine-readable shapes for schedulers:
`todos.task_dependency_edges.v1` for a single task (`dependencies` = upstream
prerequisites, `blocked_by` = the incomplete prerequisites currently blocking
the task — empty means dispatchable, `blocks` = downstream dependents) and
`todos.project_dependency_graph.v1` for `--project` (a `nodes`/`edges`/`cycles`
adjacency list). Both are identical on the sqlite and http transports. Human output
and `--graph --json` are unchanged.

The same workflow is available to MCP clients through
`add_task_dependency`, `remove_task_dependency`, `get_task_dependencies`, and
`get_blocked_tasks`. Dependency writes reject cycles, `ready` omits locked or
blocked pending tasks, and startup schema repair recreates the local dependency
table for older databases.

## Local Risk Register And Health

Risks are stored in local SQLite and can be linked to projects, plans, or tasks
with an owner, mitigation, due date, severity, probability, tags, and metadata:

```bash
todos risks add "Release blocker" --plan 1234abcd --severity high --owner codex --mitigation "Ship fallback" --json
todos risks list --plan 1234abcd --json
todos risks score --plan 1234abcd --json
todos risks export --project my-project --json
```

Health reports score a plan or project from local evidence only: blocked tasks,
overdue open work, failed verification records, failed run ledgers, dependency
depth, and open risks. MCP clients get the same surface through `create_risk`,
`list_risks`, `update_risk`, `close_risk`, `score_plan_health`,
`score_project_health`, and `export_risk_register`.

## Local Retrospectives

Retrospectives summarize a project or plan using local evidence: completed
plans, missed estimates, repeated blockers, failed verification records,
lessons learned, and suggested follow-up tasks.

```bash
todos retrospectives create --plan 1234abcd --json
todos retrospectives list --project my-project --json
todos retrospectives export --plan 1234abcd --format markdown
```

Use `--create-followups` to create the suggested follow-up tasks locally. MCP
clients get the same reports through `create_retrospective`,
`list_retrospectives`, and `export_retrospectives`.

## Local Agent Reliability Scorecards

Reliability scorecards summarize each agent from local evidence only: completed
and failed tasks, passed and failed verification records, failed run ledgers,
stale task/resource locks, retry history, and handoff quality.

```bash
todos reliability show codex --json
todos reliability list --project my-project --json
todos reliability export --format markdown
```

MCP clients get the same summaries through `get_agent_reliability_scorecard`,
`export_agent_reliability_scorecards`, and the `todos://agents/reliability`
resource.

## Local Agent Reports

Agent reports compose the local planning surfaces into one report for standups,
handoffs, and agent run planning: ready tasks, blocked tasks, overdue work,
plan progress, run outcomes, verification evidence, and per-agent summaries.

```bash
todos reports local --agent codex --format markdown
todos reports local --project <project-id> --json
```

MCP clients can list sections with `list_local_report_types` and build the
same local-only `local_report` contract with `build_local_report`. The report
uses the local SQLite store only and does not call hosted analytics or external
services.

## Local Agent Locking

Task claims and locks are local SQLite leases. Agents can claim the next ready
task, renew their lock by re-locking it during long work, inspect stale work,
and safely steal or redistribute stale tasks without hosted coordination:

```bash
todos claim codex
todos --agent codex lock <task-id>
todos stale --minutes 30
todos claim codex --steal-stale --stale-minutes 30
todos redistribute codex --max-age 60
```

MCP clients get the same local coordination through `claim_next_task`,
`lock_task`, `unlock_task`, `check_task_lock`, and `get_stale_tasks`.
`claim_next_task` can opt into stale recovery with `steal_stale` and
`stale_minutes`.

`get_stale_tasks` (and the other MCP tools that read the on-box store) require
the deliberate local opt-in: start the MCP server with `HASNA_TODOS_LOCAL=1`, or
the call fails on the default posture with the typed
`API_DATABASE_FALLBACK_FORBIDDEN` refusal. See
[Upgrading From 0.15.52](#upgrading-from-01552) for the affected families and
the one case where the opt-in is ignored.

## Plan Templates

The package ships a marketplace-free template library for bug fixes, feature
implementation, security review, releases, migrations, incidents, docs
refreshes, QA, and open source package bootstraps. Templates can create one
task or a full ordered plan with dependencies, variables, priorities, tags, and
descriptions.

Two surfaces are involved, and they are not the same store:

- `todos template-library` renders the library bundled in the package. It is
  credential-free and never opens a database, so it works offline:

  ```bash
  todos template-library --json
  todos template-library --write .todos/templates
  ```

- The remaining template commands are served by the authenticated shared API.
  `todos template-init`, `todos template-preview`, `todos templates --use`,
  `todos template-export`, `todos template-import` and `todos template-history`
  require `HASNA_TODOS_API_URL` and `HASNA_TODOS_API_KEY` (or saved account
  credentials) and refuse an on-box database selection before startup:

  ```bash
  todos template-init
  todos template-preview <template-id> --var name=api
  todos templates --use <template-id> --var name=api
  todos template-export <template-id> > plan-template.json
  todos template-import plan-template.json
  ```

`todos template-library --write` writes editable JSON files that use the same
shape as `todos template-import`, so teams can fork a built-in workflow without
contacting any hosted marketplace. `todos templates --use` creates every task in
a multi-task template and wires its dependency graph in the shared account, so
agents can immediately run `todos ready`, `todos blocked`, or
`todos deps <task-id> --graph` against the generated plan. MCP clients reach the
bundled library through `list_template_library` and `write_template_library`,
which are credential-free and open no store. The remaining MCP template tools —
`init_templates`, `create_template`, `list_templates`,
`create_task_from_template`, `preview_template`, `export_template`, and
`import_template` — read the on-box SQLite store and are not served on the
default posture: start the MCP server with `HASNA_TODOS_LOCAL=1` to use them.
That opt-in is ignored when the environment sets `HASNA_TODOS_API_KEY` or
`HASNA_TODOS_API_URL`, which leaves these tools unreachable (see
[Upgrading From 0.15.52](#upgrading-from-01552)).

## Moving Tasks Between Plans

Plan assignment is editable after creation, so reorganizing a backlog never
requires deleting and recreating tasks (which would lose history, comments, and
assignments):

```bash
todos add "My task" --plan <plan-id>          # assign at creation
todos update <task-id> --plan <plan-id>       # move one task to another plan
todos update <task-id> --clear-plan           # detach from its plan
todos bulk plan --plan <plan-id> <id> <id>    # move many tasks at once
todos bulk move-plan --clear-plan <id> <id>   # detach many tasks at once
todos bulk tag <id> <id> --tag a,b            # add tags to many tasks (merges; never replaces)
todos bulk untag <id> <id> --tag a            # remove tags from many tasks
todos plans --show <plan-id>                  # verify the plan's task set
```

Plan references accept a UUID, a plan slug, a plan name, or a unique id prefix.
An unknown or ambiguous reference exits non-zero before any task is modified.
`todos add`, `todos update` and `todos bulk` follow whichever store the CLI is
configured for — the local SQLite file, or the shared dataset behind a hosted
`/v1` authority. `todos plans` does not: it is served by the authenticated
shared API only and refuses `HASNA_TODOS_DB_PATH`, `TODOS_DB_PATH`,
`HASNA_TODOS_LOCAL` and `TODOS_LOCAL` before startup.

`todos plans --artifact <id-or-slug>` and `--write-artifacts` compare local
Markdown against the shared plan record and therefore require an explicit
`--artifact-root <directory>` — a trusted local project directory the caller
chooses. Before 0.16.0 both options defaulted to the project path recorded in
local SQLite, so the older form `todos plans --artifact <id-or-slug> --json`
still appears in notes from that release; it now exits non-zero with
`--artifact-root is required`. The files stay under
`<artifact-root>/.hasna/todos/plans/<project-id>/`, and a path returned by the
API is never used.

## Local Git Traceability

Tasks can be linked to local git evidence without contacting hosted services:

```bash
todos link-commit <task-id> <sha> --message "fix parser" --files src/parser.ts
todos link-ref <task-id> task/parser-fix --type branch
todos link-ref <task-id> 42 --type pr --url https://github.com/hasna/todos/pull/42
todos record-verification <task-id> "bun test" --status passed --summary "1522 pass"
todos trace <task-id>
todos find-commit <sha-prefix>
todos find-ref <branch-or-pr>
todos blame src/parser.ts
```

MCP clients get the same local data through `link_task_to_commit`,
`find_task_by_commit`, `link_task_git_ref`, `find_tasks_by_git_ref`,
`add_task_verification`, and `get_task_traceability`, so agents can explain
which task changed a commit, branch, PR, file, or verification command.

## Local Mention Resolution

Agents can resolve task references before adding them to descriptions, plans, or
handoffs. The resolver validates local files and line anchors, scans local source
declarations for symbols, checks local git commits, branches, and fetched pull
request refs, and resolves plans, runs, tasks, and agents from the local SQLite
state:

```bash
todos references resolve file:src/index.ts:12 symbol:createTask branch:main --json
todos refs resolve plan:release run:abc123 agent:marcus --workspace .
```

The JSON output includes canonical reference keys and validated backlinks such
as `file:src/index.ts:12`, `symbol:createTask@src/index.ts:40`, `commit:<sha>`,
`plan:<id>`, and `run:<id>`. MCP clients can call `resolve_mentions` for the
same local-only report; pull request refs are validated only when present in
local git refs, and the resolver never calls hosted code search.

## Local Branch-Safe Work Plans

Before an agent starts a branch, it can ask for a local branch work plan that
checks the task or plan scope, planned files, active file conflicts, local git
status, and suggested branch/traceability commands:

```bash
todos branch-plan 1234abcd --branch task/parser-fix --path src/parser.ts --json
todos branch-plan --plan <plan-id> --branch task/release-plan --no-git-status --json
```

MCP clients can call `create_branch_work_plan` with `task_id` or `plan_id`.
The result is local-only: it does not create a branch, fetch from a remote, or
contact hosted code review services. Agents can inspect `safe_to_start`,
`conflicts`, `reasons`, and `commands` before running any git operation.

## Local Release Notes

Generate changelogs from completed local tasks and their linked plans, commits,
verification records, breaking-change notes, and migration notes:

```bash
todos release-notes --project . --format markdown
todos release-notes --tag release --since 2026-01-01T00:00:00.000Z --json
```

Tasks can add release metadata through `metadata.breaking_change`,
`metadata.breaking_changes`, `metadata.migration_note`, or
`metadata.migration_notes`. MCP clients use `generate_release_notes` for the
same deterministic JSON or Markdown output without hosted release tooling.

## Local Verification Providers

Optional provider adapters let agents standardize local verification without a
hosted dependency. Providers can classify CI logs, verify browser/screenshot
artifacts, or run explicitly configured command, script, and testbox-style
commands with retry and redacted evidence capture:

```bash
todos verify-providers set local --kind command --command "bun test" --attempts 2 --json
todos verify-providers set ci --kind ci_log --json
todos verify-providers capabilities local --json
todos verify-providers run local --task <task-id> --agent codex --json
todos verify-providers run ci --task <task-id> --log-file /tmp/ci.log --json
```

Blacksmith/testbox-style providers are inert until a local command is explicitly
configured, so the package never calls a cloud runner by default. MCP clients
use `set_verification_provider`, `list_verification_providers`,
`get_verification_provider_capabilities`, `run_verification_provider`, and
`remove_verification_provider` for the same local-only workflow.

## Local Agent Handoffs

Handoffs let one local agent leave continuation context for another without a
hosted inbox. A handoff records the session, referenced tasks, relevant files,
run ids, completed work, current blockers, and next steps. Readers can filter
for unread handoffs and acknowledge them per agent:

```bash
todos handoff --create --agent codex --session codex-42 --summary "Parser work ready for review" --tasks <task-id> --files src/parser.ts --runs <run-id> --next "Review failing fixture" --json
todos handoff --unread-for claude --json
todos handoff --read <handoff-id> --json
todos handoff --ack <handoff-id> --agent claude --json
todos handoff --recover --agent codex --session codex-42 --json
todos handoff --export <handoff-id> --output handoff.json --json
todos handoff --import handoff.json --json
todos handoff --import handoff.json --apply --json
```

MCP clients can use `create_handoff`, `list_handoffs`, `read_handoff`,
`export_handoff`, `import_handoff`, `acknowledge_handoff`,
`recover_stale_session_handoff`, and `get_latest_handoff`. Recovery handoffs
inspect local in-progress tasks, file links, and run evidence for the
agent/session and create a deterministic continuation packet; no hosted queue
or cloud service is involved. Handoff imports default to a dry-run preview;
`--apply` writes the local handoff and preserves per-agent acknowledgement
state.

## Local Run Ledger

Agent runs can record local evidence without uploading artifacts or calling a
hosted API:

```bash
RUN_ID=$(todos runs start <task-id> --agent codex --title "Parser fix" --claim --json | jq -r .id)
todos runs event "$RUN_ID" progress "writing regression tests"
todos runs command "$RUN_ID" "bun test src/parser.test.ts" --status passed --summary "14 pass"
todos runs file "$RUN_ID" src/parser.ts --status modified
todos runs artifact "$RUN_ID" logs/parser-test.txt --type log --description "focused test output" --require-file
todos runs artifact-verify "$RUN_ID"
todos runs finish "$RUN_ID" --status completed --summary "parser fixed and verified"
todos runs show "$RUN_ID"
```

Run command evidence is also mirrored into task verification evidence, file
events are linked to task file tracking, and comments can be recorded into the
task timeline. Sensitive-looking tokens, keys, passwords, and bearer values are
redacted before evidence is stored. Artifact files are copied into a local
content-addressed store beside the SQLite database, with SHA-256 integrity
metadata, redaction status, retention metadata, and metadata-only fallback when
the original path is unavailable. Use `--no-store` to record only artifact
metadata.

Loops that need bounded, idempotent JSON can use the transaction commands
instead of scripting around comments and run lookup:

```bash
todos runs begin <task-id> --key nightly:parser:42 --agent codex --title "Nightly parser loop"
RUN_ID=$(todos runs begin <task-id> --key nightly:parser:42 --agent codex --title "Nightly parser loop" --apply | jq -r .run.id)
todos findings upsert --task <task-id> --run "$RUN_ID" --fingerprint parser-timeout --title "Parser timeout" --severity high --source nightly-parser --artifact logs/parser-loop.txt --apply
todos findings resolve-missing --task <task-id> --source nightly-parser --fingerprints parser-timeout --run "$RUN_ID"
todos findings resolve-missing --task <task-id> --source nightly-parser --fingerprints parser-timeout --run "$RUN_ID" --apply
todos runs finish "$RUN_ID" --status completed --summary "nightly loop complete"
```

`runs begin` is dry-run by default and only creates a run when `--apply` is
provided. Reusing the same `--key`, `--loop-run-id`, or `--loop-id` returns the
existing compact run summary instead of creating duplicates. `runs finish` is
idempotent for already-finished runs and can resolve a run by `--key --task`.
Finding upserts are deduped by task and fingerprint, redacted before storage,
and expose only artifact paths/references by default. `findings upsert` and
`findings resolve-missing` are dry-run by default; add `--apply` to mutate local
state. MCP clients use `begin_task_run_transaction`, `finish_task_run`,
`upsert_task_finding`, `list_task_findings`, and
`resolve_missing_task_findings`; these compact loop tools are included in the
default `TODOS_PROFILE=minimal` surface.

## Local Time Tracking

Manual time logs and focus sessions stay in the local SQLite database and roll
up into `task.actual_minutes` for planning and retrospectives:

```bash
todos time log <task-id> 25 --agent codex --notes "reviewed parser"
SESSION=$(todos time start <task-id> --agent codex --idle-after 30 --json | jq -r .id)
todos time pause "$SESSION"
todos time resume "$SESSION"
todos time stop "$SESSION" --notes "implemented and tested"
todos time report --include-open --json
```

Focus sessions can be linked to tasks, plans, or run ledgers. Stopping a
completed task-linked session writes a time log with the session id and run id,
then recalculates actual minutes from all local logs. `todos time idle` and the
`get_idle_focus_prompts` MCP tool report active sessions that exceeded their
local idle threshold; no desktop notification service or hosted telemetry is
required.

## Local Capacity Forecasts

Capacity profiles give agents a local way to forecast whether a project or plan
is realistic from task estimates, actual minutes, due dates, and available
minutes per day:

```bash
todos capacity set codex --minutes-per-day 240 --days 1,2,3,4,5 --json
todos capacity forecast --plan <plan-id> --agent codex --start-date 2026-06-01 --json
todos capacity forecast --project <project-id> --format markdown
todos capacity list --json
```

Forecasts report remaining estimated minutes, logged actual minutes, forecast
work days, projected completion date, missing estimates, overdue open tasks,
and risk flags. MCP clients use `set_capacity_profile`,
`list_capacity_profiles`, `remove_capacity_profile`, and
`get_planning_forecast`.

## Local Audit Ledger

Audit ledger checkpoints hash local evidence into a deterministic chain so an
agent can seal task, run, verification, approval, and handoff records and verify
later that the local evidence still matches:

```bash
todos audit-ledger show --task <task-id> --entries --json
todos audit-ledger seal release-checkpoint --task <task-id> --json
todos audit-ledger verify release-checkpoint --json
todos audit-ledger list --json
```

The ledger stores only local checkpoint metadata in config. It does not call a
hosted service and it does not claim to prevent local deletion; it detects
changes against a previously sealed root hash. MCP clients use
`get_audit_ledger`, `seal_audit_ledger`, `list_audit_ledger_checkpoints`, and
`verify_audit_ledger`.

## Release Compatibility

Release compatibility checks give agents a local dry-run report before publish
or update work. They verify the package stays `@hasna/todos`, public, pointed at
`hasna/todos`, export-stable, migration-compatible from recent local schema
levels, and ready for Bun global install smoke tests:

```bash
todos release-compat check --json
todos release-compat check --format markdown
```

The report also includes changelog surfaces and rollback commands. MCP clients
use `check_release_compatibility` for the same `release_compatibility_report`
JSON contract.

## Local Usage Ledger

Usage reports summarize local tasks, projects, runs, commands, durations,
agent-provided token and cost metadata, and run artifact storage. Quota flags
are simulated locally so agents can check free/pro limits or project budgets
without sending data anywhere:

```bash
todos usage report --agent codex --max-tasks 1000 --max-projects 10 --json
todos usage report --project <project-id> --format markdown
```

The report is aggregate-only: raw command strings and artifact paths are not
included. MCP clients use `get_usage_ledger` for the same
`local_usage_ledger` JSON contract.

## Local Scale Hardening

Scale reports benchmark common local queries, count archive-ready terminal
tasks, check expected SQLite indexes, run integrity diagnostics, and preview
database compaction without network access:

```bash
todos scale report --older-than-days 30 --json
todos scale report --format markdown
todos scale compact --json
```

`todos scale compact --apply` runs `PRAGMA optimize` and `VACUUM` against the
local SQLite database. The default is a dry run.

## Local Activity Timeline

The timeline command gives agents one ordered, redacted view of local comments,
task history, run events, command evidence, and artifacts:

```bash
todos timeline --task <task-id> --json
todos timeline --project <project-id> --limit 50
todos timeline --run <run-id> --order asc
```

MCP clients can call `get_activity_timeline` with `entity_type`, `entity_id`,
`limit`, `offset`, `since`, and `until`. Timeline entries are derived from the
local SQLite store and local bridge exports already include the underlying
comments, runs, run evidence, files, commits, and verification records needed to
rebuild the same timeline after import.

## Local Scheduling and SLA Escalation

Tasks can carry local due dates, recurrence rules, and SLA thresholds without a
hosted scheduler. Recurring tasks spawn their next local task from the previous
scheduled due date, preserving cadence even when completion happens late:

```bash
todos add "Weekly review" --due 2026-06-01 --recurrence "every week" --sla-minutes 120 --json
todos update <task-id> --due 2026-06-08 --recurrence "every monday" --sla 90 --json
todos overdue --json
todos sla --json
todos notifications check --due-within-minutes 60 --stale-minutes 30 --terminal --json
```

`todos overdue` returns unfinished tasks past `due_at`. `todos sla` returns
unfinished tasks that are past `due_at` or whose `sla_minutes` threshold has
elapsed from `started_at` when present, otherwise `created_at`. MCP clients use
`create_task` and `update_task` with `deadline`, `recurrence_rule`, and
`sla_minutes`, and can call `get_sla_breaches` for the same local escalation
view. `todos notifications check` turns due, due-soon, SLA, stale task,
completed run, and local reminder records into redacted local alerts; it can
emit configured file/socket/script/stdout event hooks, evaluate terminal watch
rules, and suppress delivery during quiet hours without contacting an external
notification service.

## Local Task Fields

Tasks can carry local labels, severity, owner, area, and custom metadata while
keeping canonical priority on the task itself:

```bash
todos fields set <task-id> --labels bug,cli --priority high --severity s1 --owner codex --area parser --field component=parser --json
todos fields show <task-id> --json
todos fields query --labels bug,cli --severity s1 --field component=parser --json
```

Custom values are redacted before storage, labels are mirrored into task tags
for existing filters, and the metadata is included in local bridge exports.
MCP clients use `get_task_fields`, `set_task_fields`, and
`query_tasks_by_fields` for the same local-only workflow.

## Local Workflow States

Projects can define local workflow states such as review, blocked, verifying,
failed, or released while keeping storage compatible with the canonical task
statuses:

```bash
todos workflow states --json
todos workflow set <task-id> review --json
todos workflow tasks review --json
todos workflow migrate --apply --json
```

Workflow states live in local config under `workflow_states.states`. Each state
maps to a canonical `canonical_status`, can declare aliases, and can restrict
allowed transitions. The selected local state is stored in task metadata and is
included in local bridge exports. MCP clients use `list_workflow_states`,
`set_task_workflow_state`, `query_tasks_by_workflow_state`, and
`migrate_workflow_states`.

## Local Calendar And ICS

Calendar events are derived from local tasks, SLA thresholds, run ledgers, and
authored local reminders, milestones, or work blocks. Exported ICS files are
deterministic and can be redacted before sharing:

```bash
todos calendar list --from 2026-06-01T00:00:00.000Z --json
todos calendar add "Release milestone" --kind milestone --start 2026-06-01T09:00:00.000Z --json
todos calendar export --redact --out todos.ics
todos calendar import team.ics --json
```

Recurring task rules are mapped into ICS `RRULE` values when possible, and task
SLA thresholds appear as local calendar events without any Google Calendar,
hosted API, or hosted sync dependency. MCP clients use `create_calendar_item`,
`list_calendar_events`, `export_calendar_ics`, and `import_calendar_ics`.

## Local Saved Search Views

Saved views are local SQLite records for repeatable task, project, plan, run,
comment, and cross-entity searches. They can filter by query text, project,
task list, plan, task, status, priority, assignee, agent, tags, local fields,
dependency direction, and time windows:

```bash
todos views save active-cli --query parser --status pending,in_progress --tag cli --field-area parser --json
todos views list --json
todos views run active-cli --json
todos search parser --scope all --limit 50 --json
```

View output is stable JSON with `{ view, scope, filters, count, results }`.
Local bridge exports include saved views, so explicit backups and machine moves
preserve the filters without any hosted service. MCP clients use
`save_search_view`, `list_search_views`, `run_search_view`, and
`delete_search_view`.

## Local Kanban Boards

Boards are local SQLite records for task and plan workflow views. Lanes map to
workflow statuses, can carry WIP limits, and render blocked/ready badges for
agent planning:

```bash
todos board create local-flow --lane "Ready=pending" "Doing=in_progress:3" --json
todos board show local-flow
todos board tui local-flow --json
todos board move local-flow <task-id> --lane Doing --json
todos board export local-flow --json
```

Task boards render tasks; plan boards use `--scope plans` and render plans by
plan status. Board snapshots include terminal key bindings for keyboard/TUI
clients, but the state is still just local data and can be exported or imported
without a hosted web UI. MCP clients use `create_board`, `list_boards`,
`get_board_snapshot`, and `move_board_card`.

## Local Duplicate Detection

Agents can scan local tasks for likely duplicates from imported issue URLs,
stack traces, exact titles, and similar task text, then merge duplicate evidence
without deleting either task record:

```bash
todos dedupe scan --threshold 0.8 --json
todos dedupe merge <primary-task-id> <duplicate-task-id> --reason "same imported issue" --json
```

Merges archive the duplicate as `cancelled`, add a `duplicates` relationship,
and preserve comments, dependencies, dependents, run ledgers, files, inbox
items, verification evidence, history, git refs, commits, and checklist rows on
the primary task. MCP clients use `find_duplicate_tasks` and
`merge_duplicate_task` for the same local-only workflow.

## Local Agent Context Packs

Context packs create deterministic run-start bundles for Codex, Claude Code,
Takumi, or any local agent. A pack selects task, project, plan, dependencies,
acceptance criteria, recent comments, relevant files, verification history,
traceability, and run-ledger evidence from the local SQLite database only:

```bash
todos context-pack <task-id> --profile codex --format markdown
todos context-pack <task-id> --profile claude --format json
todos context-pack <task-id> --profile takumi --run <run-id> --comments 12 --files 40
todos context-pack <task-id> --profile codex --token-budget 1800 --exclude runs --compact
```

MCP clients can call `build_agent_context_pack` with the same limits and choose
JSON, Markdown, compact JSON, or compact Markdown output. Long text and evidence
are redacted and size-limited, and stale or omitted local data is surfaced as
warnings in the pack.

Budget-aware context packing is local and deterministic. Use `--token-budget`
for an approximate character-based token budget, `--include` or `--exclude` to
shape sections, and `--summary-chars` to cap the redacted summaries generated
for omitted evidence. When the pack is too large, lower-priority evidence such
as runs, traceability, comments, files, dependencies, and plan context is
summarized in a stable `context_budget` block so agents still know what was left
out.

## Local External Issue Imports

Import issue records from pasted JSON, files, stdin, or explicit URLs without
depending on any hosted Hasna service. Imports default to a dry-run preview;
`--apply` creates local tasks, stores redacted source metadata, creates linked
inbox evidence, and skips existing tasks that already have the same source URL,
GitHub owner/repo/number, or external issue key:

```bash
todos issues import --file issues.json --provider github --json
todos issues import --file issues.json --provider github --apply --json
todos issues import --provider linear --apply < linear-export.json
todos issues import "Title: Fix parser\nURL: https://tracker.example/BUG-42" --apply --json
```

GitHub, Linear, Jira, and plain URL records are normalized into local task
metadata and tags. Network access is off unless `--allow-network` is passed; for
GitHub that explicitly shells out through the authenticated `gh` CLI, while
offline files and pasted exports work without tokens. MCP clients use
`import_external_issues` with the same dry-run, apply, inbox, and dedupe
controls.

## Local Inbox Intake

Paste failures, CI logs, GitHub issue URLs, files, or local git context into a
deduped inbox and create a linked task:

```bash
todos inbox add "bun test failed: parser regression" --source-type ci_log
todos inbox add --file /tmp/ci.log --source-name "local CI"
todos inbox add https://github.com/hasna/todos/issues/42 --source-url https://github.com/hasna/todos/issues/42
todos inbox parse "Add task fix parser priority high @codex #cli due tomorrow" --json
todos inbox parse --file plan-notes.txt --apply --json
todos inbox git --diff
todos inbox list
```

Inbox bodies and metadata are redacted before storage. Repeated input resolves
to the existing inbox item instead of creating duplicate tasks. Natural-language
intake parsing is deterministic and local-only; it defaults to a dry-run preview
and creates projects, plans, tasks, dependencies, and acceptance criteria only
with `--apply`.

## Bundled Onboarding Fixtures

The package ships deterministic local demo fixtures for first-run onboarding and
agent integration tests. The default `agent-project-demo` fixture shows the
simple flow used by the public demo: create a project, add todos, generate a
plan, run an agent, record command/artifact/verification evidence, review the
remaining task, and prove export/import with the local bridge bundle.

```bash
todos onboarding --json
todos onboarding --show agent-project-demo > agent-project-demo.bridge.json
todos onboarding --import agent-project-demo --json
todos onboarding --import agent-project-demo --apply
```

Fixtures are bundled with `@hasna/todos`, redacted, offline, and local-only.
Imports default to dry-run mode and use the same bridge importer as normal
exports, so CLI, MCP, and SDK consumers can test against the exact project,
tasks, plan, run ledger, evidence, saved view, and board records.

MCP clients can read `todos://onboarding/fixtures` or
`todos://onboarding/demo`, then use `list_onboarding_fixtures`,
`get_onboarding_fixture`, and `import_onboarding_fixture`.

## Local Agent Snapshots

Agents can refresh context through stable local snapshots for projects, tasks,
plans, runs, dependencies, activity events, and evidence. Snapshots are
redacted, deterministic, and include cursors plus fingerprints so MCP clients
can poll for changes without a hosted event stream.

```bash
todos snapshots --json
todos snapshots --show tasks --json
todos snapshots --show evidence --markdown
todos snapshots --poll --types tasks,evidence --since 2026-05-22T00:00:00.000Z --json
```

MCP clients can read `todos://snapshots/catalog` and
`todos://snapshots/tasks` through `todos://snapshots/evidence`, or use
`list_local_snapshots`, `get_local_snapshot`, and `poll_local_snapshots` for
JSON or Markdown payloads.

## SDK Integration Fixtures

Downstream SDK, CLI JSON, MCP, and agent-adapter tests can generate a complete
local fixture pack from the bundled demo project:

```bash
todos sdk-fixtures --json
todos sdk-fixtures --show > sdk-fixture-pack.json
todos sdk-fixtures --write .todos/sdk-integrations --json
```

The pack includes a local bridge fixture, stable JSON contract snapshots,
project/task/plan/run/evidence snapshots, and a context pack. Copy-pasteable
examples live in `examples/sdk-integrations/`, and the full guide is in
`docs/sdk-integrations.md`.

## Local Bridge Import/Export

Export a versioned local bridge bundle for migration, backup, or explicit
hand-off to another local store:

```bash
todos export --format bridge --output todos-bridge.json
todos export --format bridge --encrypt --output todos-bridge.enc.json
todos bridge-import todos-bridge.json --json
todos bridge-import todos-bridge.json --apply
todos bridge-import todos-bridge.json --apply --resolve-conflicts
```

Bridge bundles include local projects, task lists, plans, tasks, dependencies,
comments, run ledgers, command evidence, file evidence, artifacts, stored
artifact contents, commits, refs, verification records, saved views, local board
definitions, and local calendar items. Imports default to dry-run mode and
report conflicts before writing. The package does not upload bundles or call
hosted services; machine sync transports these bundles over SSH, and any hosted
sync must consume the exported JSON explicitly.

For multi-machine local work, `--resolve-conflicts` performs a safe task merge
instead of overwriting local edits. It fills blank local fields from the
incoming bundle, unions tags, merges non-conflicting metadata keys, and records
unresolved divergent fields in `metadata.sync_conflicts` for manual review.
Local non-empty title, status, priority, and metadata values win when both sides
changed.

## Local Backups and Integrity

Create a checksum-protected local backup wrapper around the bridge bundle when
you need a restorable snapshot with manifest counts and SQLite integrity
metadata:

```bash
todos backup create --output todos-backup.json
todos backup verify todos-backup.json --json
todos backup restore todos-backup.json --json
todos backup restore todos-backup.json --apply --resolve-conflicts
todos backup integrity --json
```

Backups include the same local projects, task lists, plans, tasks, comments,
runs, commands, files, commits, refs, verification records, saved views, boards,
calendar items, and stored artifact contents as bridge exports. The backup
manifest adds SHA-256 checksums for the full payload, embedded bridge bundle,
and each bridge section. Restore defaults to dry-run mode and refuses corrupted
or schema-incompatible bundles before importing.

## todos.md Markdown Import/Export

`todos.md` files are readable Markdown checklists with an embedded local bridge
bundle for lossless round trips. Export keeps the visible tasks, projects, and
plans easy to inspect while preserving local ids, comments, run ledgers,
dependencies, files, commits, and verification evidence in a hidden metadata
block:

```bash
todos export --format todos.md --output todos.md
todos todos-md-import todos.md --json
todos todos-md-import todos.md --apply
todos todos-md-import todos.md --apply --resolve-conflicts
```

Existing plain checklists also import locally. Use `# Project: Name`, `## Plan:
Name`, checkbox items, optional `priority: high`, `comment: ...`, `depends_on:
Other task title`, `run: completed smoke`, `#tags`, and `@agent` markers to
migrate older files without a hosted service.

## Doctor: referential integrity and the exit-code contract

`todos doctor` reports schema/migration drift, referential integrity, duplicate
indexes, invalid JSON metadata, missing project roots, and unsafe database file
permissions. In local mode it audits SQLite without calling hosted services; in
remote mode it audits the configured `/v1` authority.

```bash
todos doctor
todos doctor --json
```

### Exit codes

| code | meaning |
| --- | --- |
| `0` | CLEAN — every referential condition was measured and every count is zero. |
| `1` | FINDINGS — at least one orphaned/dangling reference, or an error-severity schema check. |
| `2` | INCOMPLETE — no findings, but a condition could not be measured, so health was **not** established. |

Advisory warnings (stale `in_progress` tasks, project paths missing on this
machine, duplicate indexes) are reported but do **not** change the exit code.
Findings dominate an incomplete report: a run with both exits `1` and says how
many conditions went unchecked.

`--no-fail-on-findings` forces exit `0` for a legacy consumer that gates on the
exit code. The findings are still printed, and the suppression is stated rather
than hidden — the printed code is always the one the process returns:

```
6 integrity condition(s) FAILED — 6 row(s) affected (5 error, 1 warning). (exit 0 — findings gate suppressed by --no-fail-on-findings; the verdict is 1)
```

`--json` therefore carries both numbers, and they must never be conflated:

| field | meaning |
| --- | --- |
| `exit_code` | the status the process **returns** (what a caller observes) |
| `verdict_exit_code` | the status the reported rows **imply**, ignoring suppression |
| `fail_on_findings` | `false` when the gate was opted out of |

### Referential conditions

Doctor counts each condition separately and prints every one of them —
including the zeroes — so "measured and clean" and "never measured" are visually
distinct:

| condition | severity |
| --- | --- |
| `tasks_without_project` | warn, **error** when it hides open work |
| `tasks_without_task_list` | warn, **error** when it hides open work |
| `tasks_with_unregistered_project` | error |
| `tasks_with_unregistered_task_list` | error |
| `task_lists_without_project` | warn |
| `task_lists_with_unregistered_project` | error |

Counts come from the backing engine, one aggregate query per condition, for
**both** storage engines (SQLite tables and the Postgres JSONB record store,
which has no foreign keys and is therefore where these rows accumulate). A
self-hosted authority exposes them at `GET /v1/integrity`; a backend that cannot
answer returns `501` rather than a false clean.

In remote mode doctor prefers that aggregate. When the authority does not expose
it, the task-list conditions are still derived exactly from the collections
doctor already fetches, and the task-level conditions are reported as
`NOT CHECKED` unless you opt into a read-only paged walk of `/v1/tasks`:

```bash
todos doctor --scan-tasks     # remote only; one GET per page, no writes
```

### Repair

Integrity findings are **report-only**: `--apply` never rewrites, deletes or
re-points an orphaned row, because deciding what happens to existing orphans is
an owner decision, not a diagnostic's. Safe repairs require explicit apply mode
and, when the database is file-backed, a local backup is created first:

```bash
todos doctor --apply
```

Repairs are limited to local schema/hygiene fixes: running the migration safety
net, clearing missing parent references, pruning orphaned dependency/run rows,
resetting invalid metadata JSON to `{}`, dropping duplicate non-primary indexes,
and tightening database file permissions. `--apply` is refused outright against
a remote authority.

## MCP Server

```bash
todos-mcp
```

## HTTP mode

The Streamable HTTP transport is served by `todos-serve`, never by `todos-mcp`
(stdio only). `todos-mcp --http`, `todos-mcp --port <n>` and `MCP_HTTP=1` are
refused with exit code 2 — the listener and its auth posture are server-only
code and do not ship in a client binary that MCP clients spawn without a
credential. Start the server instead:

```bash
todos-serve                   # mounts POST /mcp next to /api/*; see "REST API" for the auth gate
```

- Bind: `127.0.0.1` by default
- Health: `GET /health` → `{"status":"ok","name":"todos"}`
- MCP: `POST /mcp` on the same server as the REST API (Streamable HTTP, stateless)

The MCP server defaults to the token-saving `TODOS_PROFILE=minimal` profile.
Use `TODOS_PROFILE=standard` for broader task/project/resource tools, or
`TODOS_PROFILE=full` when you explicitly need every tool. You can add groups
with `TODOS_TOOL_GROUPS=templates`.

High-volume tools return compact payloads by default. Pass `detail: "full"` to
MCP calls such as `get_task`, `get_status`, `get_context`, `bootstrap`, and
`task_context` when you need full data.

`list_tasks` is a 50-row page when `limit` is omitted. One authoritative
response supplies the rows and its pagination receipt: `total`, the requested
and effective/server-capped limit, the consumed server `offset`, `has_more`,
`next_offset`, and (when supplied) `snapshot`/`next_cursor`. Prefer
`next_cursor`; otherwise continue from `next_offset`. A predecessor envelope
without `total` is still usable but reports `total: null`, `has_more: null`, and
`complete: null` rather than pretending the page is a complete snapshot. The
hosted read uses the configured `https://api.hasna.com/todos` authority through
exactly one `/v1` prefix and never falls back to local storage.

The CLI task list is paged the same way:

```bash
todos list --format json                         # structured 50-row page
todos list --format json --offset 50             # next page
todos list --format compact --limit 20 --offset 40
todos list --all --format json                   # explicit bounded exhaustion
todos list --roots-only --format json            # top-level tasks only
```

`list`, `count` and the `--assigned`/`--agent-name` filters include tasks that
carry a `parent_id`: they are ordinary rows, and `show`, `deps --project`,
`plans --show` and `project-panel` read them the same way. `--roots-only` is the
explicit opt-out and maps to `GET /v1/tasks?include_subtasks=false`; omitting it
never adds a parent filter (defect d05c3d55).

Explicit `--format json` returns `{tasks,count,total,requested_limit,limit,
offset,has_more,next_offset,next_cursor,snapshot,complete,...}`. Nullable
pagination fields mean the authority did not prove them; they are never guessed
from a separate probe. If a large authority page would cross the 64 KiB page
budget, the CLI re-requests a smaller prefix from the same cursor/snapshot and
emits that authority-returned continuation. It fails closed if the authority
cannot reproduce the prefix, rather than discarding a snapshot cursor or
advancing a plain offset by fewer rows than the authority consumed. Compact
output ends with the same continuation facts and retains zero-byte stdout for
an empty result. The legacy global `--json` spelling remains a bare task array, but it is
also bounded to 50 rows unless a limit is supplied. Limits above 50 require the
existing `--all` acknowledgement. `--all` without a limit exhausts only up to
5,000 rows and 1 MiB; the byte gate measures the actual pretty-printed legacy
`--json --all` payload before writing it. Larger results fail closed and must be
narrowed or paged.

A bare array cannot say whether it is the whole answer, so **every** `--json`
list page also writes one machine-readable line to stderr, tagged
`"schema":"todos.list.page.v1"`. It carries the same page fields as the
`--format json` envelope, minus the rows: `count`, `total`, `requested_limit`,
`limit`, `server_cap`, `offset`, `consumed`, `has_more`, `next_offset`,
`next_cursor`, `snapshot`, `complete`, `composed`, `all`, `byte_limited`,
`max_bytes`, plus `continue_with` (the exact argv fragment that resumes the
walk, or `null` when there is none) and `byte_length` (the size of the array
written to stdout). The two JSON spellings therefore answer "is this
everything, and what comes next?" with the same names and the same values.

The line is written on every page, not only on an incomplete one: absence of a
line must never be read as an end signal, and a walk has to be able to finish on
the marker alone. A page the authority proved complete carries
`has_more:false` / `complete:true` — the explicit terminal receipt. A page whose
completeness the authority did not testify to carries `complete:null` and says
so. An empty page follows the same rule, so `[]` under `has_more:true` can no
longer read as "nothing found".

```bash
todos --json list --status pending > rows.json 2> page.json
# page.json carries {"schema":"todos.list.page.v1","has_more":true,"next_offset":50,...}
todos --json list --status pending --offset 50 > rows-2.json
```

Follow `continue_with`, or `next_cursor` when it is set (prefer the cursor);
otherwise continue from `next_offset`. When `has_more` is `true` but both
continuations are `null`, the page was cut short and cannot be resumed: that
happens for `--sort`, which orders rows locally, so no authority offset
reproduces them (`--all --sort updated --limit 4` on 12 rows otherwise returned
`[11,10,9,8]`, then `[7,6,5,4]`, then `[11,10,9,8]`). The line states
`continuation_unsupported: "local-sort"` in that case, and the human warning
says the same thing — narrow the query, or drop `--sort`.

A page whose rows were narrowed client-side (for example `--created-by` against
an authority that ignores the predicate) and then cut short to fit the byte
ceiling continues from the raw authority prefix it actually consumed, not from
the number of rows it emitted, so following the marker never repeats a row it
already gave you. That holds when the page had to be filled from several
authority pages, because the raw span is tracked across all of them.

`--all` combined with a client-side transform and `--limit` — `--sort`, or a
date filter such as `--overdue` — cuts short a view whose row count is not an
authority position, so no offset resumes it: the marker withholds both
continuations and states `continuation_unsupported`, and `has_more` stays `true`
so the withholding is visible. Following the withdrawn offset previously
repeated delivered rows (`--all --overdue --limit 4` over 12 rows returned
`[0,2,4,6]`, then `[4,6,8,10]`), and a sorted page could end on a false
`complete:true` while rows were still missing.

A composed view that cannot prove a safe traversal names no position at all.
The one such composition is the scalar-status union, which merges partial
per-status windows locally: when any status still has more rows, an alternative
authority ordering makes a published offset repeat a row already delivered, so
the page returns the rows it has, keeps `has_more: true`, and reports
`continuation_unsupported: "status-union-composed"` instead of inventing a
position. When every status is exhausted the merged view is exact and the page is
terminal as usual. The working alternatives are to query one status at a time, or
to filter so the authority performs the composition itself. Direct pages are
unaffected: a fully consumed authority page keeps the continuation the authority
itself supplied, whether that is an offset or a cursor.

`complete` is only ever the authority's own statement. A response that carries
`complete: null` is an authority saying it does not testify, and the CLI
reports `null`; it infers an end from `has_more`/`total` only for a predecessor
that omits the field entirely.

stdout is unchanged: the legacy spelling still answers a bare task array, and
the line appears on stderr where a redirected `> rows.json` does not collect it.

## REST API

```bash
todos-serve
```

`/api/*` and `/mcp` are **local-only data planes and they fail closed**. The server
refuses to start unless one of the following is true:

| Configuration | Result |
| --- | --- |
| `HASNA_TODOS_SERVER_API_KEY=<key>` (or `--api-key <key>`) | `/api/*` + `/mcp` require the key |
| at least one `todos api-keys create` key exists | `/api/*` + `/mcp` require a key |
| `--allow-anonymous` **and** a loopback bind | anonymous, loopback peers only (local dev) |
| a cloud `DATABASE_URL` is configured (hosted `/v1` deployment) | `/api/*` + `/mcp` are **not served**; `/v1` stays authenticated |
| nothing of the above | **server exits non-zero** with the env var to set |

```bash
todos api-keys create "My app"          # then send x-api-key / Authorization: Bearer
HASNA_TODOS_SERVER_API_KEY=<key> todos-serve --host 0.0.0.0
todos serve --allow-anonymous           # local dev only; refused for a non-loopback --host
```

`--allow-anonymous` (or `TODOS_ALLOW_ANONYMOUS=1`) is refused for any non-loopback
bind host, and even when enabled it only serves requests whose transport peer is
itself loopback — so it can never publish an anonymous task read/write plane
off-box. (`todos-mcp --http` used to set it implicitly; that path is gone — the MCP
bin is stdio-only and `todos-serve` is the only process that serves `/mcp`.) Set
`HASNA_TODOS_SERVER_API_KEY` (and send it from your MCP client) to require a
credential there too.

Pass the generated key from your app as `x-api-key` or `Authorization: Bearer`.

`HASNA_TODOS_SERVER_API_KEY` is the **server's** own variable — the credential
`todos-serve` demands from its callers. It is deliberately NOT one of the client
credential names: `HASNA_TODOS_API_KEY` is how a client finds a key (the resolver
documented under [Credentials and Service Authority](#credentials-and-service-authority)),
and a fleet client key exported on a workstation must never silently become the
local server's accepted key — one name cannot play both roles on opposite sides
of the same trust boundary. `todos serve` prints one line at startup naming which
variable supplied its accepted key. For one release the client names
(`HASNA_TODOS_API_KEY` / `TODOS_API_KEY`) are still accepted as a documented
fallback so an env written before 2026-09-05 keeps working; the startup line
flags the deprecated spelling so operators can move the value.

Always-on / hosted deployments should use the versioned `/v1` API, which
authenticates independently against the cloud key store. `/health`, `/ready`,
`/version` and `/openapi.json` are the only routes that are public by design.

### Hosted `/v1` writes need a neutral `User-Agent`

The hosted `/v1` plane is served through a Cloudflare-protected gateway that
rejects automation-library default User-Agents. Python's `urllib` sends
`Python-urllib/3.x` and `requests` sends `python-requests/x.y`; the gateway
answers those with HTTP 403 (Cloudflare error 1010, browser-signature rule)
even though the identical request from `curl` succeeds with the same headers
and key. The origin is never reached, so no code change on the API server can
help — the caller must present a non-python `User-Agent` on every request
through the gateway:

```python
import json
import urllib.request

req = urllib.request.Request(
    "https://api.hasna.com/todos/v1/tasks",
    data=json.dumps({"title": "ship the parser"}).encode(),
    method="POST",
    headers={
        "x-api-key": "<your-key>",          # or Authorization: Bearer <your-key>
        "content-type": "application/json",
        "user-agent": "hasna-todos/1.0",    # never the urllib default
    },
)
urllib.request.urlopen(req)                  # 201, not 403 Cloudflare 1010
```

The `requests` equivalent sets the same `user-agent` header on the request or
session; a fixed neutral product string works for both reads and writes. Do not
rely on the library default User-Agent.

Agent callers can trim REST responses with field selectors:

```bash
curl "http://localhost:19427/api/tasks?fields=id,title,status,priority"
curl "http://localhost:19427/api/tasks/<id>?fields=id,title,status"
curl "http://localhost:19427/api/tasks/<id>/history?limit=20"
```

## Data Directory

Data is stored in `~/.hasna/todos/`.

## Local-Only Security Boundary

`@hasna/todos` is an open source package with a local-first storage boundary.
The CLI, MCP server, and SDK read and write on-box state under the explicit
local opt-in (`HASNA_TODOS_LOCAL=1`) and otherwise talk only to the Todos `/v1`
authority the operator configured. They never require a billing provider, a
SaaS account, or a remote model provider.

Release checks enforce that boundary before publishing:

- package metadata must stay public and point at `hasna/todos`
- install snippets must use `bun install -g @hasna/todos`
- package dependencies and generated tarballs are scanned for private or hosted
  service coupling
- public text surfaces and packed files are scanned for secret-like values
- local runtime tests use a no-network fixture for local-only workflows
- `bun run verify:release` performs a non-authoritative two-build review,
  validates commit/tree provenance and deterministic tarball bytes, and runs an
  isolated Bun install smoke test from the candidate tarball without changing
  the global installation
- authoritative publication is available only through `prepublishOnly` with an
  externally supplied `HASNA_TODOS_EXPECTED_COMMIT` matching `HEAD`; skips and
  final-pack mutation scripts are rejected
- the install smoke plan itself is covered by tests: it installs only with Bun,
  verifies `todos`, `todos-mcp`, and `todos-serve`, and rejects private or
  hosted endpoint references

## License

Apache-2.0 -- see [LICENSE](LICENSE)
