Skip to content

Reef 0.3.0

Choose a tag to compare

@github-actions github-actions released this 02 Nov 22:19
· 18 commits to main since this release
776e3c8

The rest of Phase 2's agent surface and its live views. Agents get brief JSON by default, a task-first reef --help, symbol-level diffs and a queryable history (reef history, the MCP query_changes tool), reef schema for generating tool definitions, and one-line installs for Claude Code, Codex and opencode, with the MCP server published to the MCP registry and as an MCPB bundle per target. Humans get a web UI (reef ui) with a live smartlog, a workspaces and fleet page, and an activity feed with a replay scrubber. Status answers from an incremental index instead of a tree walk, a daemon can be reached remotely over TLS with a token, and harnesses that isolate sessions in git worktrees can ask Reef for workspaces instead (the workspace provider contract). Every Phase 2 exit criterion is met; the Python and TypeScript SDKs (R2.06, R2.07) follow in a later release.

Upgrading from 0.2

Piped output is now brief JSON: compact, one list element per line, and for ws list, status and diff only the keys an agent acts on, with zero values (false, 0, "", []) left out, so a check such as .divergent == false no longer matches. A script that reads piped reef output and needs the full objects passes -o json, or sets REEF_OUTPUT=json once. The MCP server's list_workspaces, get_status and get_diff return the brief objects too.

Restart the daemon after upgrading: reef daemon stop once reef ws list shows no active workspaces, and the next command starts 0.3's. A 0.2 daemon keeps running after the binaries are replaced, and it answers commands that use new options of calls it already has as if the option were absent: --at-op and --at read the current state, diff --symbols returns no symbols, and ws list shows no file counts. A command whose first call the 0.2 daemon lacks restarts it when it has no active workspaces (not under --no-autostart or --remote); otherwise it fails with exit code 9 and next set to reef daemon stop.

The new daemon migrates its database (schema 6, for remote tokens) when it starts, and a 0.2 daemon then refuses that database: there is no going back to 0.2 on the same state directory. Its first start also backfills each repository's history index (repos/<id>/history.sqlite3, up to 100,000 operations and 512 MiB, [history]) in the background, and the state directory gains ui/ and remote/.

reef init --user now writes the user-level files only of the agents it detects by their home directories, so a machine without ~/.claude (or CLAUDE_CONFIG_DIR) no longer gets the Claude Code skill; --agents names them instead.

