Skip to content

Anvil v2.2.21

Choose a tag to compare

@culpur culpur released this 14 Jun 21:43

Anvil v2.2.21 — Durable Daemon Sessions, Self-Building MCP, Multi-Repo Workspaces, Journal, and the Big Modularization

Released: TBD (release authorization pending) — notes current through 4f7d045 (#922)

v2.2.21 is the largest single-release reshape Anvil has had since v2.0. Five new feature surfaces land alongside a complete session-execution migration into anvild (the TUI is now a viewer over a Unix socket), a P0 security fix for binary downgrade prevention, and the largest modularization pass in project history — ~30 multi-thousand-line files split into ~100+ focused sub-modules across every crate in the workspace.

This release stops Anvil from being a TUI that happens to write some background files and starts it being a daemon that hosts sessions which a TUI viewer attaches to. Close the TUI; the session keeps running. Reopen anywhere — terminal, AnvilHub web viewer — and reattach. Crash the daemon; restart it; resume from the journal. That's the v2.2.21 product story.

Durable Daemon Sessions — Session-in-Daemon Arc (D.1–D.4)

The biggest architectural shift: session execution now lives in anvild, not in the TUI process. Four phases land together.

D.1 — Session protocol contract (#841)

crates/anvild/src/session_proto.rs defines the on-wire types. SessionRequest covers Start | Attach | Detach | Input | Interrupt | Kill | Resume | ReleaseInput | List | Status. SessionEvent covers Started | Attached | Detached | PeerAttached | PeerDetached | TextDelta | AssistantMessage | UserMessage | ToolCall | ToolResult | CompactingStatus | Ended | Error | SessionList | SessionStatus.

Wire format: length-prefixed JSON, 1 MiB max frame, over the daemon's existing Unix socket at ~/.anvil/run/routined.sock. Per-session monotonic seq numbers let attached viewers dedupe and reorder on reconnect. 38 unit tests cover every variant round-trip plus framing edge cases.

D.2 — SessionActor in anvild (#842)

crates/anvild/src/session/ adds the runtime that owns the turn loop. Each session is a tokio task wrapping a SessionActor: input arrives over an mpsc, output broadcasts over tokio::sync::broadcast. The turn loop calls the existing runtime::providers::ApiClient trait, dispatches tool calls (Bash / Read / Write / Edit / Grep wired end-to-end; LS/Glob/WebFetch/WebSearch/MCP_*/Notebook etc. return explicit ToolResultPayload::Err { "not yet wired in daemon-mode" } so callers see structured "not yet" rather than silent skip), writes journal records, and stamps workspace_id for active workspaces.

14 unit + 2 integration tests cover spawn → input → tool call → kill, interrupt cancellation, and journal-record-per-tool-call.

D.3 — TUI as viewer (#843)

crates/anvil-cli/src/session_client/ repoints the TUI from "owns a LiveCli that runs the turn loop" to "owns a SessionClient that talks to a SessionActor." The existing TUI render path is unchanged — SessionEvents fan in via session_client/bridge.rs and translate to the existing TuiEvent enum.

Three modes:

  • anvil (default) — try daemon, fall back to in-process LiveCli if no socket. Safe default for v2.2.21 introduction.
  • anvil --daemon — require daemon mode; error if no socket. Power-user opt-in.
  • anvil --no-daemon — force in-process (preserves the v2.2.20 behavior for users with broken anvild).

17 unit + 2 integration tests cover length-prefixed frame round-trip, state replay, bridge translation, and the "daemon returns stub error → fallback path" code path.

D.4 — Multi-client attach + crash recovery (#844)

Event ring buffer: every session keeps a 512-event ring alongside the broadcast channel. Late-attaching clients receive the full ring replay before subscribing to live events, so reconnects don't drop history.

Peer awareness: SessionEvent::PeerAttached { client_id } and PeerDetached { client_id } fire when other clients join or leave. Attached viewers can show a "Bob is also watching this session" indicator.

Input ownership: the first client to send Input becomes the typing owner. Other clients sending Input receive SessionEvent::Error { recoverable: true, "input owned by <client_id>" }. The owner releases via SessionRequest::ReleaseInput, after which the next sender claims it.

Crash recovery via journal replay: on session lifecycle events anvild writes ~/.anvil/sessions/<id>/manifest.json (manifest schema includes started_at_ns, last_activity_ns, model, permission_mode, workspace, status). On daemon restart, scan_orphans() walks the directory and surfaces unfinished sessions in SessionRequest::List as orphaned: true. The user issues SessionRequest::Resume { session_id } to reconstruct the SessionActor with the manifest + last 50 journal records as historical context. The resurrection emits SessionEvent::Started { replay: true } so the client knows it's a resume, not a fresh session.

Manifest GC: orphaned manifests older than 7 days get deleted on anvild startup with a log line. 13 unit tests + 2 integration tests cover every flow.

anvil routined Daemon (#834)

A new top-level binary: crates/anvild/. Long-running background daemon that executes routines (the v2.2.20 F6 event-routine model) even when no interactive session is open. Launchd plist + systemd user unit are generated by anvil routined install.

Unix-socket IPC at ~/.anvil/run/routined.sock with line-framed JSON. Eight verbs in v2.2.21: status | list | mount | dismiss | kick | tail | reload | session (the last one carries the D.1 SessionRequest envelope). The daemon scheduler ticks every 30s and reads routine schedules from ~/.anvil/routines/*.toml. Run archive at ~/.anvil/routines/<name>/runs/<ULID>/{run.json,stdout.log,stderr.log}.

CLI surface: anvil routined {install|uninstall|start|stop|restart|status|list|kick <name>|tail <name>|logs}.

OTel: anvil.routined.heartbeat every 30s (tagged pid, version); anvil.routined.routine.{run_started,run_completed} counters tagged with routine name + exit status.

16 unit tests + 1 integration test. Per [[feedback-anvil-capability-contract]] the new routined capability satisfies the 8-axis check (def / registration / completion / handler / dispatch / rendering / gate / OTel + tests).

Durable Session Journal (#833)

Every tool call across every session now appends a structured record to ~/.anvil/journal/YYYY-MM-DD/<session_id>.jsonl. Schema:

{
  "v": 1,
  "id": "01HXYZ...",
  "ts_ns": 1717272000000000000,
  "session_id": "sess-...",
  "tool": "Bash",
  "args_hash": "sha256:...",
  "args_redacted": { ... },
  "duration_ms": 412,
  "exit_status": "ok",
  "output_sha256": "sha256:...",
  "output_size_bytes": 1234,
  "workspace_id": null
}

Records use ULIDs (sortable timestamp + 80 bits random). Redaction scrubs token-like keys ((?i)(token|password|secret|api_?key|authorization)) and values matching token patterns (ghp_, sk-, JWT, hex≥32, base64≥40). Concurrent writers from multiple processes are safe via O_APPEND atomic appends ≤ PIPE_BUF.

CLI: anvil journal {stats | grep <pattern> | session <id> | tool <name> | since <human-time>}. The seven-day default window for grep and since keeps queries fast on multi-month journals.

OTel: anvil.journal.record.written counter tagged by tool, anvil.journal.query.duration_ms histogram, anvil.journal.bytes_written_total counter.

38 unit tests in runtime::journal. Indexes (by-tool, by-session) are deferred to v2.2.22 (filed as a noted TODO) because naive grep over JSONL stays under 200ms even on 10 MB journals.

Self-Building MCP Servers (#835)

crates/runtime/src/mcp_gen/ watches the journal for recurring tool-call patterns and proposes custom MCP tool servers automatically. Three-stage pipeline:

  1. Detector: slides over the last 7 days of journal records, clusters by (tool, args_structural_hash). A cluster is "interesting" when ≥3 occurrences AND mean Levenshtein ≤ 0.4 (normalized) AND variability concentrated on ≤2 keys in args_redacted.
  2. Codegen: emits a stdio MCP server in Rust (template-based) with parameters derived from the variability axes. Output goes to ~/.anvil/mcp-proposals/<id>/{REASON.md, SAMPLE-INVOCATIONS.md, server.rs, Cargo.toml, proposal.json}.
  3. Sandbox: cargo build --release --target-dir=/tmp/mcp-gen-sandbox/<id>/ against the generated crate. Synthetic input run with 10s timeout; reject on panic / invalid JSON / timeout.

The agent only proposes — per [[feedback-anvil-self-improvement-gating]] the user accepts or rejects via anvil mcp {scan | proposals | show <id> | accept <id> | reject <id>}. Accepted proposals move to ~/.anvil/mcp/<id>/ and write a registration stub (the actual claude mcp add equivalent registration is filed for v2.2.22).

OTel: anvil.mcp_gen.{detected, proposed, accepted, rejected} counters. 13 tests cover clustering, codegen compilability, sandbox rejection paths, atomic proposal accept, and idempotent scans.

Multi-Repo Workspaces (#836)

Work across multiple repos in one session with clean tool-call scoping, secret isolation, and journal tagging. Config at ~/.anvil/workspaces/<name>.toml:

name = "client-work"
description = "Foo Corp engagement"

[[repo]]
name = "frontend"
path = "~/projects/foo-frontend"
tool_allowlist = ["Read", "Bash", "Edit", "Write", "Grep"]
secret_scope = "foo-frontend"
journal_tag = "foo-fe"

[[repo]]
name = "backend"
path = "~/projects/foo-backend"
tool_allowlist = ["Read", "Bash", "Edit", "Write", "Grep"]
secret_scope = "foo-backend"
journal_tag = "foo-be"

anvil workspace switch <name> writes ~/.anvil/state/active_workspace.txt. When a workspace is active, every Bash/Read/Write/Edit/Grep tool call calls runtime::workspaces::scope::check_path(path, &ws) before executing; out-of-scope paths return ScopeError::OutOfScope. Symlinks are resolved via canonicalize() before the prefix check.

Secret isolation: env vars from ~/.anvil/secrets/<scope>/ are gated per active repo via the ScopedEnv wrapper around Command::env. Workspace A's secrets aren't visible when workspace B is active.

Journal records under an active workspace get their workspace_id field stamped with journal_tag:repo_name (e.g. "foo-fe:frontend"), enabling per-workspace journal slicing.

CLI: anvil workspace {list | create <name> | switch <name> | clear | show [name] | validate <name>}. Slash commands: /workspace {switch|clear|show}.

LSP bridge entrypoint reserved (anvil lsp prints "deferred to next release"); full LSP for VS Code/JetBrains/Zed is v2.2.22+ work.

9 runtime tests + 22 cmd_workspace CLI parser/handler tests.

Rail-Key Honesty (#832)

Two real bugs in the vertical_split rail keybindings that the drift-gate tests failed to catch:

  • g switch deck was only setting rail_focus = RailFocus::Deck (highlighting the rail row). It did NOT call cycle_deck_pane() — the label was lying. Now g actually cycles the deck pane through Conversation → Transcript → ToolResults (matches Ctrl+R) and pushes the "Deck pane: " system message. Label renamed g cycle deck so what you see is what you get.
  • ? help only fired in transcript (historical-view) mode; from a fresh prompt the binding was dead, falling through to insert_char. Now ? in live mode pushes a one-line keybind summary as a System message, gated on empty-input + no-modal + completion-not-visible (same gate as g/d/s/a).

Strengthened drift gate: rail_labeled_actions_actually_fire now asserts each rail key's CANONICAL ACTION fingerprint appears in the handler body, not just that some handler exists. Catches future "label says X, handler does Y" regressions.

anvild Downgrade Prevention (#827) — P0 Security

The v2.2.19 anvild had a self-update loop that overwrote anvil binaries on disk. A stale daemon (running from a deleted ~/.Trash/ binary) silently downgraded a user's freshly-installed v2.2.20 to v2.2.19 because the OS hadn't reaped the inode. Four defenses:

  1. The new anvild crate has zero binary-write code by design. Built from scratch without porting the v2.2.19 self-update loop.
  2. Static-analysis regression gate: crates/anvild/tests/binary_write_audit.rs walks every .rs in anvild source on every test run, fails CI if any fs::write|copy|rename appears near a string literal matching /bin/anvil(d)?(.exe)?. Authors must opt out via // allow_anvil_binary_write: <reason> comment marker.
  3. Inode-deleted self-detection: every 30s scheduler tick, anvild checks that std::env::current_exe().metadata() succeeds. On Linux this catches the (deleted) symlink suffix; on macOS it catches missing files moved out from under the process. anvild self-terminates with a clear log line.
  4. Stale-anvild sweep on anvil --update: before downloading, scan running processes for anvild/anvil with deleted-or-trashed executable paths. Refuse to proceed; print the offending PIDs and the kill command to copy-paste.

13 guard unit tests + 2 audit tests + 1 stale-detector smoke test.

anvil --update version-display + cache headers (#826)

Two fixes for the v2.2.20 update incident:

  1. Cache-Control headers: update_check::curl_get now sends Cache-Control: no-cache + Pragma: no-cache. The v2.2.20 incident saw a stale Cloudflare /api/version cache claim "already up to date" for 56 minutes while v2.2.20 was live. No-cache forces revalidation against origin.
  2. Version-display regression test: update_and_version_show_same_compile_time_version asserts both --update's "Current version" line and --version's rendered output read from env!("CARGO_PKG_VERSION") at compile time. Source-audit check confirms update.rs uses the literal {VERSION} constant (not a runtime-fetched value).

verifier surface fixes (#825 source-side)

Five of six broken surface checks in release-surfaces.yaml:

  • anvilhub_homepage / anvilhub_install / culpur_anvil_wp — the grep -oE 'v?2\.2\.[0-9]+' | sort -uV | tail -1 pattern ranked v-prefixed v...19 lexicographically higher than ...20. Fix: pipe through sed 's/^v//' to normalize before sort, change verify_expect to bare {version}.
  • v2_2_18_mouse_capture_default_off — original fingerprint string mouse_capture_default_off_regression never existed; re-anchored on actual test name mouse_capture_default_is_off in tui/paste.rs.
  • v2_2_18_autocompact_context_window — file path wrong (compaction.rscompact.rs) and variable evolved (context_windowcontext_max during the E.2D conversation split). Re-anchored on stable should_auto_compact function name in conversation/compact.rs.

The remaining surface (installer_public_copy_parity) closes when v2.2.21 deploys — local install.sh source has the required fingerprint (5 matches).

The Big Modularization — Phase C + Phase E

The largest refactor in project history. ~30 source files over the 800-LoC threshold got split into ~100+ focused sub-modules.

Phase C — main.rs (#840)

crates/anvil-cli/src/main.rs: 12,008 → 450 lines (96% reduction). LiveCli + REPL bodies + vault + startup-locale + tab-runtime + all CLI handler bodies extracted into cli_handlers/, live_cli/, startup.rs. main.rs is now a thin dispatcher.

Phase E.1 (Wave 1) — four giant files

  • #846 E.1A: live_cli.rs (9,778 LoC) → live_cli/ with 11 sub-modules (format, vault, helpers, session, repl, repl_commands, etc.)
  • #847 E.1B: tui/mod.rs (5,973 → 5,114) — extracted configure_menu.rs, git_info.rs, context_model.rs
  • #848 E.1C: wizard.rs (4,544 LoC) → wizard/ with 12 sub-modules
  • #849 E.1D: commands/handlers.rs (7,741 LoC) → handlers/ with 9 sub-modules

Phase E.2 (Wave 2) — eight more

  • #852 E.2A: commands/lib.rs (4,097) + specs.rs (3,238) → slash_enum.rs + slash_parse.rs + specs/{core,agent,dev,tools,meta}.rs
  • #853 E.2B: providers.rs (2,831) + utils.rs (2,293) → providers/{runtime_client,tool_executor,permission_prompter,auth,...}.rs + utils/{fs,git,format,prompt_helpers,shell}.rs
  • #854 E.2C: daemon.rs (2,353) + mcp_builder.rs (2,558) → daemon/{process,keepalive,service,tick}.rs + mcp_builder/{generator,templates,wizard_bridge}.rs
  • #855 E.2D: runtime/conversation/mod.rs (1,921) + runtime/hooks.rs (2,657) → conversation/{turn,compact,messages,conv_tests}.rs + hooks/{config,runner,matchers}.rs

Wave 1 follow-ups (residual oversize)

  • #850: handlers/memory.rs (3,619) → memory/{show,lifecycle,clean,episodic,procedural}.rs; handlers/mod.rs (2,320) → slash_dispatch.rs + chain.rs + cursor.rs + layout.rs + reflect.rs + rewind.rs
  • #851: live_cli/{mod, repl, repl_commands}.rs further split via subdirectories (repl/{tui,plain,notifications}, repl_commands/{tui_dispatch,plain_dispatch,cmd_runner})
  • #856: handlers/memory/mod.rs (1,797) → 74 LoC; extracted 1,723 lines of inline test module to memory/tests.rs
  • #857: live_cli/repl/tui.rs (2,230 → 1,235) by extracting the relay-message dispatcher to repl/relay_dispatch.rs; repl_commands/cmd_runner.rs (1,596 → 999) by extracting 15 helper methods to cmd_helpers.rs

Drift gates kept honest throughout

clippy_print_gate_is_in_place_on_tui_touching_files updated to list every new TUI-touching sub-module, so the #![deny(clippy::print_stdout, clippy::print_stderr)] discipline survives the split per [[feedback-tui-stdout-anti-pattern]]. Slash-spec drift gate (every_slash_command_variant_has_a_spec) green throughout the commands-crate split. Rail-label-action drift gate added per #832.

Phase F — CLI Inline Tests (#839)

22 cmd_workspace tests + 10 cmd_mcp tests = 32 new parser + handler tests for the new headless CLI subcommands shipped in #835 / #836. Covers every subcommand, every alias, every error path, plus a temp-ANVIL_HOME end-to-end create → list → switch flow.

Compacting bar + LLM compaction (#829, #830, #831)

The /compact command now generates a real LLM-written summary (via summarize_messages_via_llm) instead of head-count statistics. Two summary kinds tracked: Mechanical (head-count) and LlmGenerated (real model summary).

The TUI gains an anvil-and-hammer compacting status bar during compaction. Four hammer frames, 32-block molten progress bar, heat ramp (HEAT_STOPS constants byte-identical between Rust and the AnvilHub viewer's JS). The bar renders in all four TUI layouts (classic, vertical_split, three_pane, journal) and on the AnvilHub web viewer via the new RelayMessage::CompactingStatus { active, chunks } relay event.

Viewer parity — /ssh surface (#828)

Three viewer-parity bugs around remote SSH sessions: output relay timing, modal mirror state, and tab emit. Wires the /ssh slash command through the same relay path the other tools use so the AnvilHub viewer mirrors what the local TUI sees.

Polishing

Test totals

~3,200 tests passing, 0 failing across the workspace, all serial-execution clean:

  • anvil-cli (bin): 1264 passed / 0 failed / 3 ignored (up from 1198 pre-v2.2.21 — +66 new tests)
  • commands: 275 passed / 0 failed
  • runtime: 1564 passed / 0 failed / 1 ignored
  • anvild (lib): 97 passed / 0 failed (13 of which are #827 guard tests)
  • anvild integration: 8 across session_proto_integration, session_actor_integration, session_d4_integration, binary_write_audit, install_unit

LoC delta

~28,000 LoC delta from v2.2.20 across 11 features + ~100+ new sub-modules. The repo is bigger but every file is smaller — the largest source file in the v2.2.21 tree (live_cli/repl/tui.rs at 1,235 LoC) is smaller than the smallest of the top-five v2.2.20 files.

anvild is now required (#868 + #869)

#868 — Mandatory in wizard. The first-run wizard's anvild step was a three-choice modal (Install / Ask later / Never). As of v2.2.21 it is a single declarative info step: "Installing anvild as a background service for your account. This is required." There is one button — OK — and the wizard always installs the service unit. If the install fails for any reason (binary not yet on PATH, etc.), the error is logged to ~/.anvil/log/anvild.log and the wizard continues. Run /heal on the next launch to retry.

Existing users who opted out: If your ~/.anvil/config.json has "autostart": "no", "ask", or "disabled" — values written by older Anvil versions when you chose "Never" or "Ask later" — the config is automatically migrated to "autostart": "yes" on the next launch before the TUI opens. You will see a one-time message: [anvil] anvild is now required and has been re-enabled. Run /heal to verify status.

/heal severity escalation: The daemon probe in /heal previously returned NotApplicable when the service unit was absent (it was optional). Now it returns Broken, which surfaces the red ✗ line and a repair button. Users who previously skipped anvild installation will see the /heal modal on their first v2.2.21 launch.

ANVIL_SKIP_DAEMON_BOOTSTRAP renamed: The env var that some users and CI scripts set to bypass daemon bootstrap is now ANVIL_TEST_SKIP_DAEMON_BOOTSTRAP. The old name is no longer recognized. This variable is test-only infrastructure, not a supported user escape hatch.

#869 — Canonical naming. Every platform service surface now uses the literal string anvild:

Surface Before After
macOS launchd Label net.culpur.anvild net.culpur.anvild (already correct)
macOS plist filename net.culpur.anvild.plist net.culpur.anvild.plist (already correct)
Linux systemd Description anvil routine daemon Anvil background daemon (anvild)
Windows schtasks /TN AnvilD anvild
Windows task XML <URI> absent \anvild
Windows task XML <Description> absent anvild — Anvil background daemon
/heal modal label Daemon anvild
OTel log prefix [anvild::*] [anvild::*] (already correct)

User-facing error strings that said "is the daemon running?" now say "is anvild running?" (2 sites). The anvild --help usage line "Start the daemon" is now "Start anvild".

Named, Multi-Tab Daemon Sessions (#911)

Building on the D.1–D.4 session-in-daemon arc, #911 makes daemon sessions first-class and human-addressable. The SessionManifest gains a name and last_active_at_ns (Phase 1), a Rename IPC verb + Renamed event carry it on the wire (Phase 2a), and the TUI drives the full lifecycle: /session list|rename|kill over one-shot IPC (2b), then /session new|open|load|close mapped onto TUI tabs with per-tab daemon routing (2c). /session kill resolves sessions by name via a shared resolver across every verb. Phase 3 lands same-cwd auto-resume — reopen Anvil in a directory with a live daemon session and it reattaches instead of starting cold — plus the production manifest lifecycle (create/touch/retire).

Relay Bridge — Attach From the Browser (#914, #917, #919, #920, #921)

The relay bridge lets a remote viewer (AnvilHub web) pair to a live daemon session over a passage WebSocket and drive it.

  • #914 — relay_bridge. The passage WS bridge, the IPC verbs that start/stop it, and the headless CLI (anvil routined relay start) that auto-opens the viewer in the browser. The relay returns the full pairing PIN to the IPC caller; sessions root derives from the daemon home (not the env at call time); pairing no longer fails as "expired" when the viewer connects before the host.
  • #917 — P0 dispatch fix. Daemon-side tool dispatch had a name-drift bug where every tool fell through to the D.2 stub. Fixed — daemon-hosted sessions now execute real tools.
  • #919 — viewer UX. Input echo, a thinking spinner, and live context in the relayed viewer. Viewer input to an unmapped tab now starts a session on demand.
  • #920 — /permissions end-to-end over the relay, with slash commands routed via user_message and a sticky pair flag.
  • #921 — conversation replay on (re)pair. A ring recorder makes resume/replay carry the actual conversation, so a viewer pairing (or re-pairing) mid-session sees the history, not a blank pane.

Performance — Connection Pooling, Parallel Tools, Faster Search (#898, #899, #900)

Three of the five optimizations scoped in OPTIMIZATION-PLAN-v2.2.21.md land here:

  • #898 — shared reqwest::Client across the workspace (crates/api/src/shared_client.rs), wired into every provider. Stops paying a fresh TLS handshake + DNS lookup + connection-pool setup on every API call. reqwest::Client is Arc-internally, so the shared instance clones cheaply and reuses HTTP/2 connections.
  • #899 — parallel read-only tools. When the model requests ≥2 read-only tools in one batch (read_file, glob_search, grep_search, WebFetch, WebSearch, Skill, ToolSearch, Task*, …), they fan out across worker threads via closure injection and merge back in original order so the transcript stays valid. Mutating tools still run sequentially through the permission gate. A single job runs inline (no thread overhead); a regression test asserts 3×100ms tools finish in <200ms.
  • #900 — gitignore-aware streaming search. grep_search now uses the ignore crate: it walks and searches simultaneously (no pre-collecting the whole tree), respects .gitignore/.ignore, and skips binary files — so it no longer drowns in node_modules/ or target/.

The Big Modularization — Round 2 (#867, #873–#896)

The largest reshape in project history continued past Phase E. anvil-cli was split into focused crates — anvil-ollama, anvil-mcp-builder, anvil-tui, anvil-wizard (#867) — and eight crates were extracted from runtime (#873): anvil-vault, anvil-journal, anvil-permissions, anvil-oauth, anvil-hooks, anvil-mcp, anvil-memory, anvil-curator. Round 2 extracted five more (#892–#896): anvil-search, anvil-relay, anvil-reflection, anvil-skill-chain-exec, anvil-routines. Intermediate crates anvil-session-types and anvil-auth-cli (#875) broke dependency cycles. runtime shrank from ~70K → ~57.7K LoC; the workspace grew to 31 crates. Supporting work: [profile.release] LTO + strip (#889), compile-time locale-key drift detection (#890), an anvil-e2e integration-test crate (#888), and a tools/lib.rs split (#897).

Daemon Hardening — P0 Fixes (#902–#910, #913, #917)

The daemon execution path was load-bearing for the first time, and a cluster of P0s were found and fixed under live verification:

  • #905 — wire the real ProviderRuntimeClient into anvild (architecture fix; the daemon was not actually talking to a provider).
  • #906 — Attach must wire the broadcast::Receiver to a forwarder task, with daemon-side tracing for Input + turn lifecycle; #910 adds the session_forwarder e2e gate that would have caught it.
  • #903 — bind SessionClient I/O to the long-lived session runtime (REPL P0).
  • #904 — pid_path must read routined.pid, not anvild.pid.
  • #902 — /heal daemon probe checks the actual PID file + process liveness.
  • #908/#909 — show the daemon session_id; stop double-rendering AssistantMessage in daemon-TUI.
  • #913 — auto-compact now actually compacts (4 of 5 compaction bugs fixed; reads Anthropic Usage.input_tokens instead of a char-count proxy, see also #858).
  • IPC moved to length-prefixed framing (#865) and a dual-transport abstraction (Unix sockets + Windows named pipes), with Windows signal handling and a Scheduled-Task installer.

Closed items

  • #825 — verifier surface fixes (source-side)
  • #826 — anvil --update version-display + Cache-Control headers
  • #827 — anvild binary downgrade prevention (P0 security)
  • #828 — /ssh viewer parity
  • #829 — LLM-generated compaction summary
  • #830 — anvil-and-hammer compacting status bar
  • #831 — compacting bar parity across all 4 TUI layouts + AnvilHub viewer
  • #832 — rail-key honesty
  • #833 — durable session journal
  • #834 — anvil routined daemon
  • #835 — self-building MCP servers
  • #836 — multi-repo workspaces
  • #837 — v2.2.21 master tracker
  • #839 — cmd_workspace + cmd_mcp CLI inline tests
  • #840 — main.rs modularization (Phase C)
  • #841 — session protocol (Phase D.1)
  • #842 — SessionActor in anvild (Phase D.2)
  • #843 — TUI as viewer (Phase D.3)
  • #844 — multi-client attach + crash recovery (Phase D.4)
  • #845–#857 — Phase E modularization arc + follow-ups
  • #858 — autocompact reads real Usage.input_tokens, not char-count proxy
  • #860 — relay.rs split into pairing/protocol/snapshot/state/host
  • #862 — impl AnvilTui (4048 LoC) split into sibling files
  • #863 — anvild wired as default REPL execution backend
  • #864 — config type defs extracted to config/types.rs
  • #865 — routined.sock migrated to length-prefixed framing; dual-transport (Unix + Windows pipes)
  • #867 — crate split: anvil-ollama, anvil-mcp-builder, anvil-tui, anvil-wizard extracted from anvil-cli
  • #868 — anvild mandatory in wizard; migration for existing opted-out configs
  • #869 — anvild canonical naming across all platform surfaces
  • #870 — P0: sandbox install paths under ANVIL_HOME; robust binary resolution
  • #871 — anvil-ollama callbacks left broken by #867 sub 1
  • #873 — crate split: 8 crates extracted from runtime (vault, journal, permissions, oauth, hooks, mcp, memory, curator)
  • #874 — commands/subcommands.rs subgrouped into domain files
  • #875 — render/configure moved to anvil-tui; anvil-session-types + anvil-auth-cli intermediate crates
  • #887 — ollama_tune domain logic moved api → runtime
  • #888 — anvil-e2e integration test crate
  • #889 — workspace [profile.release] LTO + strip
  • #890 — compile-time locale-key drift detection
  • #892–#896 — crate split round 2: anvil-search, anvil-relay, anvil-reflection, anvil-skill-chain-exec, anvil-routines
  • #897 — tools/lib.rs (2,859 LoC) split into 4 files
  • #898 — shared reqwest::Client across workspace
  • #899 — parallel read-only tool execution
  • #900 — gitignore-aware streaming grep_search + binary skip
  • #902 — /heal anvild probe checks real PID file + process liveness
  • #903 — P0: bind SessionClient I/O to long-lived session runtime
  • #904 — P0: pid_path reads routined.pid, not anvild.pid
  • #905 — P0: wire real ProviderRuntimeClient into anvild
  • #906 — P0: Attach wires broadcast::Receiver to forwarder task
  • #908/#909 — daemon-TUI shows session_id; stops double-rendering AssistantMessage
  • #910 — session_forwarder e2e gate
  • #911 — named, multi-tab daemon sessions + same-cwd auto-resume
  • #913 — auto-compact actually compacts (4 of 5 bugs)
  • #914 — relay_bridge: passage WS bridge + headless CLI + auto-open viewer
  • #917 — P0: daemon tool-dispatch name drift (all tools hit D.2 stub)
  • #919 — relay viewer UX: input echo, thinking spinner, live context
  • #920 — /permissions end-to-end over relay
  • #921 — conversation replay on (re)pair via ring recorder
  • #922 — vertical-split rail keys behind Ctrl+B leader

Carrying forward to v2.2.22

  • #803 — F4 cross-session repeated-error detection
  • #809 — F2 same-name + distinct-content conflict UX
  • #810 — AnvilHub JS halves (F1 lineage panel + F2 sync API). Note: the daemon-side relay/viewer transport landed in v2.2.21 (#914/#919/#920/#921); the remaining AnvilHub web JS is the carry-forward.
  • anvilhub_tui_preview page update + culpur_products_wp wpdb update (deploy-side, resolves when v2.2.21 deploys)
  • anvil --daemon flip to default when v2.2.21 daemon path has soaked
  • Full LSP bridge implementation for VS Code/JetBrains/Zed
  • MCP-gen accepted-proposal auto-registration with claude mcp add equivalent
  • Journal secondary indexes (by-tool, by-session)
  • Most "not yet wired in daemon-mode" tool stubs (LS, Glob, WebFetch, WebSearch, MCP_*, Notebook, Task, TodoRead/Write)

Upgrade

brew upgrade anvil
# or
curl -L https://anvilhub.culpur.net/install.sh | sh

Pre-upgrade: if you have older anvild processes running from binaries you've moved or trashed, kill them first:

pgrep -af "anvild|anvil daemon" | awk '{print $1}' | xargs -r kill

(anvil --update will refuse to run if it detects stale daemons; the prompt tells you the exact PIDs to kill.)

Post-upgrade: the daemon-default path is OFF in v2.2.21 (use anvil --daemon to opt in). Sessions still run in-process unless you ask for daemon mode explicitly. The flip happens after a release of soak.


Downloads

Platform Binary
macOS ARM (M1/M2/M3) anvil-aarch64-apple-darwin
macOS Intel anvil-x86_64-apple-darwin
Linux x86_64 anvil-x86_64-unknown-linux-gnu
Linux ARM64 anvil-aarch64-unknown-linux-gnu
Windows x86_64 anvil-x86_64-pc-windows-gnu.exe
FreeBSD x86_64 anvil-x86_64-unknown-freebsd
NetBSD x86_64 anvil-x86_64-unknown-netbsd

Installation

# macOS/Linux
curl -LO https://github.com/culpur/anvil/releases/download/v2.2.21/anvil-$(uname -m)-$(uname -s | tr A-Z a-z)
chmod +x anvil-*
sudo mv anvil-* /usr/local/bin/anvil

Built locally via Culpur CI/CD pipeline (Docker cross-compilation).