hephaestus/docs/reference/heph-nvim.md
Erich Blume e3db2ac550 heph.nvim: plug-and-play managed daemon (autostart, self-heal, client/server guardrail)
The plugin now manages its own hephd by default (autostart = true): if nothing
is serving the socket it spawns a local daemon against the default XDG paths,
kills only what it spawned on VimLeavePre, and self-heals — rpc.call retries
once through a respawn hook when the connection drops (the prior owner releases
the DB lock on exit, so a respawn can claim it).

- daemon.ensure() connects to an already-running daemon (any mode) or spawns one
  we own; stop_spawned()/is_managed() track lifecycle.
- A server/client daemon you started is always respected (spawn only when nothing
  serves the socket). autostart = false → connect-only, warns/errors if down,
  and clears the self-heal hook so it fails loudly.
- config: autostart defaults true; new `db` option; $HEPH_SOCKET / $HEPH_DB
  fallbacks isolate a dev Neovim onto a separate daemon + DB.

e2e: managed_daemon_spec covers autostart spawn, self-heal-after-kill, and
connect-only error. 10 specs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:37:49 -07:00

101 lines
5.7 KiB
Markdown

---
title: heph.nvim
modified: 2026-06-01
tags:
- reference
- design
---
# heph.nvim
The primary user surface (tech-spec §8): a Neovim plugin that replaces
obsidian.nvim and is a **thin client of the local `hephd`** over its
unix-socket JSON-RPC. Notes, journals, and tasks are edited as ordinary
buffers; the daemon owns all storage and sync. Built in checkpointed slices on
`feature/v1-prototype`; this card tracks the stable surface as it lands.
## Architecture
`heph.nvim/lua/heph/` modules, each small and single-purpose:
| Module | Responsibility |
|---|---|
| `rpc` | libuv (`vim.uv`) unix-socket JSON-RPC client. A blocking `call()` is built over the async pipe by pumping the loop with `vim.wait` until the matching id returns. Demuxes responses by id; partial lines are buffered; JSON `null` decodes to Lua `nil` (`luanil`). A `Session` is one connection — the module keeps a default singleton and lets tests open isolated sessions. |
| `node` | Buffer-backed nodes. A node is a buffer named `heph://node/<id>` with `buftype=acwrite`; `BufReadCmd` loads the body via `node.get`, `BufWriteCmd` saves the whole buffer via `node.update`. |
| `link` | Parse the `[[wiki-link]]` under the cursor (mirroring `extract.rs` grammar) and follow it via `node.resolve` (exact, never fuzzy `search`). Unresolved links are allowed. |
| `journal` | Open/create a dated journal node (idempotent — deterministic id). |
| `daemon` | Managed-daemon lifecycle: `ensure` (connect if a daemon already serves the socket, else spawn one we own), `stop_spawned` (kill only what we spawned, on exit), readiness-poll. Shared with the e2e harness. |
| `config` / `init` | `setup(opts)`, socket resolution, default keymaps. |
| `command` | The `:Heph <subcommand>` dispatch + completion. |
Surfaces never touch SQLite — every operation is a daemon RPC (tech-spec §3).
The plugin is **mode-agnostic**: Tactical/Strategic/Organizational are
plugin-side compositions of daemon primitives, not daemon concepts.
## Daemon lifecycle
`setup({})` is **plug-and-play** by default (`autostart = true`): if nothing is
serving the socket, the plugin spawns a local `hephd` against the default XDG
paths, kills only the daemon *it* spawned on `VimLeavePre`, and **self-heals**
`rpc.call` retries once through a respawn hook if the connection drops. It only
ever spawns when nothing is already serving the socket, so a `server`/`client`
daemon you started is respected. With `autostart = false` the plugin **connects
only** and warns/errors if unreachable — for when you run your own daemon. The
`$HEPH_SOCKET` / `$HEPH_DB` env knobs (and `mise run dev`) isolate a dev Neovim
onto a separate daemon + DB so real data is never touched.
## Daemon RPC dependencies
Beyond the existing methods (tech-spec §6), the plugin relies on
**`node.resolve {title} → Node | null`**: an exact, owner-scoped,
non-tombstoned alias-then-title match — the same mapping the store uses to
materialize `wiki` links, so "follow link under cursor" jumps to the *same*
node the stored link points at.
## Commands (as of slice 11c)
| Command | Action |
|---|---|
| `:Heph today` / `:Heph journal <YYYY-MM-DD>` | Open today's / a dated journal |
| `:Heph follow` (also `<CR>` in a node buffer) | Follow the `[[link]]` under the cursor |
| `:Heph open <id>` | Open a node buffer by id |
| `:Heph search <query>` | Full-text search; pick a result to open |
| `:Heph next [scope]` | Tactical "what is next?" view (`<CR>` opens a task's context) |
| `:Heph list [attention]` | Organizational survey of the outstanding set |
| `:Heph capture <title>` | Capture a committed task (pick attention) |
| `:Heph attention [color]` | Set the current task's attention |
| `:Heph done` / `:Heph drop` / `:Heph skip` | State change on the current task |
| `:Heph promote [attention]` | Promote the `- [ ]` line under the cursor to a committed task |
| `:Heph log <text>` | Append a breadcrumb to the current task's log |
"Current task" is resolved from the buffer: a `task` node, or a canonical-context
doc whose owning task is followed via its `canonical-context` backlink. The
`next`/`list` views render the titled rows the daemon returns (`list` enriched to
carry titles + the context id, so no N+1 `node.get`). Pickers use built-in
`vim.ui.select`, auto-upgrading to Telescope when installed.
**Promotion** (`:Heph promote`) mints a committed task from the `- [ ]` line
under the cursor (the daemon's `task.promote`, `item_ref` = the cursor item's
1-based index among context items, computed code-fence-aware to mirror
`extract.rs`) and rewrites that line into a `[[link]]` to the new task. To keep
that link unambiguous, wiki-link resolution excludes canonical-context docs, so
`[[Task Title]]` resolves to the task, not its identically-titled context doc.
## Testing (tech-spec §9)
The headless e2e suite drives the plugin in `nvim --headless` against a real
`hephd` over a temp socket, asserting both buffer contents and resulting DB
state (via an isolated RPC session). It uses a **self-contained busted-style
runner** (`tests/e2e/runner.lua`) — no external plugins, no network — so it is
deterministic. `mise run test-nvim` builds the daemon and runs the suite against
system-installed Neovim; a deliberately failing spec exits non-zero (no
false-green; the runner also fails if it discovers zero specs). CI runs the same
suite through the **`test-nvim` Dagger function** (`.dagger/`, invoked by
`build.yaml` as `dagger call test-nvim`), which bakes a pinned, arch-detected
Neovim onto a Rust image, builds `hephd`, and runs the suite — reproducible, and
identical to the native `mise run test-nvim` path.
## Related
- [[tech-spec]] — §8 surface spec, §6 RPC API, §9 testing strategy
- [[design]] — the mode model (Tactical/Strategic/Organizational) and rationale