diff --git a/docs/changelog.d/v1-prototype.feature.md b/docs/changelog.d/v1-prototype.feature.md index e1f144f..aca928b 100644 --- a/docs/changelog.d/v1-prototype.feature.md +++ b/docs/changelog.d/v1-prototype.feature.md @@ -21,3 +21,4 @@ Begin the v1 prototype (Phase 1, tech-spec §11.1), built in TDD slices: - `heph.nvim` follow-or-create: pressing `` on a `[[wiki-link]]` whose target doesn't exist yet now **creates** a doc with that title and opens it (the zettelkasten gesture), materializing the source's backlink — so you can link a journal entry to a brand-new note in one keystroke. Plus `:Heph doc ` to create a standalone wiki entry, and `:Heph home` — a single designated landing/index page (open-or-create by title, configurable via `opts.home`) to grow a map of content around. `:Heph journals` opens a recent-days picker (preview existing days, `@create` for new ones; count via `opts.journal_days`, default 7) — the dailies workflow. Pickers (Telescope) now support a preview pane. The `:Heph next`/`list` views are interactive: `<CR>` opens a task's context, `a` adds a task (prompt title + attention), `d` marks the task under the cursor done, `r` refreshes — with a dimmed key hint shown above the list. - Dev/installed isolation tooling: a `mise run dev` task runs the working-tree `hephd` on isolated `.dev/` paths, and a how-to ([[install-heph]]) covers installing `heph`/`hephd` from the forge (build-from-source), the lazy.nvim plugin setup, and pointing a dev Neovim at the dev daemon via `$HEPH_SOCKET`/`$HEPH_DB` so it never touches the installed store. - CLI as a complete task surface (§1, §6.2.1): `heph` now implements the entire daemon API and is the task capture/scripting surface. Structured fields are flags with **human dates** (`--do-date tomorrow|+3d|fri|YYYY-MM-DD`, shown back compactly in `next`/`list`) and **recurrence** (`--recur` presets/natural-language like "every 3 days", or a raw `--rrule`). New verbs: `list`, `done`/`drop`/`skip`, `attention`, `edit` (reschedule do-date/late-on/recurrence, re-attention, re-file — backed by the new `task.set_schedule` RPC), `promote`, `show`, `log` (append or tail), `health`, `node update`/`rm`, `resolve`, `links`/`backlinks`, `link add`, `project add [--parent]`, `sync [--status]`, `conflicts [resolve]`. Projects are referenced by name. Date/recurrence parsing is unit-tested; the new verbs have real-socket process tests. +- Daemon lifecycle is now an explicit OS service, and all surfaces are connect-only (no more auto-spawn). `heph daemon start/stop/restart/status/uninstall` idempotently manages a launchd agent (macOS) or systemd user service (Linux) that runs `hephd` on your default store; `heph.nvim` no longer spawns or supervises a daemon — it just connects and points you at `heph daemon start` if none is running. Rationale: once the CLI became a first-class surface, a daemon owned by one surface couldn't be shared (see [[run-the-daemon]], [[design]] §4). diff --git a/docs/explanation/design.md b/docs/explanation/design.md index 54d30a3..5fb93ef 100644 --- a/docs/explanation/design.md +++ b/docs/explanation/design.md @@ -96,6 +96,7 @@ Layers, top to bottom: - The **web UI** is the occasional hub-served surface. A later iOS/Watch client talks to the hub directly. - 🔒 **Single binary, three modes.** One Rust binary runs as `local` / `server` / `client` ([[tech-spec]] §3.1); the `heph` CLI shares the same command surface. Mode is two orthogonal axes (backend + inbound listener) plus an optional `hub_url` that makes any `local` instance a syncing **spoke** — the everyday device is `local` + `hub_url`, the hub is `server`, `client` is the online-only convenience. - **Per-device daemon (`hephd`):** owns the local SQLite handle, the CRDT/op-log state, and background sync. All local surfaces connect to it over a local socket. This is what makes multi-surface access concurrent and safe on one SQLite file, and gives one place to run background sync. + - 🔒 **Lifecycle = an explicit OS service; surfaces are connect-only (decided 2026-06).** Since the daemon is shared across surfaces (CLI, TUI, nvim), no single surface owns it — so none auto-spawns it (an earlier nvim "managed daemon" that spawned + killed-on-exit was removed: a surface-owned daemon can't be shared, and "when do we stop it?" has no good answer). Instead it runs as a launchd agent (macOS) / systemd user service (Linux), managed by **`heph daemon start/stop/restart/status`** ([[run-the-daemon]]). Surfaces only connect, and tell you to run `heph daemon start` if nothing is serving the socket. - **Core crate (Rust lib):** data model, query engine, markdown parsing + wiki-link extraction, and sync logic. Linked by both `hephd` and the hub server. - **Hub:** `hephd` running in "server" mode on blumeops k3s — central SQLite, the sync rendezvous point, and the web UI host. May be offline for long periods; devices keep working and reconcile when it returns. diff --git a/docs/how-to/how-to.md b/docs/how-to/how-to.md index 49715b9..265cd37 100644 --- a/docs/how-to/how-to.md +++ b/docs/how-to/how-to.md @@ -17,4 +17,5 @@ Task-oriented guides for common operations. ## heph - [[install-heph]] — Install `heph`/`hephd` from the forge, set up the Neovim plugin, and isolate in-repo development +- [[run-the-daemon]] — Run `hephd` as an OS service with `heph daemon start/stop/restart/status` - [[import-todoist]] — Seed a heph store from your Todoist projects + tasks (`mise run import-todoist`) diff --git a/docs/how-to/install-heph.md b/docs/how-to/install-heph.md index 7fcdf68..0c034b6 100644 --- a/docs/how-to/install-heph.md +++ b/docs/how-to/install-heph.md @@ -48,15 +48,22 @@ git clone --branch feature/v1-prototype \ { dir = vim.fn.expand("~/.local/share/heph/checkout/heph.nvim"), config = function() - require("heph").setup({}) -- plug-and-play: spawns + manages its own hephd + require("heph").setup({}) -- connect-only: talks to the daemon you started end, } ``` -`setup({})` is plug-and-play — it starts and supervises a local `hephd` against -the default paths, so you don't need a separate service. Update the plugin by -`git pull`ing the checkout. (A future split of `heph.nvim` into its own forge -repo will make this a normal `{ "eblume/heph.nvim" }` spec.) +The plugin is **connect-only** — it talks to a `hephd` you run as a service, it +does not start one itself. Start the daemon once: + +```bash +heph daemon start # launchd agent (macOS) / systemd user service (Linux) +``` + +See [[run-the-daemon]] for `start`/`stop`/`restart`/`status`. Update the plugin +by `git pull`ing the checkout (and after a `cargo install` upgrade, `heph daemon +restart` to pick up the new `hephd`). (A future split of `heph.nvim` into its own +forge repo will make this a normal `{ "eblume/heph.nvim" }` spec.) ## 3. Isolate development diff --git a/docs/how-to/run-the-daemon.md b/docs/how-to/run-the-daemon.md new file mode 100644 index 0000000..8ada221 --- /dev/null +++ b/docs/how-to/run-the-daemon.md @@ -0,0 +1,66 @@ +--- +title: Run the heph daemon +modified: 2026-06-02 +tags: + - how-to +--- + +# Run the heph daemon + +`heph` and `heph.nvim` are thin clients — they talk to a `hephd` daemon over a +unix socket and **never start one themselves** ([[design]] §4). Run the daemon +as an OS-managed service with `heph daemon`: + +```bash +heph daemon start # install + start (idempotent) +heph daemon status # is it installed/running? where are its socket/db/log? +heph daemon restart # restart — run this after upgrading the binary +heph daemon stop # stop it now +heph daemon uninstall # stop and remove the service for good +``` + +All verbs are idempotent — `start` when it's already running is a no-op, `stop` +when it's already stopped is fine. + +## What it manages + +- **macOS** — a launchd **LaunchAgent** (`org.hephaestus.hephd`) at + `~/Library/LaunchAgents/org.hephaestus.hephd.plist`, with `RunAtLoad` + + `KeepAlive` (starts at login, restarts if it crashes). +- **Linux** — a **systemd user service** (`heph.service`) at + `~/.config/systemd/user/heph.service`, with `Restart=on-failure`, enabled for + login. + +Either way it runs `hephd --mode local` against the default store +(`~/.local/share/heph/heph.db`) and socket, with logs at +`~/.local/share/heph/hephd.log`. + +> **`stop` vs `uninstall`:** `stop` halts the daemon now, but the service is +> still installed, so on macOS it starts again at next login. Use `uninstall` +> to stop it persistently. + +## After upgrading + +When you rebuild/reinstall (`cargo install … --force`), the running daemon is +still the old binary until you restart it: + +```bash +heph daemon restart +``` + +## Development isolation + +`heph daemon` manages the **installed** daemon on the default paths. For in-repo +development, run the working-tree daemon on separate paths instead and point a +dev Neovim/CLI at it (never touches your real store): + +```bash +mise run dev # working-tree hephd on .dev/ paths +HEPH_SOCKET="$PWD/.dev/hephd.sock" HEPH_DB="$PWD/.dev/heph.db" nvim +HEPH_SOCKET="$PWD/.dev/hephd.sock" heph next +``` + +## Related + +- [[install-heph]] — install `heph`/`hephd` and the plugin +- [[design]] — §4 the connect-only surface model diff --git a/docs/reference/heph-nvim.md b/docs/reference/heph-nvim.md index 7987101..db4b19b 100644 --- a/docs/reference/heph-nvim.md +++ b/docs/reference/heph-nvim.md @@ -24,8 +24,7 @@ buffers; the daemon owns all storage and sync. Built in checkpointed slices on | `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. | +| `config` / `init` | `setup(opts)`, socket resolution, default keymaps. The plugin is **connect-only** — it never spawns a daemon (see Daemon lifecycle). | | `command` | The `:Heph <subcommand>` dispatch + completion. | Surfaces never touch SQLite — every operation is a daemon RPC (tech-spec §3). @@ -34,15 +33,20 @@ 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. +The plugin is **connect-only**: it never spawns or supervises a `hephd`. The +daemon is an explicit, OS-managed service started once with **`heph daemon +start`** (a launchd agent on macOS, a systemd user service on Linux — see +[[run-the-daemon]]); every surface (CLI, TUI, this plugin) is a pure client of +that one daemon. On `setup({})` the plugin resolves the socket and connects; if +nothing is serving it, it notifies once with guidance to run `heph daemon +start` (and a dropped connection is retried with a plain reconnect — never a +spawn). The `$HEPH_SOCKET` / `$HEPH_DB` env knobs (and `mise run dev`) point a +dev Neovim at a separate daemon + DB so real data is never touched. + +> **History:** earlier iterations had the plugin auto-spawn and supervise its +> own daemon (`autostart`, self-heal, kill-on-exit). That was removed once the +> CLI became a first-class surface — a daemon owned by one surface can't be +> shared, so lifecycle moved to an explicit service ([[design]] §4). ## Daemon RPC dependencies diff --git a/docs/reference/tech-spec.md b/docs/reference/tech-spec.md index 8ff1ce9..2ebf50d 100644 --- a/docs/reference/tech-spec.md +++ b/docs/reference/tech-spec.md @@ -364,7 +364,7 @@ See [[design]] §5–§7 for the constraints later phases impose on present choi - ✅ **`heph.nvim` slice 11b (§8) — task views:** **`list` enriched** to titled [`RankedTask`] rows (title + `canonical_context_id`, shared `ranked_from_row` with `next`) so the Organizational view needs no N+1 `node.get`. Plugin: **Tactical `next`** + **Organizational `list`** views (rendered scratch buffers, `<CR>` opens a row's canonical-context doc — the node autocmd narrowed to `heph://node/*` so view buffers don't trip it); **task capture**, **set-attention**, **done/drop**, **skip**, **per-task `log` append** — all resolving "the current task" from the buffer (a `task` node, or a context doc via its `canonical-context` backlink); **`vim.ui.select` picker** (`picker.lua`) with Telescope auto-upgrade; `:Heph next/list/capture/attention/done/drop/skip/log/search` subcommands. e2e specs: **capture→next→open context→add/check checklist→done**, and **recurring fresh-checklist** (complete rolls forward in place; the next occurrence is all-unchecked — the §4.4 hard requirement). - ✅ **`heph.nvim` slice 11c (§8) — promotion + Dagger CI:** backend **`task.promote {container_id, item_ref, attention?, project?}`** — mints a committed task from the `item_ref`-th `- [ ]` context item (1-based, document order via a new `extract::context_item_lines`) and rewrites that source line into a `[[link]]` to it. **Wiki-link resolution now excludes canonical-context docs** (`resolve_id`), so `[[Task Title]]` deterministically resolves to the task, not its identically-titled context doc — a general fix surfaced by promotion. Plugin: `:Heph promote`, `promote_under_cursor` (save-if-dirty → `util.context_item_index_at_cursor` mirrors extract's fence rules → `task.promote` → reload). e2e spec (f). **CI via Dagger:** a `test_nvim` function in `.dagger/` 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 shim suite (cargo + cargo-target cache volumes); `build.yaml` calls `dagger call test-nvim`. `run.lua` fails on zero-specs (no false-green) — validated end-to-end (failing spec → Dagger exit 1). - ✅ **`heph.nvim` UX iteration + install (§8) — post-11c, makes the plugin a daily driver:** - - **Plug-and-play managed daemon:** `setup({})` spawns + supervises its own `hephd` (default XDG paths), kills only what it spawned on exit, and **self-heals** (`rpc.call` respawns + retries once on a dropped connection). A daemon you run yourself is respected (spawn only when nothing serves the socket); `autostart=false` ⇒ connect-only. **Bugfix:** `daemon.wait_ready` must not call the rpc probe inside a `vim.wait` predicate (nested `vim.wait` deadlocks Neovim) — bit on the 2nd launch via the prior daemon's stale socket; now a plain-loop probe + socket-unlink on exit, with a regression test. + - **Managed daemon (~~plug-and-play autostart~~ — SUPERSEDED 2026-06 by `heph daemon`, below):** the plugin used to spawn + supervise its own `hephd` and kill it on exit. Removed once the CLI became a first-class surface — a surface-owned daemon can't be shared. Lifecycle is now an explicit OS service ([[design]] §4); all surfaces are connect-only. - **Knowledge-base UX:** **follow-or-create** (`<CR>` on an unresolved `[[link]]` mints the doc + materializes the source backlink), **`:Heph doc`**, **`:Heph home`** (an open-or-create index/landing page), **`:Heph journals`** (recent-days dailies picker with Telescope preview + `@create`). - **Interactive task views:** `:Heph next`/`list` buffers gained `a` add / `d` done / `r` refresh (+ `<CR>` open), with a dimmed key-hint **header line** (virt-lines-above the first row render off-screen — use a real header). - **Dev/installed isolation:** installed `heph`/`hephd` own the default paths; `mise run dev` runs the working-tree daemon on `.dev/` paths; `$HEPH_SOCKET`/`$HEPH_DB` point a dev Neovim at it.