Backend (TDD):
- task.promote {container_id, item_ref, attention?, project?}: mint a committed
task from the item_ref-th `- [ ]` context item (1-based, document order via a
new extract::context_item_lines) and rewrite that source line into a [[link]]
to it. Unit + rpc_socket tests.
- resolve_id now excludes canonical-context docs, so [[Task Title]] resolves to
the task, not its identically-titled context doc (deterministic; a general fix
surfaced by promotion's ULID-tiebreak ambiguity).
Plugin: :Heph promote / promote_under_cursor (save-if-dirty → compute item index
with a code-fence-aware scanner mirroring extract.rs → task.promote → reload the
rewritten buffer). e2e spec (f): promote a context line, assert the new task in
next, the source line became a link, and the container backlinks the task.
CI via Dagger: a test_nvim function bakes a pinned, arch-detected Neovim
(v0.11.2 — Debian's is too old for vim.uv) onto rust:1-bookworm, builds hephd,
and runs the self-contained shim suite (cargo + target cache volumes);
build.yaml calls `dagger call test-nvim`. run.lua now fails on zero specs (no
false-green). Validated end-to-end: passing suite → exit 0, failing spec →
Dagger exit 1.
117 Rust tests + 7 nvim e2e specs green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.9 KiB
| title | modified | tags | ||
|---|---|---|---|---|
| heph.nvim | 2026-06-01 |
|
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 |
Locate / spawn / readiness-poll hephd (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 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.