Commit Graph
90 Commits
Author SHA1 Message Date
vhaudiquet 3efafd9c63 chore: bump the workspace to 0.2.3 2026-11-12 11:03:29 +01:00
vhaudiquet 3415e27463 feat(code): ask the request shape on the custom-endpoint login, and honor the stored choice everywhere
The one kind whose dialect its route cannot imply is the openai kind:
OpenAI itself serves the Responses API, an arbitrary OpenAI-compatible
server only promises chat completions. That choice was implicit; now
the user can make it.

Config: ProviderInstance gains api (Option<ProviderApi>, chat |
responses), serde snake_case, defaulting to absent — every config
written before the field reads as unset and keeps its route rule.

Registry: build() takes the stored choice as its fifth argument. An
explicit chat/responses beats the route rule for the openai kind;
unknown spellings fall back to the rule. A wiremock test proves the
lift: build(openai, override_url, responses) lands on /responses.

Frontends: the TUI's add-instance flow inserts a ChooseApi stage
between the base-URL and key prompts for the custom-endpoint row only
— arrows choose, enter records and descends, esc returns to the URL
prompt; chat completions leads the list so enter-without-looking keeps
what the route rule would have picked. The choice rides the pending
login into the backend, the saved instance, and back out through
from_profile. The ACP resolver honors the stored field via
stored_api(); claw's ProviderParams gains the same field (config-file
writable; its login forms do not ask yet). Every other kind's dialect
stays its endpoint's own decision.

New tests: the stage's default and esc contract, the explicit Responses
pick, the end-to-end landing in the saved instance, the config serde
round-trip both spellings plus old-file absence, and the registry's
precedence rule. fmt, clippy -D warnings, and all 135 test groups
pass; the WIP in screens/settings.rs, ui/pane.rs, palette.rs, README
is untouched.
2026-11-12 00:29:21 +01:00
vhaudiquet 762b49eaf4 fix(rhema-code): indent list items' paragraphs in the transcript
The markdown renderer tracked list depth only to pick a marker. A
paragraph that follows a blank row inside an item started at column 0,
so a tool's catalog lines and its "Where it runs" paragraph read as
text after the list rather than part of the bullet. `rhema system show`
previews the prompt through this renderer, which is where it showed.

An item now records its content column, marker width included, and a
row the renderer starts inside one is padded to it. The paragraph that
introduces a sublist keeps it attached: the blank row the paragraph
asks for at its end is taken back, the hug a sublist already got when
no lead-in preceded it.

