One [workspace.lints.rust] table replaces the deny attribute
scattered across every lib.rs, and each crate opts in through
[lints]. The lint now also reaches the integration-test crates,
whose roots gain their one-line docs.
The last forge content in config.rs was vocabulary, not data:
`ForgeKind` (name/parse/token_env/default_base) and the pure
`resolve_token` helper. By the same rule that moved `Watch` to the
watch table and left `CalendarWatch` in the calendar integration,
the kind belongs to the crate that speaks the forges.
- `rhema-forge-integration` gains `kind::ForgeKind` and
`resolve_token`, with the parse and token-fallback tests moved
alongside — the claw's config tests were testing the integration's
type through a re-export.
- The claw's surfaces import the type from the integration directly;
the crate root re-exports it, so `rhema_claw::ForgeKind` keeps
resolving for everything downstream.
config.rs is now config: credentials, the surface, paths, cadence,
and nothing that knows what a forge or a calendar is.
Same move as the watches, and the config's last piece of integration
state goes with it:
- `rhema-calendar-integration` owns the calendar table now
(`store.rs`): SQLite in the shared dispatch database, the pattern
the claw's watch table established. `CalendarWatch` gains the
`validate` the config's boot check used to run — per-kind URL and
credential rules, plus the lead-under-horizon check — so an invalid
calendar is loud at the one door every writer shares.
- `CalendarSource::from_store` reads the table fresh on every detect
pass (the forge source's pattern): a calendar added over the route
is live next tick, a removed one stops polling, no restart in the
story. The static constructor stays for tests.
- `POST/DELETE/GET /v1/calendars` on the daemon: the POST body is the
integration's `CalendarWatch` shape, validated at the table; DELETE
takes `?name=`; the api listing names the door.
- `ClawConfig.calendars` is gone. A config still carrying it is
refused at parse time, like the retired `watches:` seed. The
config-side validation and its tests moved with the type.
The config file now carries only what boot needs: credentials, the
surface, paths, the poll cadence. Everything the daemon reconfigures
at runtime lives in its tables, behind its HTTP doors.
CalendarWatch, the client resolution, and the slot policy were all
feed domain living in the claw's config and source tree; only the
extension adapter was the claw's. The reminder source now ships in
the integration as the default answer to "what does a start time
mean": one slot per lead time, calendar:<name> keys, fed to the
consumer's dispatcher. The crate doc drops its no-policy claim to
match, and the claw's resident prefixes take the calendar prefix
from the integration so the spelling cannot drift.
The triage mapped the forges' notification reasons onto the claw's
verdicts, but it co-evolves with the vocabulary, not with the claw:
a reason that enters it (Gitea's collapsed shape) needs a verdict in
the same change, and its tests pin the mapping against the
vocabulary's own types. It now lives in the integration crate as the
default classification; a consumer may reclassify on top. The claw
imports it like the rest of the forge seam.
Gitea's notifications feed names no per-row reason, so the adapter
collapses every row under one fixed NotificationReason::Other spelling
— and the claw's triage mapped every Other to Surface: the notification
reached the device queue and never a dispatch. A comment on a thread
the account authored could therefore never wake the agent on a
reason-less forge, whatever the watches said.
The adapter now publishes that spelling as gitea::COLLAPSED_REASON —
it is the seam's contract, not a detail — and the triage treats it as
the known shape it is: a notification on a thread of the account's own
dispatches the implementer. A spelling no adapter owns still surfaces;
an unknown shape remains the user's to judge, never the agent's to
guess at.
The stored `colors` key names a palette — rhema (the default,
Jadeite), aquamarine, black-and-white, or dracula — and the
Appearance page of /settings cycles it live, like the theme and
icon toggles beside it. Each palette carries a dark and a light
appearance under the existing `theme` key; dracula's light side is
derived, the official palette ships only dark.
The appearance decision moves off token comparison: Theme now
answers is_dark() from the ground/text luminance, so palettes
beyond the original two get the right per-profile fallback tones.