Added

  • Harness packages (ADR 0052): one line sets up an agent for every repository. Claude Code: claude plugin install reef --marketplace reef-vcs/reef (the skill, Reef's MCP server, and hooks that declare the files it edits). Codex: codex plugin marketplace add reef-vcs/reef && codex plugin add reef@reef (the skill and the server, from a plugin in the vendor-neutral Agent Plugins format). opencode: reef init --agents opencode --user. This repository is both marketplaces, pinned to each release's tag.

  • reef init --agents codex and --agents opencode, also detected by .codex/ and by opencode.json, opencode.jsonc or .opencode/ (and opencode by OPENCODE=1): the skill in .agents/skills/reef/SKILL.md, which both read, and Reef's server in .codex/config.toml (merged keeping the file's comments) or opencode.json; with --user, in the home directory.

  • reef presence hook edit and end: a harness's hook reports the files the agent is about to edit as an editing entry, from the hook event on stdin and JSON Pointers into it. One entry per session, replaced by its next edit, lasting 5 minutes; it always exits 0, prints nothing, and never starts a daemon.

  • An MCPB bundle per target on each release, reef-<version>-<target>.mcpb, holding reef and reef-daemon, and the server listed on the MCP registry as io.github.reef-vcs/reef. Releases also carry reef schema's document and descriptor set, attested with the rest.

  • The onboarding benchmark's scenario E: after the Claude Code plugin.

  • The workspace provider contract (ADR 0053, docs/workspace-provider.md): reef ws provider create | attach | release, for a program that isolates its sessions in directories and asks Reef for them instead of making git worktrees. Fields come as flags, or as one JSON object on stdin with --at FIELD=POINTER mapping the program's own event; each command prints one object (contract: 1), or the path alone with -o text. A name makes create return the same workspace while it lives, and a create from inside a workspace starts on its change. --hold-while PID|parent keeps the lease alive while that process runs, through a detached reef ws renew --follow; nothing is released when it exits, so the daemon reclaims the workspace with a final snapshot once the lease runs out. Two reasons: PROVIDER_INPUT_INVALID, HOLDER_NOT_RUNNING.

  • reef ws renew --follow [--while-pid PID] renews a lease until it ends, the process exits, or a signal.

  • reef here reports held and no longer tells an agent to release a workspace that a provider's holder holds.

  • The reef-workspaces Claude Code plugin, opt-in beside reef in the same marketplace: claude plugin install reef-workspaces --marketplace reef-vcs/reef makes Claude Code's worktrees (--worktree, subagents with isolation: worktree) Reef workspaces, through its WorktreeCreate and WorktreeRemove hooks; a resumed session's first prompt holds its workspace again (UserPromptSubmit).

  • Symbol-level diffs (ADR 0043, API revision 8). GetDiff with symbols reports each file's added, removed, changed and moved definitions, matched across the whole diff (renamed files and moves included), with each file's coverage, and the call sites in the new tree of the changed, removed and moved ones: exact through uploaded SCIP, by name through tags, with how many definitions share the name. expand_symbols returns only the named symbols' lines. max_call_sites and symbol_source tune it. Symbols are computed when at most 1,000 files differ.

  • reef diff --symbols lists each file's symbol changes under it, nested, with up to three call sites each and the totals; --stat adds the line counts, --call-sites N sets how many are listed, and --source picks tags or SCIP. reef diff --expand SYMBOL shows only that symbol's lines. JSON gains symbol_summary and each file's symbols only with these flags.

  • The MCP server's get_diff takes format: "symbols" and expand. tools/list is held to 20,250 bytes.

  • A history index and HistoryService.QueryChanges (ADR 0044, API revision 9): which changes touched files or symbols, who wrote them (activity per agent and workspace), when, and what became of them (open, landed on trunk(), abandoned, undone), filtered by symbol, path, agent, workspace, change, outcome and time window, paged or counted by agent, outcome or workspace, with the history each answer covers. The daemon follows the operation log into repos/<id>/history.sqlite3, backfills it, and keeps it through GC. [history] configures it. Two reasons: HISTORY_DISABLED, INVALID_HISTORY_QUERY.

  • reef history [PATH]... with --symbol, --change-kind, --by, --workspace, --change, --outcome, --since/--until (RFC 3339 or a duration ago), --include-empty, --group-by and --limit.

  • The MCP server's query_changes tool. tools/list is held to 21,250 bytes.

  • Replay (ADR 0046, API revision 11): GetStatus, ListWorkspaces, GetWorkspace and ListOperations take at_operation and answer as of that operation: the workspaces whose working copy its view holds, each on its change then, with what the log does not record (presence, attention, stores, the watch, lease liveness) left unset. GetOperation finds an operation by time with ended_at_or_before. Two reasons: OPERATION_NOT_RETAINED, TOO_FAR_BACK; STATUS_SOURCE_OPERATION.

  • --at-op on reef status, ws list, ws inspect and op log, and --at WHEN (an RFC 3339 time or a duration ago) on every command that takes --at-op.

  • ListWorkspacesRequest.changed_file_counts fills Workspace.changed_file_count, how many files each active workspace's change changes as of its last snapshot (API revision 10). reef ws list asks for it, in JSON and in a new FILES column, and so does the MCP server's list_workspaces.

  • -o brief: compact JSON with only the keys an agent acts on (ADR 0045).

  • A web UI (ADR 0047): a React and TypeScript app the daemon serves on a loopback listener, read-only, over gRPC-web. It lists every registered repository with its latest operation and its active agents, live from the operation and presence streams, and has a reference page for the state colors (/states). The first reef ui binds the listener, which stays until the daemon stops; a daemon whose user never runs reef ui opens no port.

  • reef ui opens the web UI in a browser on a new session, through a launch file, so the session's token is on no command line. --print prints the session URL instead; --open opens a browser even when stdout is not a terminal, where it otherwise opens nothing.

  • DaemonService.CreateWebSession (API revision 12): binds the web listener if needed and mints a session; ADMIN. Five reasons: WEB_UI_DISABLED, WEB_UI_NOT_BUILT, WEB_UI_UNAVAILABLE, WEB_SESSION_INVALID, WEB_ACCESS_DENIED.

  • [ui] in the daemon's config.toml: enabled (true) and listen ("127.0.0.1:0"; a loopback address only).

  • Released binaries embed the web UI. A build from source embeds a placeholder page instead, which reef ui refuses with WEB_UI_NOT_BUILT, unless just web-build (Node 24) ran before cargo build.

  • The web UI's smartlog, at /repos/<id>/smartlog and linked from the overview: a revset's changes as a live graph, by default Reef's log set with each stack's fork point (trunk() | working_copies() | ancestors(mutable(), 2)), stacks hanging off a trunk spine with left-out ranges dashed. Stacks keep their places as agents snapshot, and a refresh follows the operation stream, one read at a time. Selecting a change shows its description, parents, files and who wrote it; ?at=<operation> shows the graph at that operation, frozen. The page prints the reef log command that lists the same entries.

  • A browser smoke test of the web UI (just web-e2e, CI's web e2e job): a daemon built with the app, on a scratch state directory, in Chromium, Firefox and WebKit, checking the session hand-off, one operation stream and one presence stream shared by two tabs, live updates of the overview, of a smartlog row in place, and of the workspaces page (lease health, an agent's activity, a write, a release), and the no-session page after a restart.

  • The web UI's workspaces page, at /repos/<id>/workspaces and linked from the overview's agent count and the smartlog: an Agents table (each agent with its workspaces, current change, what it is doing and its lease health) and a Workspaces table (owner, agents, change, changed files, disk beyond the base, lease, last snapshot), live from the shared streams. Lease health comes from the daemon's heartbeat entries, never the browser's clock: heartbeating, not heartbeating (never renewed), missed heartbeats (a renewing holder stopped), or waiting for a heartbeat just after a daemon start. Disk is measured one workspace at a time, re-measured only where an operation touched it, or on "Measure again". Workspaces that end and agents that leave keep their rows until hidden. A detail pane shows a selected workspace or agent; ?at=<operation> shows the page at that operation, frozen, without presence or disk. A nav links the smartlog and the workspaces page.

  • The web UI's activity feed, at /repos/<id>/activity: the operation log as a live feed, one agent's snapshots and edits in a workspace (or one user's jj commands) coalesced into one row while no more than 5 minutes apart, with every count exact across reconnects, reloads and garbage collection, new rows held behind a count while you read, the reef op log command that lists the same operations, and a pane with an operation's details and effects. It holds up to 10,000 operations, 1,000 more per "Load older".

  • The scrubber, in a header shared by the smartlog, workspaces and activity pages: one slider over the loaded operations that moves the open page to ?at=<operation>, by arrow keys (a burst is one navigation), by dragging on a time axis of operation counts, or by a time (GetOperation by time beyond what is loaded).

  • Operation ids in the web UI link to the activity feed: the overview's latest operation, the smartlog's and the workspace pane's causes and snapshots, and the "at operation" banner.

  • Developer mode in the web UI: a switch in Settings, at the top right, off by default and remembered per daemon, that shows ids, the CLI commands behind each page, the smartlog's revset, exact times and the API's words, as the pages showed them before the design pass.

  • An incremental status index (ADR 0048, API revision 13): StatusWalkReason and WorkspaceStatus.walk_reason say why a status walked the tree; source is index or walk. [status_index] in the daemon's config.toml: enabled (true) and sync_timeout ("1s"). JSON from reef status -o json and the MCP get_status gains walk_reason when the source is walk.

  • A remote listener (ADR 0049, API revision 14): a daemon serves the API, gRPC-web and the web UI to other machines on one TLS port, to callers holding a token. Off by default; [remote] enabled = true, or --remote on reef-daemon (reef daemon start --serve-remote), turns it on. TLS is Reef's own, on rustls and ring: a generated key clients pin by its fingerprint, or [remote] tls_cert and tls_key, reloaded when they change. ADMIN RPCs are never served remotely.

  • RemoteService (ADMIN): CreateRemoteToken, ListRemoteTokens, RotateRemoteToken, RevokeRemoteToken. Tokens are named, read or write, expire (30 days by default), rotate with an overlap, and revoke at once, open streams included. GetDaemonStatusResponse.remote, Operation.remote and Workspace.remote_token_id; operations a remote call causes carry reef.remote-token and reef.remote-token-name. Eight reasons: REMOTE_TOKEN_INVALID, REMOTE_ACCESS_DENIED, REMOTE_WORKSPACE_NOT_OWNED, REMOTE_BUSY, REMOTE_TOKEN_NOT_FOUND, REMOTE_TOKEN_EXISTS, TOO_MANY_REMOTE_TOKENS, INVALID_REMOTE_TOKEN_NAME. The database moves to schema version 6.

  • reef remote add|list|remove|show and reef remote token create|list|rotate|revoke; reef --remote <name> (or REEF_REMOTE, REEF_REMOTE_TOKEN, REEF_REMOTE_FINGERPRINT) runs a command on a remote daemon. reef daemon status shows the remote listener.

  • [remote] in the daemon's config.toml: enabled (false), listen ("0.0.0.0:7470"), names (the host name), tls_cert and tls_key.

  • reef schema prints the CLI's commands (each one's input schema, the argv that turns an input into words, and its output with -o json, -o brief and on a pipe), the MCP tools with output schemas, and the API's services and messages as canonical proto3 JSON, in one JSON Schema 2020-12 document; reef schema cli <words>, mcp <tool> and api <name> print one, and reef schema --descriptor-set prints the API's protobuf descriptor set (ADR 0050). It runs offline. The MCP server's tools/list is unchanged.