The tool sections put a blank row between the row and its description
so the description is a paragraph of the item — the shape the indent
applies to, where a soft break would have joined it to the schema.
2026-11-11 18:13:17 +01:00
vhaudiquet 1663dee661 fix(agent-tools): render tool schemas without HTML-safe escapes
minijinja's tojson escapes `<`, `>`, `&` and `'` so its output can ride
a script tag. A system prompt is not a script tag, so every apostrophe
in a field description reached the model as \u0027 and every
`<provider>` as \u003c: harder to read, and more tokens for the same
text.

The renderer registers a json filter that serializes through serde_json
and leaves those characters alone, and both tool sections use it.
tojson stays for a user template whose output does feed a page. The
serialization is also compact, which drops the spaces tojson puts
after every separator.

The tool row is `name | arguments` with the description indented
underneath, so a multi-line description can no longer push the schema
column below another tool's prose, where it read as part of that tool.
2026-11-11 18:13:17 +01:00
vhaudiquet 263184ff8e chore: bump the workspace to 0.2.2 2026-11-10 19:44:04 +01:00
vhaudiquet 3e2ff7599d feat(chatgpt): add the ChatGPT subscription adapter and login
Rhema had no way to spend a ChatGPT subscription: the models were reachable only through an OpenAI API key, which bills the API account instead. The new adapter signs in through OpenAI's browser OAuth flow with PKCE and a loopback callback, stores the returned token JSON as the instance credential, and sends turns to the Codex Responses backend with the subscription's live model catalog.

The flow runs on the existing InteractiveLogin seam, so the wizard, the settings dialog, and the headless frontend all drive it. The TUI shows the link's host with an open button and a copy button: enter opens the default browser, and c copies the full URL, which keeps a long authorization query off the screen and out of a manual selection. The provider registry, config, ACP, claw, and the web settings page gain the new provider kind.
2026-11-10 18:06:00 +01:00
vhaudiquet 05045e4ee5 fix(claw): carry a kind's stored route into construction 2026-11-09 23:38:53 +01:00
vhaudiquet ddb26bf34d build(deploy): copy the OpenCode manifest into the images 2026-11-09 23:32:18 +01:00
vhaudiquet 8e76d26c5e feat(claw): discover OpenCode credentials 2026-11-09 23:32:18 +01:00
vhaudiquet fa0e3d4470 feat(android): resume from the seq, and walk back through the session
The chat screen asked the claw for everything the cache did not hold by
counting events, and a session that ran for weeks cost the whole record
the first time it opened here. The stream now names each frame's seq and
the cache stores it, so a reopening screen sends since=<seq> and receives
only the gap. The cache migrates in place: the old count of events is
exactly the last seq plus one, because the record numbers densely.

A screen with no cache opens on the newest turn and walks back through
the new history route when the reader reaches the top, one whole turn
per page, so the phone does not hold a weeks-long transcript to show the
last few messages. Pages are folded through the same reducer the stream
uses and prepended, since the list runs oldest first: appended, they
would land at the newest end. A reset comment (the record no longer
holds the cursor) drops the mirror, and the tail that follows rebuilds
it. The outcome tag colours a steered turn as an accent rather than a
failure, matching the web UI.
2026-11-08 18:01:52 +01:00
vhaudiquet 70d9bb0d34 fix(claw): a resumed stream numbers its live frames from the cursor
The frame pump counted the live seqs from the last replayed event, which
is right only when the replay carried something. A client whose cursor
already sat at the record's head replays nothing, so the pump restarted
at zero: the client's next resume then named seq 0 and replayed the
whole record, and every event it printed in between carried a number
that named something else.

Base the count on the cursor when the gap is empty. The test resumes at
the head of a finished turn and asserts the next live frame names the
record's next seq.
2026-11-08 18:01:42 +01:00
vhaudiquet ed79c0cf1b feat(web/claw): open on the newest turn, walk back, and resume from the seq
A reload of a weeks-old session replayed the whole record before the
first pixel, and every reconnect reset the mirror and replayed it
again. The stream now names each frame's seq and the client keeps it,
so a retry sends since=<seq> and receives only the gap, and the mirror
is left alone: its items are still true.

A first open with no cursor gets the tail, cut to a turn boundary,
which paints the latest conversation at once. The turns above it come
from the history route, one whole turn per page, fetched when the
reader nears the top or when the transcript is too short to fill the
screen; a page folds through the same reducer as the live stream and
joins above, and the scroll position moves down by the page's height so
the reader keeps their place. A reset comment (the record no longer
holds the cursor) drops the mirror; the tail that follows rebuilds it.

The dev proxy rebuilt the upstream URL from the pathname alone, so
since and before never reached the daemon under nuxt dev: every resume
was a full replay and every history page was the tail. It forwards the
query string now.
2026-11-08 18:01:30 +01:00
vhaudiquet 744ba8b06c feat(claw): the event stream names its seqs, so a client resumes instead of replaying
A session that runs for weeks made every reconnect cost the whole
record: the events route replayed all of it, and the only cursor was a
count of events held, which cannot survive a gap. The replay is now
addressed by seq.

Every frame carries the record's seq as its SSE id, and since is that
seq, so a reconnecting client reads exactly the events it missed. With
no cursor the stream opens on the tail (the newest 400 events, cut back
to the first turn boundary in them) and goes live; the turns above it
come from a new history route, one whole turn per page, each ending
where the previous page began, with a more flag to stop the walk. A
cursor the record can no longer serve comes back stale with a reset
comment and the tail, read under the same lock as the live receiver so
the reset has no hole either.

The transcription fold in the store already had the seq column dense
per key; this exposes it as three range reads (after a cursor, the
newest window, the window below a point) instead of one whole-record
read, and keeps the in-memory record's seqs dense the same way, so a
store-less assembly answers the same protocol.
2026-11-08 18:01:10 +01:00
vhaudiquet 94b82e2636 fix(web): the bugs a chat that never closes for a turn exposes
An investigation pass over the client, driven by steering's new shape —
a composer that is always live makes adjacent latent bugs visible.

The event stream now watches for silence: the claw keeps the
connection alive with a comment every fifteen seconds, and three
missed ones abort the fetch and reconnect, so a half-open TCP after a
laptop sleep no longer reads as 'live' forever. A reconnect clears the
attempted-attachment set with the transcript, or an image whose fetch
failed during the outage never retries against the recovered stream.
Saving a token reloads the identity and accounts the 401 froze: the
banner cleared, but the pages stayed empty until a browser reload.

A failed POST retracts its optimistic sent row (and its pending echo,
so a message the claw did take still renders from the stream's echo);
before, a send into an unreachable claw left a phantom row waiting for
a reply that could never come. Drafts are per-session now — the
composer's text belonged to no session, so a draft typed into one
session sent into whichever was selected at Enter. The providers
login form distinguishes a landed login (the roster carries its id)
from a failed one and no longer closes on an unrelated state refresh.

Two cost fixes that read as jank on long turns: the streamed reply's
markdown re-parsed the whole text per delta (quadratic) and now
re-renders on a short trailing debounce; the attachment-image watcher
walked the whole transcript on every streamed chunk and now scans only
the items it has not seen. The accounts page drops a stray Rust-ism
comment, and the avatar row's meta line admits webp like its accept
attribute always did.
2026-11-08 18:00:56 +01:00
vhaudiquet 041240f706 feat(claw): a message into a live turn steers it
The resident dispatch's turn now runs under a steer select: the
messages route, after its ingest, signals a per-key interrupt registry,
the drop lands at the turn's current await (an in-flight shell command
dies with it), the dangling calls close, and the dispatch loops on the
same lease — the steered message runs as the next round, same session,
same stream. The interrupted turn closes on the stream with turn_done
carrying outcome "steered", and the shim's whole and streamed
answers both wait through steered end markers for the round that
actually replies.

The messages route answers 202 the moment the message is accepted; the
drain pass runs detached, so a turn's minutes never hold the request
open. The web composer leans on that: it never closes for a running
turn, the placeholder and the send button say that sending steers, a
steered outcome gets its own tone, and the sent-row echo dedup pops a
pending list instead of comparing the last item — a steered chain's
extra end marker can no longer double the row.
2026-11-08 18:00:39 +01:00
vhaudiquet 52ee642eee chore: bump the workspace to 0.2.1 2026-11-08 17:54:12 +01:00
vhaudiquet f5b4d35d99 fix(deploy): the claw image manifest gains rhema-provider-registry
The fetch stage parses the workspace before the sources land, off each
member's Cargo.toml COPY'd one row at a time. The new registry crate
rode in through the crates/* member glob, rhema-acp grew a path
dependency on it, and the enumerated list had no row for it, so cargo
died at manifest parse looking for
/build/crates/rhema-provider-registry/Cargo.toml. The row joins the
list in alphabetical place.
2026-11-08 17:53:04 +01:00
vhaudiquet 5aed7b6a3d refactor(provider-registry): the fleet's construction recipes move into one build
Every frontend spelled the same recipes in its own backend assembly,
and rhema-code spelled them three times: deepseek is the generic
adapter at DeepSeek's endpoint, zai is the Anthropic adapter at Z.ai's
route under bearer auth, each keyed kind takes its own adapter — with
the DeepSeek endpoint a string literal in three crates and the zai
recipe hand-rolled in each. The construction could not centralize
while the model catalog lived on inherent adapter methods: claw's
AnyProvider enum existed to box listings a trait object could not
reach, and code holding only a provider name had to construct
concrete types to list models at all.

rhema-provider gains the ModelListing seam, implemented by every
adapter, so the catalog is reachable through a trait object. A new
rhema-provider-registry owns the wiring: build() takes a
rhema-config PROVIDERS spelling plus whatever the caller resolved —
a key, the llama.cpp server address, a stored route override, blank
reading as unset — and returns the turn provider and the catalog
listing. What stays with the frontend is policy: credential
resolution, the retry wrapper, the default model. The base constants
and the named zai route constructor stay on the adapters they
describe. claw's AnyProvider died with its last reason to exist, ACP
no longer defaults an unknown catalog kind to google's, and adding a
provider is now adapter plus one registry row instead of three
frontend edits that had already drifted apart.
2026-11-08 17:53:04 +01:00
vhaudiquet 12182492ea refactor(config): the family's state and cache roots join Home
Two hand-rolled XDG resolutions survived the settings refactor: claw's
state_dir matched on XDG_STATE_HOME and HOME itself, and the ACP
adapter's session cache matched on XDG_CACHE_HOME itself — same class
as the global dir helpers this crate already buried. And the family
rule they implement was nowhere written down: the claw has its own
keychain namespace since the last refactor, yet its config, state, and
cache live under rhema's, with nothing saying that is deliberate.

Home carries the state and cache roots now, resolved with the config
base in discover and stated as the family rule in its doc: the
family's products share the XDG bases and nest below `rhema/` — claw
at `rhema/claw.yaml` and `rhema/claw`, the ACP session cache at
`rhema/acp-sessions`. A frontend from outside the family relocates
the whole set instead: relocated derives the frontend's name from its
config base's final component, dot stripped, and puts its state and
cache under that name, so godcode's artifacts never land under
`rhema/`. claw and the ACP adapter ask Home now; claw's NoStateHome
error variant died with its last user, and the ACP store's embedder
seam (AcpOptions::session_dir) was already there. SPEC records the
decision beside the keychain one it mirrors: the namespace is per
frontend, the nesting is per family.
2026-11-08 17:53:04 +01:00
vhaudiquet c3188af8bc refactor(config): the provider registry owns the default models
The per-provider fallback ids were spelled in three crates that had
already drifted. claw's resolved_model carried a stale match that
answered deepseek, copilot, and zai with google's fallback while its
own backends.rs knew better; acp and the headless one-shot still used
the copy-pasted google default for openrouter, which claw's module doc
records as deliberately replaced by openrouter/auto — a router
deserves its own router id. Keeping three tables honest was a
comment-level contract ("the same defaults the headless one-shot
uses"), and the comments lied.

ProviderInfo gains default_model, every row fills it, and
rhema_config::default_model(name) answers with google's default for
an unknown spelling, the same fallback every caller hand-rolled. The
acp resolve, claw's backends and resolved_model, and the headless
one-shot read the registry now; the three copies are gone. claw's
resolved_model test pins the arms that had silently dropped off.

Behavior changes, both toward the recorded decisions: the headless
openrouter fallback becomes openrouter/auto, and claw's deepseek,
copilot, and zai fallbacks become their real ids instead of google's.
2026-11-08 17:53:04 +01:00
vhaudiquet df4cf87a20 refactor(config): make the settings contract frontend-agnostic
rhema-config held both vocabularies: the stored settings and the
terminal UI's theme, palette, and onboarding state, under one Config
type with a global directory. A second frontend had to take the UI
settings to get the profiles, and every path helper resolved
$XDG_CONFIG_HOME/rhema no matter who called.

The crate now ships only the framework's contract. Settings is the
stored section, flattenable as one serde field. Home carries the
directory conventions as a value, so a frontend keeps its settings,
skills, subagents, and SYSTEM.md under its own base while
.agents/skills stays machine-global. Store is the seam a foreign
frontend implements over its own file and format, YamlStore is the
reference impl, and Platform collects home, store, and keychain
namespace at one construction site. KeyringSecretStore::named gives
each frontend its own OS keychain service, so two frontends never
share credentials by accident. examples/godcode.rs builds the whole
shape runnable: its own directory, its own JSON document, its own
namespace.

rhema-code's store::Document flattens the section beside its UI
fields, the layout on disk unchanged. serde's flatten silently drops
unknown keys, so the load path re-checks every leftover key against
the section: a typo'd profiles: stays loud inside the bigger
document. CodingAgentConfig gains home: Option<Home>, so a frontend
redirects skill discovery without calling the framework. claw and
acp name their keychain namespaces, and the mcp, coding-agent, and
subagents crates read conventions from the Home they are handed.
2026-11-08 17:53:04 +01:00
vhaudiquet d067f4632f refactor(tools): rename rhema-tools-contract to rhema-claw-tools-contract
The crate binds the claw to its client tools, not tools at large,
so it carries the claw's name: package, lib and dep names, the
mock's binary, and the conformance suite's use paths. Specs and
README follow. The tools/contract directory keeps its name, and
no reference to the old name remains.
2026-11-06 23:05:46 +01:00
vhaudiquet 3283d92227 chore: bump the workspace to 0.2.0 2026-11-06 15:32:41 +01:00
vhaudiquet 1467f98c96 docs: the phone reports are state, not conversation
The claw README claimed the phone ingest dispatches battery, network,
and DND flips to the agent; the ingest door stores topic-keyed
payloads and answers "observed", and the Android worker posts nothing
but topic keys. The README now says state-only with the bare
phone:<device> key as the dispatch path, and the worker's comment
matches what it actually posts.
2026-11-05 19:12:18 +01:00
vhaudiquet c7adc6404a fix(deploy): the image manifests stage the mcp members
The manifest-first COPY list predates the mcp crates: mcp-to-cli and
rhema-mcp had no manifest line, so the registry cache populated
without them and the source-copy build resolved their deps cold —
the cache layer the staging exists to provide never covered two
workspace members. Both Dockerfiles stage the pair.
2026-11-05 19:08:05 +01:00
vhaudiquet 5a8be19b4c fix(calendar): fed slots stay fed across a restart
The reminder source skipped an already-fed slot only while the slot
sat in the dispatcher's pending inbox; a claw restart drained that
inbox, and every future lead of the same occurrence re-fed — a
reminder fired two or three times per calendar event. The source
keeps its own fed-slot ledger in the calendar store now (keyed by
calendar, occurrence, and lead), records each slot right after a
successful ingest, and prunes rows whose occurrence is an hour past
every lead, so a pruned row can never resurrect a reminder. The
store-backed source's ledger survives a restart; the static-list
constructor keeps an in-memory one for the process lifetime, said so
in the docs that previously claimed dedup was someone else's problem.
Store failures surface loudly (a silently lost row is a re-fired
reminder); the prune warns and continues. Offline tests cover the
once-only rule, the restart via a tempdir reopen, and the prune.
2026-11-05 19:06:41 +01:00
vhaudiquet dad99c7664 fix(calendar): the ICS address parser survives a multibyte prefix
bare_address sliced trimmed[..7] after a len() >= 7 check — byte
count against a byte slice is fine, but the guard and the slice
disagree once the text is lossy-decoded elsewhere; an ATTENDEE line
whose value opens with a multibyte run panicked the reminder source
on a feed the operator does not control. The slice is get(..7) now,
and a test feeds multibyte prefixes through both the ATTENDEE and
ORGANIZER paths.
2026-11-05 19:05:16 +01:00
vhaudiquet 00adf092ff fix(forge,claw): forge keys separate the issue from the pull
The watch key encoded source, repo, and number but not the thread
kind, and on GitLab an issue and a merge request share one number
space: iid 7 twice produced one key, one snapshot, and comments on
the merge request landing in the issue (or a 404 fallback that
misrouted the other way). The key carries the kind now —
{source}:{kind}:{owner}/{name}#{number}, with GitLab spelling the
pull variant mr — comment and snapshot route outright by kind, and a
kind-less payload parses to the unparsable key instead of guessing
issue. The claw's callers thread the kind from the payload and strip
the segment where the bare repo is wanted; pre-kind stored keys parse
as None rather than misroute. Four route-pinning wiremock tests cover
the GitLab split, and the GitHub/Gitea clients take the kind without
using it (their issues and PRs share the same REST space).
2026-11-05 19:03:52 +01:00
vhaudiquet 10efe85f38 feat: the see macro turns a command's output into images
There is no native tool for this, and images are shell data like any
other, so the surface is a bash function the model pipes into:
curl -sL https://example.com/photo.png | see, or see path.png. The
name reads as a sentence in the pipe position, keeps working as a
plain argument for local files, and is one const in vision.rs if it
ever needs to change.

Two halves, joined by a staging directory. Provision writes the
BASH_ENV init file one non-interactive bash sources; see() execs the
rhema binary's hidden __vision mode, which sniffs magic bytes, stages
them under a 0700 per-process dir, and says so on stdout. 5 MB per
image (the strictest API ceiling), png/jpeg/gif/webp only, and a
refusal exits 2 with the reason on stderr so the model reads the
limit in-band rather than losing the bytes to a silent drop. Attach
runs after every bash command that reaches an exit: ShellTools drains
the staging dir into ToolResult.images, a new core field. Bytes never
ride stdout, so output caps and truncation cannot eat them, and a
staged image attaches even when the command failed. The model staged
it, so it belongs in the context. The caps: 5 MB staged, 8 images per
result, the rest named in the result's text, because a for loop
staging 30 screenshots must not cost a context window.

serde keeps the field default, so old checkpoints load unchanged.
Anthropic carries native image blocks in the tool_result content
array, text first. OpenAI-dialect tool messages (llama.cpp, Copilot,
OpenRouter, DeepSeek) and Gemini carry text only, so each withheld
image is named in a trailing note the model can act on. The
transcript's byte accounting and summaries count the images; the
coding-agent config carries the prompt section, and the render adds
the section only when wired.

Frontends provision only where their bash runs locally: the TUI's
session_agent_config, the headless one-shot, the ACP adapter, the
claw's daemon, the same gate the MCP wire applies, because a session
whose bash runs elsewhere cannot write where the drain reads. The
vision wiring lands before the MCP wire so BASH_ENV names the MCP
file last and its baked predecessor line reaches the vision init;
both functions end up defined. Subagent inner shells inherit the
parent's wiring, so a spawned agent keeps the macro in its own
commands. SPEC's decision bullet, AGENTS.md's scope line, and the
README row land with the feature.
2026-11-05 18:52:37 +01:00
vhaudiquet 99ecceacb5 feat(claw): the daemon wires MCP into its agents
The claw's side of §10: one bridge per process, established at first
agent build and never re-read — the claw is single-instance and its
servers are stateful across calls, so a mid-process rewire would
orphan running sessions' socket paths. The global `mcp.yaml` is the
claw's config scope (no profiles here); the off switch is
`disabled_mcp_servers` beside the rest of claw.yaml, and both agent
construction sites — chat turns and `coding_agent` jobs — layer the
wiring on: the init file and socket and token into `shell_env`, the
post-filter lines into the prompt's `mcp` section, the persisted set
into the config. A frontend without wiring changes nothing, so the
daemon without `mcp.yaml` behaves exactly as before.

The test configs gain the new field; the fd-limit discoveries from
the workspace run are already handled by the harness runs above.
2026-11-04 23:13:12 +01:00
vhaudiquet d5328667e4 feat(code,claw): the rhema mcp route, frontend wiring, disabled persistence
The spec's §10 consumer steps land. The rhema binary routes `mcp`
and everything unrecognized (the external subcommand, which is the
road the init file's stubs take: `rhema __server <name> ...`) to
rhema_mcp::cli::run, whose exit code owns the process; outside a
wired shell it exits 4 silently, exactly the spec's contract. The
claw gains its first subcommand the same way (`claw mcp ...`).

The frontends wire: session_agent_config (the TUI's three call
sites) and the headless one-shot merge the wiring's shell_env into
the agent config and the lines into the prompt's mcp section --
always behind the sandbox gate, so a session whose bash runs
elsewhere advertises nothing it cannot run. The registry keys by
profile name: a profile switch rewires, staying put reuses the
bridge and its already-spawned servers. The bridge itself moved to
a dedicated thread with its own runtime in this pass, which is what
makes wire() synchronous -- a dedicated runtime dropped at return
took the accept loop with it the first time, and a blocking CLI read
starving a current-thread runtime taught the same lesson from the
other side.

Muting persists: disabled_mcp_servers lands beside skills_disabled
in the config file, the settings toggle writes both the live set and
the config through save_config, and the running agent rebuilds so
the very next turn sees the change -- the MCP twin of the skill
toggle, with the round-trip test extended to assert the config.

The process-level pin the spec's §9 asked for: a bash child sources
the provisioned init file from BASH_ENV, calls the generated
function, the stub execs the CLI bin (the same dispatch the rhema
binary routes to), and the envelope prints -- plus the unknown-tool
path exiting 2 through clap's own text. provision and wire grow
provision_with_bin/wire_with_bin test seams so the exec chain can
run without building the frontend; the standalone rhema-mcp-cli
bin target is the seam's exec side.
2026-11-04 22:58:55 +01:00
vhaudiquet abcafccc8e refactor: remove rhema-vercel-agent
The Vercel frontend existed to prove the shared harness extraction:
two frontends speaking the same wire protocol, one assembly, one
gate. The proof landed; the deployment didn't, and the crate cost a
full axum server, a Postgres store, and a Sandbox REST client to
keep carrying. It leaves the tree; git history keeps it for the day
a second frontend returns.

The Cloudflare DO is now the harness's one consumer, and nothing
downstream depended on the crate. README, AGENTS.md, and the two
dockerfiles lose their vercel entries; the cloud-agent and
cloudflare-code doc comments name the remaining frontend. SPEC.md
section 11 still tells the two-frontend story; it stays until the
direction settles.
2026-11-03 22:31:45 +01:00
vhaudiquet 6e641102c6 feat(rhema-provider): the retry ladder follows the failure
The old ladder doubled from one second and capped at two: three
attempts, three seconds — enough for a hiccup, nothing for a
reassociation or a real outage. The policy now takes two explicit
ladders and picks by error class. A dead link (Network) is the local
machine's problem and waiting can fix it, so it walks 1s, 3s, 30s,
60s, 300s — six attempts, six and a half minutes all told. A
struggling server (Timeout, 408, 5xx, an exchange that closed before
any content) fixes itself in seconds or not at all inside a turn,
and every timeout attempt also burns the request's own timeout, so
its ladder stays at 1s and 2s. Each ladder's length sets its attempt
count, so the policy can no longer disagree with itself.

Each wait is announced as an Event::RetryWait (error, delay,
attempt, total) before it runs. rhema-code carries it as a deadline
and the status strip counts it down — the error in the alarm color,
the attempt, the seconds left — while any event from the next
attempt clears it. Esc still cancels mid-wait: the executor drops
the future at the sleep. ACP, claw, subagents, and the cloud wire
ignore the event; from_event returns Option now, so a frameless
event sends no frame. The hosted frontends keep the short ladder on
both classes: their invocation outlives no five-minute wait.
2026-10-31 18:56:13 +01:00
vhaudiquet 951430e44b build(deploy): the docker images copy the ecrou manifest
The workspace manifest globs crates/*, so cargo fetch parses every
member's Cargo.toml; rhema-sandbox-ecrou joined the tree without its
manifest joining the COPY list, and both image builds died at manifest
parse ("failed to read /build/crates/rhema-sandbox-ecrou/Cargo.toml").
Add the line to claw.Dockerfile and sandboxd.Dockerfile, in the
alphabetical position the list keeps.
2026-10-29 18:12:12 -05:00
vhaudiquet 06c03724c8 fix(deploy): the image manifest stages know the new workspace members
The images resolve the whole workspace graph before cargo fetch, so
every member manifest has to be in the build context by then, and
the copied list had fallen six crates behind the workspace: the
backend split added sandbox-local, sandbox-lxd, and sandbox-server,
and the cloud frontends added cloud-agent, cloudflare-code, and
vercel-agent. rhema-code path-depends on the sandbox backends, so
the claw image was the first to break — the fetch died reading the
lxd manifest.
2026-10-27 22:30:01 -05:00
vhaudiquet e92739aa1b chore: collapse the ifs the 1.98 clippy flags
The pinned toolchain's clippy grew opinions the older lint let stand:
nested if/if-let pairs it wants as let-chains, and a modulo test it
wants as is_multiple_of. Machine-applied collapses, fmt-settled, no
behavior change — the gauntlet runs clean again.
2026-10-27 01:12:46 -05:00
vhaudiquet 50e0a3b949 chore(claw): shorten the bootstrap_save manifest's wording
The soul/user explanation leaves the stored description; the kind
enum in the parameters already carries the distinction.
2026-10-26 22:21:45 -05:00
vhaudiquet 43d4c86d5e feat(android): attach photos and camera shots
The (+) door stages gallery picks and camera shots as removable
chips against the daemon's own budget (eight a message, 5 MiB
decoded each, refusals surfaced, never silent drops); send reads
the files back as bare base64, the old {"text"} body unchanged
when there are none. Sent rows preview their staged files at once
and a replay fetches by attachment id like any attachment card.
Model options carry the vision badge only when the listing knows.
2026-10-26 23:34:41 -05:00
vhaudiquet 6efb69331d feat(web): attach images from the composer
The (+) door and the clipboard door both stage files as removable
chips, the claw's own budget enforced client-side (eight a message,
5 MiB decoded each, errors surfaced, never silent drops). The sent
row renders local previews at once; the stream's user_message echo
carries the stored ids and dedupes against it, so a replayed message
renders its thumbnails through the same bearer-guarded fetch as an
attachment card. Model options mark vision when the listing knows.
2026-10-26 23:24:41 -05:00
vhaudiquet cfb349508f feat(claw): serve the vision flag on model listings
The models route's cards serialize as rhema-provider's ModelCard
spells them, so supports_vision rides exactly when the adapter knows
it and is absent when it does not — unknown stays absent, never
false. The route's doc now pins that contract, and a listing builder
on Backend lets the surface tests mount one offline.
2026-10-26 23:13:41 -05:00
vhaudiquet 23b13f41b8 feat(claw): pass image parts through the OpenAI shim
The shim's last user message now cuts the image_url parts along with
the text: an inline data URI with a parseable mime rides to the turn
as an image block, a remote http(s) url and an unknown mime are
skipped, and the messages route's image budget applies. Text-only
requests are unchanged.
2026-10-26 23:03:41 -05:00
vhaudiquet 1323e69dc4 feat(claw): image attachments on chat messages
A chat message may carry inline images: the messages route checks the
budget (5 MiB decoded per image, eight a message) and the dispatch
copies the bytes into the attachment store, so the replay streams ids
and media types over SSE, never megabytes. The turn's input carries
the inline image blocks the providers take, base64 verbatim — the
session checkpoint holds what the model saw.
2026-10-26 22:52:41 -05:00
vhaudiquet 5405e3c5b0 feat(message): the image content block
Providers map images to per-backend content blocks, so the block rides
the message seam beside the tool blocks and round-trips through session
checkpoints. Untagged serde keeps old payloads loading; the new variant
sits last so the field sets disambiguate. Exhaustive matches on Content
across the adapters and views gained a mechanical Image arm; sending
images is a later effort.
2026-10-26 20:44:05 -05:00
vhaudiquet 18d58ea8a4 fix(ci): the wasm check and the docker fetch stages pass again
privacy.rs bound the opened handle in a name only the unix build
used, so `-D warnings` failed the wasm target; the bind is now
`_file` and the unix branch uses it.

The docker images resolve the whole workspace manifest graph before
`cargo fetch`, so they now copy the tools contract with its workspace
root plus the sandbox manifests rhema-code path-depends on, and the
stub loop covers tools/*/ too.
2026-10-25 19:22:11 +02:00
vhaudiquet 5e9952ac6d fix(android): a worker crash lands in the log instead of dying silent
The diagnostics page showed the status job "waiting" while its runs
and the report-now kick died without a trace: any exception before the
first log line was invisible, indistinguishable from a job that never
starts. Every worker now wraps its run — a crash logs its type, message,
and first frame; a cancellation logs as such and rethrows. The screen
gains the report-now kick's own row (a one-time job's empty list is
"never run", not an error) and the tap lands in the log, so the next
stale-row mystery names its cause instead of hiding it.
2026-10-25 19:18:13 +02:00
vhaudiquet 7225dfddd3 feat(android): a diagnostics page answers why the fleet row is stale
A stale "last seen" had no client-side answer: a dead worker, a down
claw, and a claw that drops writes all looked identical from the
settings screen. The new page separates them: the WorkManager jobs'
live state (a cancelled schedule renders as "not scheduled"), the two
clocks — the phone's last landed report against the claw's last stamp
— whose disagreement names the broken end, auto-detected system checks
(battery optimization, background restriction, notifications) with
jumps into the exact settings page, a report-now kick, and a 200-line
in-memory flight recorder where every worker run logs its outcome.
2026-10-25 10:28:38 +02:00
vhaudiquet e4f4a3a10b chore: enforce missing_docs from the workspace manifest
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.
2026-10-25 07:02:48 +02:00
vhaudiquet 15d3478429 fix(calendar): the source constructor reports CalendarError 2026-10-25 07:02:48 +02:00
vhaudiquet 5eef154fcb refactor(claw): typed errors at the store and tool seams
BackendError and WatchShapeError replace the String registry and
watch seams, the tool operations report ToolError, the accessors
drop the get_ prefix, the too-many-arguments allows become
expects, and the llama catalog passes through unchanged.
2026-10-25 07:02:48 +02:00
vhaudiquet 90212d1e0f refactor(claw): the forge kind is the integration's — config.rs carries no forge code
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.
2026-10-24 16:48:18 +02:00
vhaudiquet 5047ff8803 refactor(sandbox): one crate per backend
The seam crate carried every backend behind features — local, client,
server — so a wasm worker linked the axum daemon and a host daemon
linked reqwest it never dialed. The family mirrors the provider family
now, as the design always said it would: rhema-sandbox keeps the
contract, the ShellSandbox adapter, the wire protocol, the seeding
helper, the scripted mock, and — behind the new `spawn` feature — the
process family's shared machinery (exec_util, fsroot); the backends
are sibling crates, rhema-sandbox-local, -docker, -ssh, each dependent
only on the seam; the HTTPS client is rhema-sandbox-http (reqwest
behind its `native` feature, off on wasm, whose worker brings its own
HttpTransport over host fetch); the daemon is rhema-sandbox-server,
and the rhema-sandboxd binary moves with it.

Consumers rewire, semantics don't: rhema-code names its backends
directly; rhema-cloud-agent keeps its transitional re-exports;
rhema-cloudflare-code and rhema-vercel-agent take the HTTP client
reqwest-free and native respectively; the sandboxd image builds
-p rhema-sandbox-server. Cargo paths only — no behavior, no protocol,
no spec change.
2026-10-24 12:38:47 +02:00
vhaudiquet a9329ce6bd chore(claw): keep the live usage tally off the interactive wire
The running tally is gauge material, not transcript; the claw's
TurnEvent stream stays as it was.
2026-10-24 04:04:48 +02:00
vhaudiquet db75f7c4b8 feat(tool): a streaming resolver reports its own status
The streaming progress seam grows from a bare output callback to
ToolProgress: output chunks as before, plus an optional ToolStatus —
the phase a tool's inner work is in and, when the tool runs model
calls of its own, their cumulative spend and the model they ran on.
The engine stamps the call's id on it and the sink receives it as an
ephemeral ToolStatus event, replacing any earlier one for the call;
status is screen-facing, never history.

The subagent tool is the first emitter: its inner turn's lifecycle
becomes phase transitions (thinking, running, writing output) and a
running token total. Every sink decides what a status is worth: the
cloud wire forwards it, ACP and the claw have no slot and drop it.
2026-10-24 03:58:14 +02:00
vhaudiquet 7f6c13f608 chore: apply cargo fmt 2026-10-24 02:02:39 +02:00
vhaudiquet aa9a68ab6d feat(contract): /v1/calendars joins the tool contract, one allowlist for both doors
The calendar table gets the same contract treatment as every other
data domain: a named route constant, three allowlist rows (GET, POST,
DELETE /v1/calendars/{name}), mock-router arms that enforce the
door's loudest rules — a named calendar, an ics with a URL — and
conformance cases (add, the invalid refused, listed, removed) that
run against the mock and the real daemon alike, so the two cannot
drift. The claw's auth middleware reads the same list, which is how
the rows land there with no claw-side code.

The spec names the doors and the pattern they establish: domains the
daemon reconfigures at runtime live in tables behind their routes,
validation runs at the door, and claw.yaml carries only what boot
needs.
2026-10-24 01:58:09 +02:00
vhaudiquet 818079d1f4 refactor(claw): the calendar table is the one truth — live, not config
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.
2026-10-24 01:52:09 +02:00
vhaudiquet c6a3ed8c97 refactor(claw): the watch table is the one truth — the config stops carrying watches
The YAML `watches:` seed was the last piece of forge state the config
file carried. Everything else about watches already lived in the
extension: the table (SQLite, runtime-managed), the HTTP routes
(`GET/POST/DELETE /v1/watches`), the chat tools, the poller that reads
the table fresh every pass. A static seed list on top meant two homes
for one fact — and the seed's own semantics (import-once, ignored
once the table has rows) had to be explained to every reader.

Now the config carries none of it:

- `ClawConfig.watches` is gone, with the `Watch` seed type and the
  boot-time validation loop that guarded it. A config still carrying
  `watches:` is refused at parse time — `deny_unknown_fields` turns
  the retirement into a loud migration, not a silent no-op.
- `Watch` moved to `watch_store` (re-exported from the crate root),
  where the row's vocabulary belongs; `validate_watch` keeps guarding
  the one door every writer shares — the routes and the tools both
  call it, so the moved validation tests test it there.
- `seed_from_config` and the daemon's seeding are gone; the operator
  seeds once over HTTP or `watches add`, and the table stays true.

The one guarantee the seed carried that nothing else did — boot
refuses a tokenless watch — goes with it, deliberately: a tokenless
watch now fails at its first poll, loudly, like any other watch the
agent adds at runtime. Boot-time certainty was paying for config
state the daemon no longer owns.
2026-10-24 01:35:35 +02:00
vhaudiquet f96f8c4c1d feat(tools): the ingest credential — the event door, write-only
The agent token is deliberately barred from /v1/ingest: the events
are the inbound channel, and the model must not feed it itself. But
the same bar closes the door on every honest integration too — a
sidecar poller with events to hand in had to hold the human token,
which is wider than its job. This adds the missing class: a third
bearer, the ingest token, answering the event door and nothing else.

`ingest_allowed` is the whole credential in one function — one
route row, `POST /v1/ingest` — symmetric with `agent_allowed` and
suite-covered the same way: the mock and the daemon's real router
both answer the new checks (a push succeeds with the contract header;
a watch read and a consent answer are 403). The middleware classifier
gains the third arm, boot refuses an ingest token equal to either
other credential — one value playing two roles silently widens the
scoped one — and the daemon provisions it to tool shells as
`RHEMA_INGEST_TOKEN`, so a tool that only pushes events never sees
the wider agent token at all.

The door is the first half of the extensions story (rhema-tools spec
§8): a self-timed sidecar is now a tier-1 tool with a credential
narrow enough to hold.
2026-10-24 01:17:43 +02:00
vhaudiquet 98bc0fd623 feat(claw): the scoped agent token and the client-tool routes
The claw is the server half of the client tools. The middleware now
knows two bearer classes: the human token answers every /v1 route as
before; the agent token answers only the routes the contract
allowlists, and only with the contract header at the matching version
— consents, sessions, ingest, and the phones answer it 403, so a
model holding the token cannot prompt-inject its way past the scoping
to answer its own secret asks. Boot refuses the configuration where
agent_token is the human token: the scoping is the safety, and one
value playing both roles silently unscopes everything the shell
holds.

Three new routes write the tables the resident already reads: push to
the notification queue, schedule and cancel tasks the resident's tick
fires — the tools get native-shaped writes without the daemon growing
a second brain. A JSON fallback gives the one error shape everywhere,
unknown routes included. The daemon composes the tools' environment
(RHEMA_URL, RHEMA_TOKEN, the tools' bin dir on PATH) and hands it to
the resident's shell through the agent-tools env layer; with no
agent_token it provisions nothing, exactly as before.

The proof is the contract's conformance suite run against this router
over real HTTP — same answers as the mock, checked at test time.
2026-10-24 01:02:28 +02:00
vhaudiquet 2fbd93fd79 feat(tools): notify — the first client tool
Desktop notifications are the wave-1 built-in the resident has no
native way to send: the daemon's pushes land in the poll queue, and
this is the program that turns one into an OS notification —
`notify push --title --body [--device]` on the shared plumbing, the
claw's `POST /v1/notifications` on the far end.

The binary is the template for every later client: connection from
the environment the daemon provisions, one contract-headered call,
JSON or error shape, exit code. The e2e test drives the real binary
against a mock claw — a wrong token is a 401 with the code to match,
a consent route is a 403, a good push prints the row — so a packaging
regression cannot pass silently. `skills/` ships the markdown that
teaches the agent the tool exists and what its answers mean.
2026-10-24 01:02:28 +02:00
vhaudiquet 29099add9d feat(tools): the shared client plumbing
A client tool is three moves: read the connection from the
environment, make one HTTP call carrying the bearer and the contract
header, and print either the answer as one JSON line or the one error
shape — then exit with the code that says which happened. Common is
those three moves, so no client reimplements them and all clients
fail identically: `Config::from_env` (`RHEMA_URL`, `RHEMA_TOKEN`),
the `Client` call with the contract envelope, `print`/`fail` and
the `ToolError` exit codes.

The HTTP client is `ureq`, blocking, no TLS: the contract speaks to
a loopback or otherwise trusted daemon, and a remote TLS endpoint is
a stated non-goal in v1 — a thin client stays thin.
2026-10-24 01:02:28 +02:00
vhaudiquet 75585f2dd3 feat(tools): the contract crate — allowlist, header, conformance suite
The client tools need one shared definition of what a tool may do,
and the daemon needs to enforce exactly that definition, not a
hand-copied copy of it. This crate is that definition, pure: no HTTP
client, no process — the allowlist as data, the version header as
constants, the request/response and error types, and the conformance
suite that drives any transport through the checks the contract
promises (the bearer gate, the version handshake, the allowlist's
denials, round-trips through every allowed route, one error shape).

`agent_allowed` is the whole security surface in one function:
method and path segments with `*` and `**`, exact otherwise — a
daemon's middleware asks it and nothing else, so the claw's scoping
and the tools' expectations cannot drift apart. The suite runs
against a mock router in this crate's tests and against the daemon's
real axum router in the claw's; same results is the definition of
implementing the contract.
2026-10-24 01:02:28 +02:00
vhaudiquet fb05983b05 feat(deploy): the rhema-sandboxd image
A multi-stage build shipping the daemon: bash, git, and a CA store in
the runtime so git and tarball seeds work; the base directory is a
volume, the token is a flag with no env default, the port must be
passed because the binary defaults to an OS pick. Smoke-tested against
a real daemon: healthz auth, exec, put/get, session dirs under the
mounted base.
2026-10-24 07:13:33 +02:00
vhaudiquet 0e5a6c5658 feat(web): interleave thinking blocks with tool calls
Fold thinking deltas into the turn's stream-ordered entries as a
thinking entry, merged back-to-back the way the stream delivers
them, instead of one flat string rendered above every call. Each
thinking block now sits where it streamed — between the calls it
precedes. Live turns keep their thinking expanded; finished turns
render it collapsed.
2026-10-22 20:44:15 +02:00
vhaudiquet 3cab0efd39 feat(claw): move the phone tool spec into a TOOL.yaml manifest
phone loads its spec from an embedded manifest under
crates/rhema-claw/tools; the Rust side keeps only the invoke half.
It stays replay-unsafe: a resumed run must not re-queue a control
command or re-retire a device. With this every registry-served tool
spec in the daemon comes from a manifest; only the ToolResolver-style
tools (identity_exec) keep their specs inline.
2026-10-20 23:21:28 +02:00
vhaudiquet c1a06d1747 feat(claw): move the coding_agent tool spec into a TOOL.yaml manifest
coding_agent loads its spec from an embedded manifest under
crates/rhema-claw/tools; the Rust side keeps only the invoke half. It
stays replay-unsafe: a resumed run must not re-enqueue a coding job
against an instance that is already running it.
2026-10-20 22:56:50 +02:00
vhaudiquet 9bf9d61573 feat(claw): move the ask_secret tool spec into a TOOL.yaml manifest
ask_secret loads its spec from an embedded manifest under
crates/rhema-claw/tools; the Rust side keeps only the invoke half. It
stays replay-unsafe: a resumed run must not re-prompt the human or
re-store the credential the first ask already landed.
2026-10-20 22:32:51 +02:00
vhaudiquet 1df2718032 feat(claw): move the attach_file tool spec into a TOOL.yaml manifest
attach_file loads its spec from an embedded manifest under
crates/rhema-claw/tools; the Rust side keeps only the invoke half. It
stays replay-unsafe: a resumed run must not re-attach a file the
transcript already carries a card for.
2026-10-20 22:08:49 +02:00
vhaudiquet 162cb21d7c feat(claw): move the identity_set tool spec into a TOOL.yaml manifest
identity_set loads its spec from an embedded manifest under
crates/rhema-claw/tools; the Rust side keeps only the invoke half. It
stays replay-unsafe - a resumed run must not re-write the identity.
2026-10-20 21:44:04 +02:00
vhaudiquet 2747018b8f feat(claw): move the identity store tool specs into TOOL.yaml manifests
identity_save, identity_list, and identity_forget load their specs
from embedded manifests under crates/rhema-claw/tools; the Rust side
keeps only the invoke half. identity_list declares replay_safety:
safe - the store answers with its own redacted records - while save
and forget stay unsafe: a resumed run must not re-store an account or
drop one twice. identity_exec keeps its spec inline; it is a
ToolResolver, the exec seam itself, not a registry tool.
2026-10-20 21:19:54 +02:00
vhaudiquet da483daadd fix(android): let the schedule carry a failed report, not backoff
A failed post returns retry(), which moves the next run onto
WorkManager's exponential backoff — 30s doubling, capped at five hours.
A couple of failures while the claw is unreachable is enough to leave
the fleet without a report for most of a day, which reads exactly like
the phone being gone.

The snapshot is only saved when every post landed, so the next
scheduled run re-derives the whole transition set and posts it again.
The fifteen-minute schedule is the retry; return success and keep it.
2026-10-20 07:40:43 +02:00
vhaudiquet 385788c935 fix(android): refresh the phone's own row when it reports
The fleet card reads last-seen from the claw, and only a landed report
moves it. Periodic work is deferred while the device idles — Doze
stretches the fifteen-minute floor to hours — so a phone sitting in the
human's hand could show itself as "last seen 3h ago" with nothing wrong
on the claw.

Kick a status run on resume, the same way the commands worker already
is, and reload the fleet whenever a run lands. The landed-run signal is
a new clock in StatusRepository, written in the same edit that saves the
snapshot (which only happens once every post of the run succeeded), so
the card reloads on a report that actually reached the claw.
2026-10-20 07:40:43 +02:00
vhaudiquet 4a91635730 feat(claw): move the notify tool spec into a TOOL.yaml manifest
notify loads its spec from an embedded manifest under
crates/rhema-claw/tools; the Rust side keeps only the invoke half.
The drift test pins the shipped set: the manifest loads against its
builtin, the registry serves the manifest spec, and notify stays
replay-unsafe - a resumed run must not re-send a notification.
2026-10-20 19:30:33 +02:00
vhaudiquet c1779c2423 feat(claw): move the watch tool specs into TOOL.yaml manifests
watch_repo, unwatch_repo, and list_watches load their specs from
embedded manifests under crates/rhema-claw/tools; the Rust side keeps
only the invoke half. The manifests stay name-sorted, which is the
order the registry serves and the roster shows. list_watches declares
replay_safety: safe - re-reading the table on a resumed run is free -
and the drift tests pin the shipped set: every manifest loads against
its builtin, every builtin is named by a manifest, and replay safety
reaches the resolver from the file.
2026-10-20 19:06:33 +02:00
vhaudiquet 8c14ea7749 feat(claw): move the bootstrap tool spec into a TOOL.yaml manifest
bootstrap_save now loads its spec from an embedded manifest, one
folder under crates/rhema-claw/tools; the Rust side keeps only the
invoke half, behind the run.builtin id. Drift tests follow in the
registry's own suite shape once the family grows; for now the
registry asserts the manifest loads, and the render tests exercise
the manifest-served spec through the roster.
2026-10-20 18:41:48 +02:00
vhaudiquet bcadeb193c fix(claw): follow the onboarding section rename
The section-header restructure renamed the onboarding offer from
"## Setup" to "## Onboarding" in ONBOARDING.md; three prompt
assertions still looked for the old heading and failed. They follow
the file now.
2026-10-20 18:17:09 +02:00
vhaudiquet 4d5dc3f677 fix(deploy): copy the rhema-transcript manifest into the claw build
The crate was missing from the per-member manifest COPY list, so the
workspace failed to resolve in the registry-cache layer: rhema-claw
path-depends on rhema-transcript, whose Cargo.toml never landed in
the context.
2026-10-20 08:25:20 +02:00
vhaudiquet c536952ba2 refactor(claw): structure the system prompt with section headers
The Soul, User, Environment, Tools, and Memory blocks get headings,
and the watch/notify/schedule prose moves to the TOOL.yaml manifests.
2026-10-20 08:25:12 +02:00
vhaudiquet 4692c8cddc feat(claw): move the task tool specs into TOOL.yaml manifests
schedule_task, list_tasks, and cancel_task load their specs from
embedded manifests, one folder per tool under crates/rhema-claw/tools;
the Rust side keeps only the invoke half. list_tasks declares
replay_safety: safe, so a resumed run may re-execute it. Drift tests
pin the shipped set: every manifest loads against its builtin, every
builtin is named by a manifest, and the registry serves the manifest
specs verbatim.
2026-10-20 06:42:59 +02:00
vhaudiquet 60f3b5eb9b feat(agent): the transcript on by default
First landed flag-gated: the mechanics were unproven at coding-traffic
scale and the view changes what the model sees past the budget. The
claw then ran every surface on it and nothing fell over, the only user
is the deployment's own, and the sessions most likely to hit the
budget were the ones least likely to find a flag. Below the budget the
decorator is a pass-through, so the default's only always-on cost is
the zoom spec in the prompt.

transcript: None is the off switch; rhema-code spells it
--no-transcript, the claw's transcript: false now writes the resolved
value so false still means off. Saved sessions view fine: the log
stays complete and the view derives per request. The system preview
names zoom too, so what is previewed is what turns send.
2026-10-20 06:01:24 +02:00
vhaudiquet 50b8feba07 feat(claw): run the bounded request view by default
The claw's sessions never end by design — one interactive chat, forge
threads alive as long as their pull requests, coding jobs stashed
across runs — so off-by-default would mean the view is never on where
it matters most. On: chat turns wrap the provider seam with the view
and join zoom to the resolver chain, the mirror living on the handler
so it survives the per-dispatch agent rebuild; forge threads and
coding jobs set the coding agent's transcript field. The config turns
it off with transcript: false.
2026-10-20 06:01:24 +02:00
vhaudiquet b70a56c325 refactor(calendar): move the reminder source beside the feeds
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.
2026-10-20 05:53:43 +02:00
vhaudiquet fc0cbbcb6b refactor(forge): classify account notifications beside the vocabulary
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.
2026-10-20 05:49:14 +02:00
vhaudiquet 5fdd17b901 refactor(claw): assemble the daemon from an extension set
The daemon named every feature twice: once in the interactive
handler's sixteen-argument constructor, once across the source list,
the tick, and the boot pass. Both assemblies are now loops over one
ClawExtension trait, so an integration is a struct plus a line in
the standard set. The chain order, the detect order, and the boot
order are unchanged; the surface suite pins them. Each extension
struct is the boundary a later move into the integration crates
would lift wholesale.
2026-10-20 05:39:57 +02:00
vhaudiquet 8ab513d49f fix(claw): list the subagents manifest in the claw image
rhema-code grew a path dependency on the new rhema-subagents crate, but
the image's manifest-only staging layer never copied its Cargo.toml, so
workspace resolution died before cargo fetch could run.
2026-10-19 23:14:31 +02:00
vhaudiquet cb30ba9469 style(claw): rustfmt the watch-credential change
The credential-resolution commit landed with two violations this
editor's format-on-save caught — an unsorted import in the handler and
wrapping in the daemon and source — reformatted here so the tree is
cargo-fmt clean.
2026-10-19 20:14:07 +02:00
vhaudiquet 1cdc2e8648 feat(claw): derive a blank watch credential from the matching account
A repo watch row carries no account link: its only credentials were the
token stored on the row and the forge's environment variable, so a
watch added without either polled — and replied — anonymously. Against
a private repository that is a 404 on every pass, and nothing ever
lands: no dispatch, no reply, only a per-pass warning in the logs.

The watch credential now resolves in three tiers: the row's token,
then the env var, then the token-kind account identity of the same
forge — instance-scoped on a self-hosted forge (the gitea watch of
git.example borrows the gitea account saved with that base URL). A
password-kind account is skipped, never borrowed. ForgeSource and the
ClawHandler resolve through the same chain and key their client caches
on the resolved value, so the reply authenticates as the identity the
poll did — and a rotation or a later-saved account is a new client on
the next pass, no restart.
2026-10-19 19:44:39 +02:00
vhaudiquet 7c707e7a09 feat(claw): list account watches beside repo watches on the surface
A saved account whose forge notifications are not actually followed was
invisible: the save's watch flag defaults to false on the tool and the
HTTP route, and GET /v1/watches listed only the repo-watch table. The
first question a silent forge thread raises — is my account even
subscribed? — was unanswerable from outside. The route now answers it.
2026-10-19 19:40:59 +02:00
vhaudiquet 04549a7df5 fix(claw): dispatch reason-less forge notifications to the thread's agent
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.
2026-10-19 19:40:44 +02:00
vhaudiquet 81da26b955 feat(rhema-code): add the colors setting with four palettes
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.
2026-10-17 04:28:31 +08:00