How the packages fit together
- Grove has one engine and several clients.
grove.coreowns decisions while CI enforces dependency boundaries. - The daemon serves REST and SSE. The TUI, client SDK, MCP server and web dashboard expose the engine through different interfaces.
The package layout
grove/
├── core/
│ ├── __init__.py # public exports
│ ├── config.py # configuration cascade
│ ├── workspace.py # workspace state
│ ├── git.py
│ ├── tmux.py
│ ├── mewbo.py
│ ├── store.py # atomic storage
│ ├── manager.py # orchestration
│ ├── registry.py
│ ├── activity.py
│ ├── sessions.py
│ ├── auth.py # pairing
│ ├── mailboxes.py # native coordination
│ ├── native.py # steering clients
│ ├── native_launch.py
│ ├── native_worker.py
│ ├── paths.py
│ ├── errors.py
│ ├── contracts/
│ └── agents/
│ ├── native_owner.py
│ ├── native_claude.py
│ └── native_codex.py
├── daemon/ # loopback FastAPI
├── client/ # local PTY and SSH SDK
├── mcp/ # tools over the SDK
└── tui/ # Textual
├── cli.py
├── app.py
├── theme.py
├── _status.py
├── keys.py
├── screens/
└── widgets/
webapp/ # Next.js and its BFF
- Public exports define the engine API. pyproject.toml declares the import contracts that CI checks.
The boundaries, enforced
- Core cannot import UI packages. The daemon depends on core, never on clients.
- The client SDK cannot import daemon or TUI internals. MCP reaches the daemon through
GroveClient, including from another host. include_external_packages = trueextends checks to external imports.lint-importsenforces these boundaries on every push.
The public share boundary
- Bearer authentication protects daemon routes except pairing, liveness and
/public/{token}. A share token opens one workspace, never the fleet. WorkspaceState.share_tokenis the publication state. Clearing it revokes the link without a second boolean to synchronize.- Public routes have a separate prefix. A route census prevents accidentally exposing an authenticated route.
grove.core.contracts.publicexplicitly selects public fields, including nested data. New fields stay private rather than escaping a redaction list.grove.core.share_policysets project policy. Expiry is fixed when a link is minted, while an optional hashed passcode is checked on every read.- Readers supply the passcode in a header. No anonymous grant, cookie or session needs storing.
Side effects at the edges
- src/grove/core/git.py, src/grove/core/tmux.py and src/grove/core/mewbo.py isolate their respective I/O.
- src/grove/core/native_launch.py prepares worker credentials and configuration. src/grove/core/native_worker.py starts the provider and holds the daemon connection.
- Managers orchestrate these edges, which tests replace with fakes. New I/O belongs at one boundary.
The contracts layer
grove.core.contractsowns branch intent, request envelopes, response views and shared palettes.- Values crossing client boundaries use Pydantic with
extra="forbid". Internal state uses@dataclass(slots=True).
The agents layer
- Adapters map Claude Code transcripts, Codex rollout files and Mewbo sessions onto
AgentActivityState. Thegenericadapter deliberately does nothing. - The registry selects by
kind. Adapters normalize protocol shape, never model behavior.
The native session control plane
- Claude Code, Codex and OpenCode default to Grove owned native sessions. Their tmux pane runs
grove-native-worker, which owns the provider channel. - The worker starts
claude -porcodex app-serverand registers through/mailboxes/connectionSSE. - Steering reaches a native owner through
OwnerSteerClientinside the daemon orDaemonSteerClientover HTTP. - Each sent or received frame becomes a timestamped stdout line. Dashboards expose this wire log as Stream.
- Questions and stream facts enter the hook spool.
ClaudeHook.drainfolds them into the existing session sidecar rather than adding another activity reader.
sequenceDiagram
participant Client as CLI / TUI / web
participant Daemon as grove.daemon + coordinator
participant Worker as grove-native-worker (tmux pane)
participant Provider as claude -p / codex app-server
participant Spool as hook spool → sidecar → activity
Worker->>Daemon: register, hold /mailboxes/connection (SSE)
Client->>Daemon: message / interrupt / set model / answer
Daemon->>Worker: control frame over the SSE stream
Worker->>Provider: provider frame on stdin
Provider-->>Worker: stream event or JSON-RPC frame on stdout
Worker->>Worker: one timestamped line per frame
Worker->>Spool: *.ask.json, *.facts.json
native: false,claude-terminalorcodex-terminalkeeps the interactive agent UI. Grove steers it through pasted keystrokes.
The observability spine
ActivityServicecombines agent state, tmux output and git changes. The TUI consumes deltas directly and the daemon streams them over SSE.RepoRegistrykeeps one manager per repository without repeatedly resolving configuration.SessionExplorerdiscovers sessions across the repository and its worktrees for CLI, daemon and web consumers.
Dependencies flow inward
- Clients depend on the engine. Reverse dependencies couple internal helpers to callers and invite circular imports.
flowchart LR
Browser([browser / phone]) -.http.-> BFF([webapp BFF])
BFF -.http + SSE.-> Daemon([grove.daemon])
TUI([grove.tui]) --> Core([grove.core])
MCP([grove.mcp]) --> Client([grove.client])
Client -.http.-> Daemon
Daemon -.REST + SSE.-> Core
Core --> Git([core.git])
Core --> Tmux([core.tmux])
Tmux --> Worker([grove-native-worker])
Worker -.stdio.-> Provider([claude -p / codex app-server])
Worker -.SSE + http.-> Daemon
Core --> Native([core.native*])
Core --> Mewbo([core.mewbo])
Core --> Store([core.store])
Core --> Contracts([core.contracts])
Core --> Agents([core.agents])
See also
- Read Public API for exported contracts.
- Read Engineering principles for design rules.
- Read Contributing for development commands and pull requests.