--- 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/` 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` | Locate / spawn / readiness-poll `hephd` (shared with the e2e harness). | | `config` / `init` | `setup(opts)`, socket resolution, default keymaps. | | `command` | The `:Heph ` 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 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 ` | Open today's / a dated journal | | `:Heph follow` (also `` in a node buffer) | Follow the `[[link]]` under the cursor | | `:Heph open ` | Open a node buffer by id | | `:Heph search ` | Full-text search; pick a result to open | | `:Heph next [scope]` | Tactical "what is next?" view (`` opens a task's context) | | `:Heph list [attention]` | Organizational survey of the outstanding set | | `:Heph capture ` | 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