MAIL v2: documentation overhaul (+ accumulated v2 feature line) - #85
Conversation
…bhook-receiver tutorial Three new pages plus three index updates, addressing the webhook documentation gap in the v2 docs branch (the first of the two docs offers from the dev-list thread on Final Prep for v2). ## docs/explanations/webhook-delivery.md (new) The conceptual contract. What webhooks are for, the payload shape (with v2's reply_to and tags from PR #74), the HMAC-SHA256 scheme (timestamp.body, not body.timestamp — bug-shape worth flagging explicitly), the headers MAIL sends, the receiver verification checklist, the retry ladder (6 attempts: immediate, +1s, +30s, +5min, +1h, +6h — total window ~7h31m), the retry conditions (timeout / 5xx / 429 retry; 2xx / other 4xx don't), and the 'inbox is source of truth' contract that shapes how receivers should handle internal failures. Sources verified against src/mail/server/src/mail_server/backends/base.py (_handle_webhook_delivered and _webhook_delivered_post on origin main; the v2-docs branch is currently behind main on the schema changes from PR #74). Wrote against the canonical main-branch shape so the docs match the v2 release contract; the docs branch needs the rebase before merge. ## docs/howtos/manage-webhooks.md (new) Operator's guide. Secret generation, POST /admin/webhooks (with events and url), GET /admin/webhooks for listing, GET /admin/webhooks/{id} for inspection, PATCH /admin/webhooks/{id} for URL / secret rotation (event types are immutable in v2), DELETE /admin/webhooks/{id}. Includes the rotation coordination note (both sides must update at the same moment to avoid signature failures in flight). ## docs/tutorials/build-webhook-receiver.md (new) Implementer's walkthrough. A single-file FastAPI receiver with: - verify_signature using HMAC over raw bytes (calls out the two most common bugs: re-encoded JSON breaking signature; missing the timestamp.body prefix). - is_duplicate / mark_processed for event_id-based dedup with a 24-hour garbage-collection window. - is_timestamp_in_window for 5-min skew rejection. - The full endpoint composing them with the right error codes (503 if secret not configured, 408 for skew, 403 for bad signature, 200 with status=duplicate for retries). - Registration command for end-to-end test. - Diagnostic checklist for the 'nothing arrives' case. ## Index updates docs/{explanations,howtos,tutorials}/README.md each gain a row linking to the new page. ## Followups in scope of the original offer - docs/howtos/manage-mailing-lists.md exists as a stub today; drafting that one is the second piece of the offer and will land as a separate commit on the same branch. - docs/references/http-api.md is a stub overall (not just for webhooks). The webhook-specific endpoints could be sketched there in a later pass; deferred so this commit stays focused on the conceptual + tutorial layer. ## Verification The four facts that are easiest to get wrong (and that I had ground truth on from the chorus-side webhook receiver): - HMAC inputs: 'timestamp.raw_body' not 'raw_body.timestamp'. - X-MAIL-Timestamp value: Unix seconds as a STRING, used in both the HMAC and the header so receivers can recompute from the header alone. - Signature header format: 'sha256=<hex>'. - Body bytes: payload.model_dump_json() (Pydantic's canonical JSON), posted as-is — re-encoded JSON has different bytes and breaks verification. All four match what _webhook_delivered_post does in src/mail/server/src/mail_server/backends/base.py:653.
Second of the two docs offers from the dev-list thread on Final Prep for v2. Adds the conceptual explainer that was missing and fills the gaps in the existing how-to. ## docs/explanations/mailing-lists.md (new) The conceptual model. Coverage: - What a list is: a swarm-scoped, addressable fan-out target with no inbox. - The address shape: list:<name>@<swarm>@<host>, with the list: prefix as the distinguishing marker against the agent / user / admin shapes. - Why lists exist: three concrete problems (broadcast in one send, stable address for changing audience, policy separated from membership). - Anatomy: name, swarm, host, owner, members, policy, metadata. Canonical address immutable; policy and members mutable. - The policy shape: three independent enumerations (visibility, join_policy, send_policy), each with v1's honored variant flagged and the deferred variants explained as forward-looking wire-format reservations. - How messages flow: local delivery picks up list:, looks up list by address, fans out per-member with metadata.list_address, nested-list members get skipped, webhook firing is per-member. - Admin vs user-agent permission split: admin owns create / patch / member add-remove / delete and gets unconditional reads; user-agents get policy-gated read / subscribe / unsubscribe / send. - Addressability examples (table of the four address shapes). - Things lists are NOT: not a queue or buffer, not a history store, not a privacy boundary in v1. Sources verified against src/mail/protocol/src/mail_protocol/core/lists.py — the policy enum and 'forward-looking' framing are paraphrased from the inline docstring. ## docs/howtos/manage-mailing-lists.md (polished) The existing how-to covered list / list-get / create / subscribe / unsubscribe / member-post / member-delete / send. Added: - An updated Starting Point that points at the new explanation and names the address-shape convention explicitly. - Step 6 (NEW): update list policy via list-patch with the v1 honored variants and a pointer to the deferred-variant discussion. - Step 7 (NEW): delete a list via list-delete with the in-flight-messages-already-expanded note. - Step 8 (was step 6): send to a list, plus a forward reference to the webhook delivery doc for how the list_address metadata surfaces on the wire. - See-also section with cross-links to mailing-lists, webhook-delivery, addressing-model, and http-api. ## docs/explanations/README.md Index row added for the new mailing-lists explainer.
docs: webhook delivery contract, manage-webhooks how-to, and build-webhook-receiver tutorial
Signed-off-by: Addison Kline <77369109+addisonkline@users.noreply.github.com>
docs: mailing-lists explanation + manage-mailing-lists polish
Add MAIL 2.0 message-reply and tag support across the protocol, server,
and client, plus a migration path for existing deployments.
Protocol:
- MAILMessage gains required `mail_version` ("2.0") and `tags`, plus an
optional `reply_to` referencing the replied-to message's id.
- Thread `reply_to`/`tags` through DraftPostRequest, MAILDraft, and
`tags` through DraftSendPostRequest (all default-safe so pre-2.0
drafts stay loadable).
- Surface `reply_to` (msg_-prefixed) and `tags` in MAILMessageInWebhook.
Server:
- post_draft stores reply_to/tags on the draft; send_draft stamps
mail_version, copies reply_to, and merges draft + send-time tags as an
order-preserving union.
- Webhook delivery payloads now carry reply_to/tags.
Client:
- New `reply` command (alias `r`): replies to the original sender,
defaults the subject to `Re: <subject>`, sets reply_to.
- `--tags` on compose, send, and reply; reply_to/tags shown in message
output and listed in `mail --help`.
Migration:
- scripts/migrate_messages_v2.py backfills mail_version/tags on persisted
message records (dry-run, backup, deployment overrides; idempotent).
Tests/docs:
- Update all MAILMessage construction sites; add coverage for tag
validators, the reply command, draft/tag-merge behavior, webhook
payload fields, and the migration script.
- Document the new fields in SPEC.md (§7.8-7.10), regenerate
spec/openapi.yaml, and update the client CLI reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Change the user-facing copyright in CLI --help footers and the backend-init epilog from "Addison Kline" to "2025-present MAIL Contributors". Add a shared --license flag (via cli_help) that prints the Apache-2.0 notice and exits, wired into all CLIs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add two draft enhancements:
1. `mail compose` can now read a draft body from a file via
`-F`/`--body-file PATH` as an alternative to the inline positional
body. Exactly one of the two must be supplied.
2. A new `PATCH /drafts/{draft_id}` endpoint lets an authenticated
user-agent update an existing draft's subject, body, reply_to, and
tags. Only supplied fields change (tags: [] clears, omitted leaves
unchanged) and updated_at is refreshed on any edit. Exposed in the
CLI as `mail draft-edit` (alias `de`).
Regenerates spec/openapi.yaml and updates the HTTP and CLI docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Admin (and list) endpoints addressed resources by three inconsistent slices of the canonical MAIL address: agents used a partial address (name@swarm), daemons/users a bare id, and lists the full list:name@swarm@host address. None of the path params were declared to FastAPI, so they were undocumented in OpenAPI and unvalidated. Standardize on a single rule: the path param is the resource's local identifier, with the user-agent prefix implied by the route and the host implied by the server (agents name@swarm, daemons worker_name, users user_id, swarms swarm_name, lists name@swarm, webhooks wh_<uuid>). member_address stays a full MAIL address — a list member may be any user-agent, possibly remote. - Add typed Path(...) params + shape validation across admin/lists routers: 422 on malformed, 404 on well-formed-but-unknown; params now documented in spec/openapi.yaml. - Lists HTTP surface now addresses lists by local name@swarm; the router reconstructs the full list: key so backends are unchanged. Message recipients still use the full list: address (delivery unchanged). - Rename agent_address -> local_address through base/memory/sqlite backends; declare host on the backend Protocol. - Fix stale "Corresponds to" docstrings, the daemon-get CLI arg bug, and list-address help text (local form). - Update tests + fixtures to local addresses; add 422 path-param tests. BREAKING CHANGE: list HTTP endpoints now take the local address (name@swarm) instead of the full list:name@swarm@host address. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a stateful, rotating refresh-token mechanism so browsers (and other long-lived clients) can renew their access-token JWT without re-entering a password. Refresh tokens are opaque, high-entropy strings stored hashed (sha256) and grouped into families: login starts a family, each rotation keeps the family and carries its absolute `expires_at` forward unchanged (no sliding window). Presenting a revoked-or-rotated token is treated as reuse and revokes the whole family. Only interactive principals (users/admins) get refresh tokens; agents and daemons re-authenticate with their credentials. Delivery is dual: the token is returned in the response body and set as an `httpOnly; Secure; SameSite=Strict` cookie scoped to `/auth`, so browsers get silent renewal while the wider API stays header-only and CSRF-immune; the CLI sends the token back in the request body. - protocol: `RefreshTokenRecord`; `refresh_token`/`expires_in` on the token response; `AuthRefreshPostRequest`/`AuthRefreshPostResponse`/`AuthLogoutPostResponse` - backends: six refresh-token methods on the protocol, implemented for both the memory (persisted via fs checkpoints) and sqlite (new `refresh_tokens` table, FK cascade on user deletion) backends, with dual-backend conformance tests - server: `/auth/token` mints + sets the cookie, new `/auth/refresh` (rotate + reuse detection + fail-closed on deleted owner) and `/auth/logout`, and `/auth/password/reset` now revokes all of the principal's families - config: env-only `MAIL_REFRESH_TOKEN_EXPIRE_DAYS` (required), `MAIL_COOKIE_SECURE`, `MAIL_COOKIE_DOMAIN` - client: `mail refresh` command; `mail login` surfaces the refresh token - docs: HTTP API reference, quickstarts, .env.example, regenerated openapi.yaml Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST/PATCH handlers took only `request: Request` and parsed bodies by hand via validators.py, so FastAPI generated no requestBody schema and the bodies were absent from /docs and spec/openapi.yaml. Promote each request model to a typed handler parameter (Approach A) across the drafts, daemon, admin, lists, and auth routers; FastAPI now validates and documents all 17 bodies. /auth/refresh and /auth/logout take the refresh-token body as an optional Body(default=None) to preserve the cookie-only browser flow; _read_refresh_token is now sync and reads the parsed payload, keeping cookie-over-body precedence. Also declare BoxFilterParams as a typed Query() param on the inbox/outbox/ trash/drafts GET endpoints (same root cause, query side), so the limit/offset/ sort_by/order filters are documented too. Drop the now-unused body and query validators (path-param validators kept). Add the missing MAIL_REFRESH_TOKEN_EXPIRE_DAYS placeholder to generate_openapi.py so the spec regenerates standalone, and regenerate spec/openapi.yaml. Add a contract test asserting requestBody/query-param presence so the regression can't silently return. The 422 body shape for invalid bodies is now FastAPI's native structured detail instead of the previous string; status code is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Messages are delivered unread and marked read when the owning user-agent
opens them via GET /inbox/{message_id}. Read state is tracked per-owner
on the inbox membership record (mailbox_items.is_read in SQLite, a
read_inbox set in the memory backend) so a fanned-out message's status is
independent per recipient. Surfaced as is_read on MAILInboxEntrySummary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bhook-receiver tutorial Three new pages plus three index updates, addressing the webhook documentation gap in the v2 docs branch (the first of the two docs offers from the dev-list thread on Final Prep for v2). The conceptual contract. What webhooks are for, the payload shape (with v2's reply_to and tags from PR #74), the HMAC-SHA256 scheme (timestamp.body, not body.timestamp — bug-shape worth flagging explicitly), the headers MAIL sends, the receiver verification checklist, the retry ladder (6 attempts: immediate, +1s, +30s, +5min, +1h, +6h — total window ~7h31m), the retry conditions (timeout / 5xx / 429 retry; 2xx / other 4xx don't), and the 'inbox is source of truth' contract that shapes how receivers should handle internal failures. Sources verified against src/mail/server/src/mail_server/backends/base.py (_handle_webhook_delivered and _webhook_delivered_post on origin main; the v2-docs branch is currently behind main on the schema changes from PR #74). Wrote against the canonical main-branch shape so the docs match the v2 release contract; the docs branch needs the rebase before merge. Operator's guide. Secret generation, POST /admin/webhooks (with events and url), GET /admin/webhooks for listing, GET /admin/webhooks/{id} for inspection, PATCH /admin/webhooks/{id} for URL / secret rotation (event types are immutable in v2), DELETE /admin/webhooks/{id}. Includes the rotation coordination note (both sides must update at the same moment to avoid signature failures in flight). Implementer's walkthrough. A single-file FastAPI receiver with: - verify_signature using HMAC over raw bytes (calls out the two most common bugs: re-encoded JSON breaking signature; missing the timestamp.body prefix). - is_duplicate / mark_processed for event_id-based dedup with a 24-hour garbage-collection window. - is_timestamp_in_window for 5-min skew rejection. - The full endpoint composing them with the right error codes (503 if secret not configured, 408 for skew, 403 for bad signature, 200 with status=duplicate for retries). - Registration command for end-to-end test. - Diagnostic checklist for the 'nothing arrives' case. docs/{explanations,howtos,tutorials}/README.md each gain a row linking to the new page. - docs/howtos/manage-mailing-lists.md exists as a stub today; drafting that one is the second piece of the offer and will land as a separate commit on the same branch. - docs/references/http-api.md is a stub overall (not just for webhooks). The webhook-specific endpoints could be sketched there in a later pass; deferred so this commit stays focused on the conceptual + tutorial layer. The four facts that are easiest to get wrong (and that I had ground truth on from the chorus-side webhook receiver): - HMAC inputs: 'timestamp.raw_body' not 'raw_body.timestamp'. - X-MAIL-Timestamp value: Unix seconds as a STRING, used in both the HMAC and the header so receivers can recompute from the header alone. - Signature header format: 'sha256=<hex>'. - Body bytes: payload.model_dump_json() (Pydantic's canonical JSON), posted as-is — re-encoded JSON has different bytes and breaks verification. All four match what _webhook_delivered_post does in src/mail/server/src/mail_server/backends/base.py:653.
The v2-docs rebase (9da55cd) committed unresolved conflict markers into eight doc files. Resolve each by keeping the finished (HEAD) content and dropping the leftover pre-writing scaffolding (Draft Outline / Steps to Cover / Validation) and duplicated Not Here blocks. No written content is lost; the run-local-mail tutorial body was wrapped inside a HEAD block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix seven concrete mismatches found cross-checking docs against source: - manage-swarms: command is 'swarm-list', not 'swarms-list' - manage-mailing-lists: 'list-patch' CLI verb is a stub that raises NotImplementedError; document the PATCH /admin/lists endpoint instead - manage-mailing-lists: management commands take the local name@swarm form, only sends use the full list:name@swarm@host recipient form - authenticate-user-agent: document the mail refresh / MAIL_REFRESH_TOKEN flow for interactive principals instead of re-login only - addressing-model: identifier hard cap is 31, not 32 - delivery-model: /daemon/deliver/remote route is wired but its handler raises NotImplementedError; say 'not yet functional', not 'not implemented' - build-minimal-http-client: token response includes refresh_token and expires_in; send response includes mail_version, reply_to, and tags Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- docs/README.md 'Proposed Layout' now lists the pages that already exist: Build a Webhook Receiver (tutorials), Manage Webhooks (how-tos), and Mailing Lists + Webhook Delivery (explanations). - initialize-memory-backend: write the previously-TODO 'reinitialize a clean slate' step (delete the deployment dir under ~/.mail-swarms and re-run backend-init) and flip its stale 'Status: stub' to 'draft'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fill in six reference stubs against verified source: - repository-layout: workspace/package map, scripts, tests, legacy - protocol-specification: SPEC.md + openapi roles, section map, contract tests, and the known spec-vs-impl identifier-length divergence (31 vs 32) - http-api: full route inventory with auth levels + status codes - data-models: protocol model field tables, constants, validators - storage-backends: backend contract, memory vs sqlite, backend-init - configuration: exhaustive env var + CLI flag inventory Also refine addressing-model to distinguish the spec's SHOULD-32 from the reference implementation's hard cap of 31. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add scripts/build_cli_docs.py, which imports each CLI's build_parser() and renders a Markdown reference (global options + subcommands with their args, aliases, and expanded defaults). Generate the four CLI reference pages (client/admin/server/daemon) from it so they cannot drift from the parsers. Regenerate with: uv run python scripts/build_cli_docs.py Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SPEC.md §6 says agent/user/admin/daemon-worker/swarm/list identifiers SHOULD be <= 32 chars, but the reference implementation hard-capped at 31. Raise all name/keyword *_LEN_MAX constants from 31 to 32 so the impl matches the spec (message tags were already 32). Contract/unit length tests are constant-relative, so they follow automatically; full suite green (769 passed). Update the docs that had flagged the divergence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
While verifying the 31->32 change against the full suite, the xfail'd
tests/integration/test_stubs.py surfaced that six operations are
NotImplementedError on the *memory* backend only and fully implemented on
sqlite: DELETE inbox/draft/trash, POST /trash/clear,
PATCH /admin/webhooks/{id}, and POST /daemon/deliver/remote. The reference
docs had presented these as universally available (and wrongly called remote
delivery unimplemented in both backends). Add a memory-backend-gaps note to
http-api, a limitations entry + capability row to storage-backends, and
correct the delivery-model remote-delivery paragraph.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fill in the five explanation stubs against SPEC.md, the package layout, the auth/refresh-token implementation, and the legacy README: - mail-v2-overview: what MAIL is, the v1->v2 comms-only refocus, non-goals - architecture: how protocol/server/client/daemon fit; message lifecycle - security-model: trust boundaries, bearer + refresh tokens, secret handling, production expectations - mail-v1-legacy: how to read the archived v1 runtime without importing its assumptions; how to run legacy tests - documentation-system: the Divio four-category model and how to place a page Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fill in the final six how-to stubs against the CLIs, scripts, and test config: - run-server: required env vars, backend choice, checkpoint interval, health check - run-daemon: daemon credentials, startup/poll behavior, log levels - run-tests: default run, marker/path subsets, coverage, legacy extra, and how to read drift/contract failures and the memory-backend xfail stubs - send-message-cli: the compose -> send two-step, outbox/inbox inspection, validation failures - manage-user-agents: mail-admin create/list/get/delete for agents/users/daemons (password is prompted, not passed as an arg) - regenerate-api-artifacts: openapi + CLI docs + llms.txt + third-party notices, with drift validation Completes P2; docs/ now has no remaining stubs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the transitional 'being reorganized' framing with a proper landing README: badges (PyPI/Python/license/spec), a highlights section, PyPI + source install, a runnable local quickstart, a package table, a documentation map linking into docs/, repository layout, development commands, and contributing/license sections. Drops the stale note about root scripts targeting the legacy runtime (scripts/ is v2-only). All relative links verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rheaton64
left a comment
There was a problem hiding this comment.
Reviewed and approved.
Large PR but the shape is honest — 31 commits total accumulated
via rebase, only one source-change line in the docs pass itself.
Verified.
The source change
mail_protocol/core/constants.py: raising the six
*_LEN_MAX constants from 31 → 32 to match SPEC §6. Behavior
change: 32-char identifiers now accepted. Length tests are
constant-relative so they follow automatically.
Chorus impact: none. Chorus doesn't hardcode the 31-limit
anywhere. Verified:
chorus/mail.pyandchorus/mb.py: no identifier length
validation on the client side. Chorus accepts whatever
addresses MAIL accepts.chorus/db/schema.py:mail_credentials.agent_address
isString(255);swarmisString(128). Both well
above the 32-char MAIL cap.- No hardcoded
31/32constants in chorus's MAIL
paths.
The docs structure
Divio-style with four clean categories (tutorials, how-tos,
references, explanations). Spot-checked the top-level index
and the layout is clear + navigable. The docs/README.md
"Start Here" section maps intent → category, which is what
Divio-style is supposed to enable.
31 commits post-rebase is a lot but the P0→P1→P2→P3
sequencing shows the work was deliberate — first unblock the
merge conflicts (P0), then fix drift in existing docs (P1),
then fill stubs (P2), then hygiene (P3). Nice discipline.
The accumulated features
Refresh tokens (PR #82), read/unread (PR #84), replies + tags
(PR #74), forwarding (PR #75), draft editing, admin endpoint
address standardization (PR #81), OpenAPI docs (PR #83) —
each was reviewed + approved on its own PR through the dev
list. This PR is the final "wrap the branch" step, not
re-review of any single feature.
Two small observations
Not blocking — just noticing:
- Repository layout doc would be useful for me as an
external consumer trying to keep MAIL's structure in my head.
I seedocs/references/repository-layout.mdin the file
list; that solves it. - The
list-patchNotImplementedError + memory-backend
stubs are correctly documented as such rather than glossed
over. That's the honest shape — matches what MAIL callers
will actually see.
Approving
Ship it.
— minichorus-pm
Lands the
kline/v2-docsbranch intomain: the accumulated MAIL v2 feature work plus a full documentation overhaul. 31 commits total.Aside from the pre-existing feature commits below, the docs pass itself is documentation-only except for one deliberate change:
750a2a3— align identifier length cap to the spec (31 → 32).mail_protocol/core/constants.pyhard-capped agent/user/admin/daemon-worker/swarm/list identifiers at 31, while SPEC §6 says they SHOULD be ≤ 32 (message tags were already 32). Raised the six*_LEN_MAXconstants to 32 so the implementation matches the spec. Behavior change: identifiers of exactly 32 characters are now accepted. Length tests are constant-relative, so they follow automatically; full suite green.Documentation overhaul (this pass)
6e8b09c).swarms-list→swarm-list, thelist-patchNotImplementedErrorstub, list address forms, themail refreshflow, and the HTTP-client token/message payloads (9d357dd).scripts/build_cli_docs.py), 5 explanations, 6 how-tos.docs/README.mdomissions and finishedinitialize-memory-backend(d0a7fd3).NotImplementedErroron the memory backend and implemented only on SQLite (9b72cb5).3fa6025).Result:
docs/now has zero stubs; all relative links verified.Accumulated v2 features (already on the branch, ahead of main)
de3b37e)6c595f9)1e400da) and forwarding (1f9b3d0)447a9ec)038af01)e1aac76)--license(bb5e5b3)Testing
uv run pytest→ 769 passed, 1 skipped, 6 xfailed (the xfails are the documented memory-backend stubs).Documented follow-ups (not changed here)
list-patchCLI verb raisesNotImplementedError— documented rather than implemented.🤖 Generated with Claude Code