Changed

  • reef --help is a short, task-first help (ADR 0051): the four commands agents need first (ws list, here, ws run, history), then every other command by name, in 509 bytes instead of 3,547. --help-all, on the root and every command, prints the full help. The global options but -o (--state-dir, --socket, --config, --no-autostart, --no-register, --timeout, -q, -v, --agent, --remote) are shown only there; completions and reef schema still list them. Every command's --help is about half as long.

  • The R2.17 token benchmark's live gate adds the mcp arm's fixed cost, as its scripted half always did.

  • reef init --user writes the user-level files of the agents it selects, recognizing each by its directory in the home directory (~/.claude, $CODEX_HOME or ~/.codex, $XDG_CONFIG_HOME/opencode or ~/.config/opencode), rather than always writing Claude Code's skill. --agents none --user now writes nothing outside the repository.

  • The web UI's design pass: an app bar with the Reef mark, the repository's name and tabs for Smartlog, Workspaces and Activity, with the activity scrubber as a strip under it; pages use the full width; repositories and agents are cards; smartlog rows lead with the description; times read "4 min ago" with the exact time on hover; identities have colored initials; operations read as sentences ("described a change") and lease health as healthy, idle, waiting or unresponsive; light and dark brand colors. Ids, commands, revsets and exact times move behind developer mode.

  • The web UI loads one chunk per page, so a page loads only its own code, and the JavaScript budget (500 KiB) applies to what each page loads. A tab older than the daemon that opens a page it had not loaded says to reload. The repository pages' nav moves into a header they share, with Activity added.

  • The web UI counts an agent once: the overview's "Active agents" and the smartlog's agent labels no longer count the daemon's lease and editing entries under the workspace's owner as a second agent beside the name the agent reports under. The web UI's JavaScript budget is 500 KiB (was 450 KiB).

  • A workspace's owner may not contain control characters (CreateWorkspace refuses one): an owner stands in for the agent on operations, which the remote listener's caller identity rides on (R2.08).

  • Reads that snapshot a workspace first (GetStatus, ListChanges, GetChange, GetDiff, ReadFile, ListFiles, ListSymbols, GetWorkspace with snapshot_first) answer from the incremental status index: no snapshot when nothing changed since the last scan, and a snapshot of the changed paths alone otherwise, instead of a walk of every tracked file. Each read first syncs with the watcher through a cookie in a directory outside the workspace root, so a write made just before it is seen.

  • A watched workspace is now fully scanned at least every watcher.watched_fallback_interval (10 minutes), busy or idle; before, a workspace with events was never fully scanned until its release. A full scan that finds a change no event named logs "status index missed changes".

  • On Linux a close after writing (IN_CLOSE_WRITE) counts as a change, so a write through a memory mapping is snapshotted.

  • Piped output is brief JSON (ADR 0045): compact, a list one element per line, and for reef ws list, status and diff only the keys an agent acts on, with zero values left out. This breaks scripts that read piped reef ws list, status or diff without -o json; -o json or REEF_OUTPUT=json restores the full JSON. On R2.17's state-repo question, reef ws list alone is 215 bytes, against 2,893 for git.

  • The MCP server's list_workspaces, get_status and get_diff return the same brief objects, without next_page_token on a last page.

  • reef ws run --workspace on a released or reclaimed workspace now says where its work is and names the reef ws create --change that starts again on it.

  • Reads at an operation share the head's commit index when it covers the operation's view: after GC they no longer rebuild and save a full index for each older operation read, and the daemon keeps 64 of them loaded instead of 8. A commit created after the operation, named by its full id in a read at it, now resolves (hidden) instead of not found; change offsets and short prefixes still resolve as of the operation.

  • ListSymbols by name or symbol reads only the matching rows of the index, not each matching file's whole entry, and (R2.16) reads them in path order with its filters and cursor in SQL, stopping at the end of the page.

Fixed

  • A command run while the daemon was shutting down, or still starting, started a second daemon, which lost the instance lock and exited, and the command reported "reef-daemon exited … before … answered". Since tonic 0.14.7 a daemon refuses connections while it drains. A command now waits, up to 15 s, while the daemon's lock is held and its socket refuses, and starts one only once the lock is free (#162).
  • A panic on the daemon's filesystem-watcher threads no longer ends a workspace's watch silently. Since notify 8.2.1 such a panic unwound out of the macOS FSEvents thread, and a later unwatch could then spin forever. A panic in Reef's tap is logged and marks every notify workspace's journal events-lost, so the next reads walk; any other panic on notify's or the debouncer's threads aborts the daemon, as notify did before 8.2.1 on macOS, and the next command starts one that rescans (#161).
  • reef init says when the .mcp.json it writes duplicates the MCP server of Reef's Claude Code plugin, installed on this machine, for this user or this repository, and how to turn the project's reef server off on this machine; it still writes the file, which is for everyone working in the repository (#153). Claude Code's user directory follows CLAUDE_CONFIG_DIR, for the user-level skill too.
  • reef mcp --help's example no longer names one harness's command (#118).
  • On macOS, creating or releasing a workspace no longer drops other workspaces' filesystem events: each notify workspace has its own FSEvents stream.
  • A read at a GC-kept copy of an operation from before the last GC failed: jj could not build an index for it once GC had removed its ancestors. It answers through the head's index now (R2.12).
  • The MCP server reads path arguments as the CLI does under --workspace: ./, .. and a trailing / resolve, and . is the root where a tool takes one (get_diff, list_files). get_diff, read_file and list_files had refused src/ with INVALID_PATH, and query_changes matched nothing for ./src. An edit_files target with a trailing / is still refused (#117).
  • The MCP server's get_diff described max_lines_per_file as "[default: no cut]"; the default is 1,000.
  • Timestamps print with exactly three fraction digits (2026-10-30T17:04:34.000Z, …34.100Z), in the CLI's JSON and text, MCP results and backup manifests, so they sort as strings. time's RFC 3339 formatting had trimmed trailing zeros and dropped a zero fraction, so a time on a whole second sorted after a later one in the same second.