Commit graph

97 commits

Author SHA1 Message Date
544c8bba0e C2(hephd-self-update): impl systemd Restart=always for clean-exit respawn
Self-restart works by exiting cleanly and letting the service manager
respawn the new binary. launchd already does this (KeepAlive=true), but
the systemd user unit was Restart=on-failure, which ignores a clean
exit (code 0). Switch to Restart=always + RestartSec=1, update the unit
test, and note in run-the-daemon that existing Linux installs must
`heph daemon restart` once to regenerate the unit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:44:36 -07:00
758854478b C2(hephd-self-update): close self-update-poll-loop
All checks were successful
Build / validate (pull_request) Successful in 3m56s
Notify-only poller landed: ticks on the interval, logs when a newer
release is available. The daemon now self-reports update availability.
2026-06-04 13:42:48 -07:00
35569b0649 C2(hephd-self-update): close self-update-opt-in-flag
--self-update flag + config plumbing landed; opt-in observable via the
startup log line.
2026-06-04 13:40:47 -07:00
4a0094f955 C2(hephd-self-update): close release-poll-version-check
Version-compare + forge release parsing landed and unit-tested.
2026-06-04 13:37:14 -07:00
e6524fddbb C2(hephd-self-update): plan add goal + prerequisite cards for hephd self-update
All checks were successful
Build / validate (pull_request) Successful in 5m23s
Kick off the C2 Mikado chain for an opt-in (default-off) hephd
self-update mode (forge-poll -> cargo install from tag -> self-restart).
Goal card plus eight prerequisite cards, indexed from how-to.md:

  release-poll-version-check, self-update-opt-in-flag (leaves)
    -> self-update-poll-loop                 (notify-only core)
  service-env-forge-access (leaf, the cargo/forge blocker)
    + self-update-poll-loop -> cargo-install-from-tag
  service-respawn-on-clean-exit (leaf, systemd Restart=always)
    + cargo-install-from-tag -> self-restart-after-update
  verify-hub-dropout-resilience (leaf, lock in the base-case guarantee)

Grounded in research of hephd's sync loop, daemon lifecycle, the
launchd/systemd service templates, and the forge releases API.
Captured from Hephaestus task 01KTA2NSNRYT902HC3VRW00S1J.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:20:46 -07:00
Forgejo Actions
254c83036b Update changelog for v1.0.3 [skip ci] 2026-06-04 12:42:46 -07:00
7f48a2a1c5 infra: add cargo-fmt-check pre-push prek hook (mirror CI)
All checks were successful
Build / validate (pull_request) Successful in 5m25s
The cargo-fmt failure on this PR slipped to CI because the pre-commit
prek hooks were never installed in the working clone. The existing
cargo-fmt hook reformats in place but only when it runs. Add a
pre-push cargo-fmt-check hook (`cargo fmt --all --check`) that mirrors
CI's Dagger `check` step exactly, so an unformatted commit is blocked
locally before it can reach the runner — even if the pre-commit hook was
skipped or not installed. Filtered to .rs pushes so Rust-free pushes pay
nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:36:12 -07:00
7914232ec4 feat: add a version RPC returning the daemon build version
Some checks failed
Build / validate (pull_request) Failing after 15s
RPC clients (the hephaestus.nvim plugin, for its `:Heph version`
command) had no way to learn which hephd they are talking to — `health`
returns counts, not a version. Add a tiny `version` method returning
`{ version: heph_core::VERSION }`, the same `X.Y.Z (sha)` string the
binaries print for --version. No store access needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:13:47 -07:00
babdb21c0a feat: heph context <task-id> reads/edits a task's context doc by id
Editing a task's canonical-context doc body previously meant looking up
its `canonical_context_id` (e.g. via `heph list --json`) and then
`heph node update <doc-id> --body`. Add a `heph context <task-id>`
command that resolves the canonical-context doc from the task's outgoing
links and:

  * prints the body with no flag,
  * `--body <text>` replaces it (`-` reads stdin, matching `node update`),
  * `--append <text>` adds a blank-line-separated paragraph.

Errors clearly when the id has no canonical-context doc (e.g. a plain
doc node rather than a task). Purely a client-side CLI convenience — no
new RPC.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:09:53 -07:00
fc25f6ac51 feat: --project arg is case-insensitive / prefix-fuzzy when unambiguous
The `--project <name>` argument matched titles case-sensitively and
exactly, so `--project hephaestus` or `--project heph` failed against a
`Hephaestus` project. Make project-name resolution forgiving but
deterministic, via a tiered match in `resolve_project_id`:

  1. exact (case-sensitive) — the historical behavior; always wins
  2. case-insensitive exact — only when unambiguous
  3. case-insensitive prefix — only when unambiguous

Ambiguous fuzzy matches resolve to None (callers report "no project
named X") rather than silently picking one. This single resolver already
backed `heph list --project` (via project_scope); route the CLI's
task/edit/promote/parent path through it too with a new `project.resolve`
RPC + `Store::resolve_project`, so every `--project` surface behaves the
same.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:57:37 -07:00
58a5544d44 feat: heph-tui --version reports version + build SHA
heph-tui was the one daily-driver binary that did not answer --version.
Add the same clap `version = heph_core::VERSION` attribute that heph and
hephd already carry, so all three report `X.Y.Z (sha)` consistently.

Addresses the heph-tui half of the cross-binary --version task.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:52:52 -07:00
Forgejo Actions
67627a0930 Update changelog for v1.0.2 [skip ci] 2026-06-04 10:22:04 -07:00
1603b37f74 fix: --version no longer false-positives -dirty on clean release installs
All checks were successful
Build / validate (push) Successful in 6m13s
The build-time dirty check used `git status --porcelain`, which counts
untracked files — including cargo's own `.cargo-ok` marker in a
`cargo install --git` checkout — so a clean tagged build reported e.g.
`1.0.1 (bcab3c16b-dirty)`. Use `git diff --quiet HEAD` (tracked changes only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:20:23 -07:00
Forgejo Actions
defaa56b23 Update changelog for v1.0.1 [skip ci] 2026-06-04 10:14:40 -07:00
521e5d62df docs: install-heph defaults to the v1.0.0 release tag (v1 is out)
All checks were successful
Build / validate (pull_request) Successful in 5m5s
Replace the pre-release 'install from feature/v1-prototype' instructions with
`--tag v1.0.0` as the default, and document `--branch main` as the track-
unreleased-work alternative.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:47:23 -07:00
598dc59580 fix: --version reports release version + build SHA; release tags a version-bump commit
Some checks failed
Build / validate (pull_request) Has been cancelled
heph-core gains a build.rs that captures the short git SHA and a
`heph_core::VERSION` const ("<crate-version> (<sha>)"); heph and hephd use it
for clap's --version. The crate version stays sourced from Cargo.toml.

release.yaml now bumps the workspace version into Cargo.toml + Cargo.lock on a
commit that only the tag points at, tags it manually, and pushes just the tag —
so cargo install --git --tag vX.Y.Z reports the real version while main stays at
0.0.0. The changelog commit moved ahead of the tag so the release includes its
own changelog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:43:10 -07:00
c266f7ee4a docs: release bumps Cargo.toml/lock on a tag-only commit; --version reports X.Y.Z (sha)
Documents the desired end state before the code change (C1 docs-first):
- release.yaml bumps the workspace version into a commit only the tag points
  at, keeping main at 0.0.0
- heph/hephd --version will report the release version plus the build SHA

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:37:14 -07:00
Forgejo Actions
774770ce39 Update changelog for v1.0.0 [skip ci] 2026-06-04 08:23:35 -07:00
ab6701d610 C0: pin Quartz docs build to v4.5.2 (v5.0.0 broke build)
All checks were successful
Build / validate (push) Successful in 6m1s
The Dagger build_docs pipeline cloned Quartz from the default branch
unpinned. Quartz v5.0.0 restructured its config layout (.quartz/plugins,
../quartz imports), breaking the docs build against our existing
quartz.config.ts / quartz.layout.ts. Pin the clone to the last v4
release (v4.5.2) to restore known-good behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 08:11:59 -07:00
b6a96013ca fix(ci): vendor libdbus in the Linux keyring store (no system libdbus-1-dev)
All checks were successful
Build / validate (pull_request) Successful in 8m11s
The rust:1-bookworm CI image has no libdbus-1-dev, so libdbus-sys's
pkg-config build failed. Enable the dbus store's `vendored` feature to build
libdbus from bundled source (self-contained, the proven path the earlier
keyring-4 build used). `crypto-rust` keeps it OpenSSL-free; openssl-sys is only
an inert lock entry (the conditional `openssl?/vendored` reference), compiled
nowhere. Linux footprint unchanged at 235 crates; vendored libdbus is a
build-time C compile, not new crates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 07:32:41 -07:00
6ba94119e4 infra: slim the keyring dependency (keyring meta-crate -> keyring-core + one store/OS)
Some checks failed
Build / validate (pull_request) Failing after 45s
keyring 4's `keyring` meta-crate has no feature gating and compiles every
platform credential backend for the target. On Linux that dragged in the zbus
async stack, a redundant libdbus secret-service, the keyutils store, a
sqlite/zstd db-keystore, and OpenSSL (~290 crates in its subtree) — a real cost
on the RAM/CPU-constrained CI runner building with CARGO_BUILD_JOBS=1.

Depend on keyring-core (the API) + exactly one store crate per OS instead:
- macOS  -> apple-native-keyring-store (keychain feature)
- Linux  -> dbus-secret-service-keyring-store (crypto-rust; libdbus, no openssl)

oauth.rs registers the per-target store as the keyring-core default itself
(replacing keyring::use_native_store). Runtime behavior is unchanged (tokens
still go to the macOS Keychain / Linux Secret Service).

hephd's Linux dependency graph: 401 -> 235 crates (-166), dropping the zbus
ecosystem and two C builds (zstd-sys, plus the redundant secret-service path).

macOS builds + the full suite are green here (228 tests, clippy -D warnings,
fmt, prek); the Linux store path is CI-verified (API confirmed from source).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 07:26:39 -07:00
a0b04eefda feat: multi-tenancy seam (resolve_owner) + hub-setup how-to (v1 prep)
All checks were successful
Build / validate (pull_request) Successful in 10m34s
The cheap "seam" that keeps the single-owner hub from calcifying, ahead of
the gilbert -> indri bring-up:

- Replace the single-tenant gate `Store::authorize_owner_sub(sub) -> bool`
  with `resolve_owner(sub) -> Option<owner_id>`. The hub auth middleware now
  resolves the token's identity to the owner it may act as (Some -> allow,
  None -> 403). Behavior is identical for the single-owner hub (claim-on-first;
  strangers still 403), but the contract no longer assumes one global owner, so
  serving N owners later is additive, not a rewrite. The per-request owner is
  marked at the exact line where downstream scoping wires through.
- New how-to docs/how-to/set-up-sync-hub.md: stand up the hub and connect an
  existing device as an offline-capable spoke, the data-safe way (Path A: the
  hub adopts the device's identity rather than rewriting the device).

The decision (cheap seam now, defer full multi-tenancy + adoption rewrite) is
recorded in the Adoption + multi-tenant task's context doc. Two enabler gaps
the how-to surfaced (heph daemon hub/spoke service flags; Path-A seeding tool)
are filed as Hephaestus tasks.

Green: 228 tests, clippy -D warnings + fmt + prek clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 07:08:39 -07:00
9fa9a9b9c1 Merge remote-tracking branch 'origin/main' into feature/extract-heph-nvim
Some checks failed
Build / validate (pull_request) Failing after 14m23s
# Conflicts:
#	.dagger/src/hephaestus_ci/main.py
2026-06-03 23:24:10 -07:00
d36ed18590 infra: extract heph.nvim into its own forge repo
Some checks failed
Build / validate (pull_request) Failing after 1m12s
The Neovim plugin now lives at eblume/hephaestus.nvim (plugin at the repo
root). Remove heph.nvim/ from the monorepo and the build/test wiring that
referenced it:

- Dagger: drop the test_nvim function + the pinned-Neovim NVIM_VERSION
- build.yaml: drop the `dagger call test-nvim` step
- drop the mise run test-nvim task and .stylua.toml + the stylua prek hook
  (no Lua remains in the monorepo)
- install-heph.md: install via a plain lazy.nvim spec pointing at the
  plugin repo over SSH (no more local-dir checkout hack)
- README / AGENTS / heph-nvim.md: note the surface lives in its own repo

The CLI/TUI -> nvim integration is unchanged (they shell out to `nvim`
expecting the heph plugin installed). The v1-prototype tech-spec §14 build
record and prior changelog fragments are left as frozen history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 22:42:39 -07:00
0c502834c2 infra: pre-v1 dependency-refresh sweep + drop fs4 + remove dead build hook
Some checks failed
Build / validate (pull_request) Failing after 3m25s
Bump all external crates to latest stable ahead of v1.0.0 (tech-spec §14.9):
keyring 3→4 (the keyring_core split + register-the-native-store model),
rusqlite 0.32→0.40, ratatui 0.29→0.30, rrule 0.13→0.14, yrs 0.26→0.27,
plus a cargo update for semver-compatible bumps.

keyring 4 moves Entry/Error into keyring_core and requires a credential
store to be registered before use; KeyringTokenStore now registers the
OS-native store once (lazily, via Once) and uses keyring_core types.
not_keyutils=true so Linux prefers Secret Service over the logout-wiped
kernel keyutils store.

Drop the fs4 dependency in favor of std::fs::File::try_lock (stable since
Rust 1.89); raise workspace MSRV 1.85→1.89. Remove the orphaned
.forgejo/scripts/build hook — CI invokes Dagger directly.

Green: 228 Rust tests + 25 heph.nvim headless e2e, clippy -D warnings + fmt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 22:19:13 -07:00
6ddc9b83bf fix: clippy single_match in heph-tui + add clippy pre-commit hook
All checks were successful
Build / validate (push) Successful in 9m35s
CI on main failed on a clippy::single_match lint in the heph-tui sidebar
key handler. Rewrite as `if let`. Also add a `cargo clippy -D warnings`
prek hook mirroring cargo-fmt, so lints are caught locally before CI
(prek previously only ran cargo fmt).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 20:53:50 -07:00
ec522f49ec infra(prek): add cargo fmt as a pre-commit hook
Some checks failed
Build / validate (pull_request) Failing after 18s
Run `cargo fmt --all` in place over the workspace on any staged .rs change,
matching the repo's other in-place formatters (ruff-format, stylua, shfmt).
Unformatted Rust now fails the commit locally (it reformats + reports
"files were modified"), so the fmt-dirty commits that slipped through this
session can't recur. CI still enforces `cargo fmt --check` via Dagger as the
backstop. Verified: passes clean, catches + fixes a deviation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 20:46:55 -07:00
dce3519345 feat: heph list --project <name> + --json; thin AGENTS.md
Some checks failed
Build / validate (pull_request) Failing after 3m21s
`heph list --project <name>` lists a project's outstanding tasks by name
(subtree-expanded, resolved server-side via a new project.scope path that
reuses the view machinery; errors on unknown names). `--json` prints raw
rows — node_id, canonical_context_id, attention/state/do_date/late_on/
recurrence/project_id — for scripting and agents. Store::project_scope on
the trait + LocalStore + RemoteStore; new project.scope RPC and a flattened
ListParams so `list` accepts an optional project name. Test covers
resolve-by-name + unknown-name error.

AGENTS.md thinned to tight command/pattern sections: dropped the historical
parity narrative and the verbose roadmap section; added a "Working state"
section documenting `heph list --project Hephaestus [--json]` as the way to
inspect heph's self-hosted roadmap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 20:38:57 -07:00
6514296b87 docs: reframe tech-spec as historical; heph self-hosts its roadmap
v1 reached Todoist feature-parity, so remaining/future work is now tracked
in heph itself — tasks in the Hephaestus project (heph view ondeck) — not in
a doc. Renamed docs/reference/tech-spec.md -> v1-prototype-tech-spec.md and
rewrote all 27 [[tech-spec]] wiki-links + README/changelog path refs (docs
checks green). Retitled + bannered the spec as a historical v1 build record
and froze its §14 tracker. AGENTS.md gains a "Planning future work" section
(capture via `heph task --project Hephaestus`, triage in heph-tui On Deck);
README status reflects parity + the three daily-driver surfaces. The design
doc remains the living rationale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 20:19:35 -07:00
9511f6a009 feat(tui): pane-specific keys, undo/redo, project delete, sidebar refresh (§8.1)
Some checks failed
Build / validate (pull_request) Failing after 12s
Triage gestures (x/d/S/A/b/e/m/D) now fire only when the task pane is
focused, so a stray key while in the sidebar can't drop or delete a task;
hints are focus-aware. `u` undoes the last triage action (drop/done/skip/
attention/move) and Ctrl-z redoes it, restoring from a pre-action snapshot
(multi-level, cap 200; tombstone-delete excluded — no restore path yet). `D`
in the sidebar deletes the highlighted project (y/N), unfiling its tasks to
the Inbox. The sidebar's Projects section now rebuilds after create/delete,
so a new project appears without a restart. Tests cover undo/redo, the
empty-undo no-op, and sidebar project delete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:37:16 -07:00
9c932c8d9a feat(tui): fzf-style move-to-project picker + create-project (§8.1)
The `m` move-to-project overlay is now a filterable picker: a prompt line
narrows the project list by fuzzy subsequence match as you type (↑/↓ or
Ctrl-n/p move, Enter selects, Esc cancels), so there's no scrolling a long
list. When the filter names no existing project, a "+ New project" row
creates it and files the task there in one step (Backend::create_project →
node.create). Tests cover fuzzy narrowing and the create-then-file flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:20:10 -07:00
01ae561a74 feat: Inbox view — outstanding tasks with no project (§8.2)
A sixth built-in filter view (listed below On Deck) showing every
outstanding task filed under no project — the capture inbox to triage. New
ListFilter.unfiled predicate kept purely in matches(); the inbox ViewSpec is
un-gated (no attention/do-date filter) so nothing hides from triage. Surfaces
automatically in the heph-tui sidebar and `heph view`. Tests cover the
predicate and the view spec; navigation tests updated for the 6th view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:12:33 -07:00
0c45bbb5f9 feat: heph-quickadd — global ⌘' quick-capture popover (§8)
A macOS global quick-capture popover to retire Todoist. New `heph-quickadd`
crate: an always-warm eframe/egui agent that registers ⌘' (global-hotkey,
Carbon — no Accessibility permission) and toggles a hidden, pre-created
window visible + focused on press — never spawning on the keypress, so it's
a muscle reflex. A single field live-parses Todoist-style inline syntax via
the shared parser; chips show ⚑ attention · 📁 project ·  do-date · ↻
recurrence as you type. Enter saves optimistically (hide now, task.create
on a bg thread; a failed RPC re-shows with the text restored). #project
autocomplete (Tab/↑↓/click; focus-locked so Tab completes instead of
traversing). Example hints rotate, fading in only after ~2s idle.

Parser lifted from heph-tui into hephd's lib (hephd::quickadd) so the TUI
and the popover share one parser. hephd supervises the helper as a child in
local mode on macOS (opt-in HEPH_QUICKADD=1, set by the installed launchd
plist) — one service to manage, no second launch agent; the helper
self-exits when orphaned so killing hephd leaves nothing behind.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:12:24 -07:00
44d6847fae feat(tui): <Enter> opens the context editor; reorder views (§8.1/§8.2)
All checks were successful
Build / validate (pull_request) Successful in 3m57s
- `<Enter>` now opens the selected task's context doc in nvim (App::enter:
  from the sidebar it drills into the task list first); the `o` binding is
  retired. Hint line updated.
- BUILTIN_VIEWS reordered to the owner's preference — Top of Mind, Tasks,
  Work Tasks, Chores, On Deck — which drives the TUI sidebar and
  `heph view`. Tests that walked to On Deck by a fixed offset now seek it
  by title.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:42:08 -07:00
2fc48a1aa9 feat: node.linkable — first-class link targets for the [[ picker (§8.4)
Some checks failed
Build / validate (pull_request) Failing after 8s
The picker listed every node, so each task showed up twice (itself + its
same-titled canonical-context doc) plus tag/log noise — and the new
preview made the duplicates look identical. New `Store::list_linkable_nodes`
/ `node.linkable` returns non-tombstoned nodes minus `tag`s and the docs
that are a task's canonical-context or log attachment (you link the task,
not its body). The Telescope picker now sources from it.

Tests: a socket test (5 nodes → 2 linkable: task + standalone doc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:15:29 -07:00
1737f8c266 feat(nvim): preview pane in the Telescope [[ link picker (§8.4)
Some checks failed
Build / validate (pull_request) Failing after 8s
Add a buffer previewer that shows the highlighted node's body as you
filter — for a task (no body of its own), it previews the canonical-
context doc instead. RPC-fed (heph nodes aren't files, unlike
obsidian.nvim's notes), pcall-guarded so a fetch miss just blanks the
preview. Reuses the global fzy_native sorter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:10:44 -07:00
d178a657e0 feat(nvim): live Telescope filter for the [[ link picker (§8.4)
Some checks failed
Build / validate (pull_request) Failing after 10s
Replace the "guess a query, then filter a frozen result set" flow with a
live fuzzy filter over every node when Telescope is present: type to
narrow `node.list`, <CR> inserts the highlighted node, <C-x> creates a
doc named the current prompt (a miss flows straight into making it). The
search-then-`vim.ui.select` two-step stays as the no-Telescope fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:03:02 -07:00
b112b0d7c1 feat: heph migrate-links — rewrite legacy [[Name]] links to [[id]] (§8.4)
Some checks failed
Build / validate (pull_request) Failing after 11s
`wikilink::to_ids` rewrites name-addressed links to the canonical id
(id-first resolve: an already-id target is left alone, a name → its id
with any label preserved). `Store::migrate_wikilinks_to_ids` runs it over
every body and re-saves through update_node (which collapses + materializes
by id); idempotent. Surfaced as the `migrate.wikilinks` RPC + RemoteStore
forward + the `heph migrate-links` CLI command (not auto-run — the owner
runs it once per store).

Name-resolution + the canonical-context hack stay for now so legacy links
keep resolving pre-migration; retiring them is a later tidy. Tests:
to_ids unit + a heph-core migrate integration (rewrite + materialize +
idempotency).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:40:57 -07:00
fd010a7066 feat(nvim): conceal wiki-link ids to styled name hyperlinks (§8.4)
`conceal.lua` hides the `[[id|` prefix and `]]` suffix with conceal
extmarks (refreshed on edit), leaving the label as a styled `HephLink`;
`conceallevel=2` + empty `concealcursor` reveal the raw `[[id|Name]]` on
the cursor's line so it stays editable. The `[[` picker now inserts the
labelled `[[id|Name]]` form (readable + conceal-ready; collapses to bare
on save). e2e asserts the conceal extmarks + conceallevel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:35:24 -07:00
ef2081fd8b feat(core,hephd): wiki-link expand-on-read / collapse-on-write (§8.4)
Keep canonical `[[NODEID]]` links readable without storing names. New pure
`heph-core::wikilink` (injected id→title): `expand` turns a bare `[[id]]`
into `[[id|Current Name]]`, `collapse` turns a name-matching `[[id|text]]`
back to bare (a custom label is preserved as an override).

- `node.get` expands on every read (nvim buffer + TUI preview both
  readable), then prepends frontmatter when asked.
- `update_node` strips frontmatter, then collapses links, then CRDT-diffs
  — so neither projection ever persists and an unchanged read→write is a
  no-op to the bare id.

Tests: wikilink unit (expand/collapse/round-trip), a heph-core collapse
+ materialize-by-id integration test, and a socket expand→collapse
round-trip. `heph export` still emits raw ids (later polish).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:32:24 -07:00
4e8f6743cf feat: wiki-links by id — id-first resolution + heph.nvim [[ picker (§8.4)
Some checks failed
Build / validate (pull_request) Failing after 6m34s
Backend: `links::resolve_id` now checks for an exact live node id before
alias/title, so a canonical `[[NODEID]]` link resolves to its node and
can't be shadowed by a like-named node. Legacy `[[Name]]` links still
resolve by name (until the migration), so this is additive.

heph.nvim: `link.insert` (bound to insert-mode `[[` and `:Heph link`)
searches via the `search` RPC and inserts `[[NODEID]]`, with a "+ Create
new doc" entry; `<CR>` follow resolves the id directly. e2e covers
search→insert→materialize and the create path.

Remaining (§8.4): read-expansion/conceal display + the one-time
[[Title]]→[[NODEID]] migration (then retire name-resolution + the hack).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:07:46 -07:00
a030ad3034 feat(nvim): italicize inline #hashtags in node buffers (§8.3)
A buffer-local syntax match (`HephHashtag`, italic, `default`-overridable)
highlights whitespace-prefixed #hashtags so they're visually obvious —
mirroring the save-time tag detection (a `# heading` doesn't match).
Attached alongside the <CR> link follow. Syntax match is the right-sized
tool here (treesitter has no hashtag node; extmark conceal is reserved
for the upcoming [[link]] display layer). e2e asserts the italic hl group.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:01:36 -07:00
8dc98dc9c1 feat(nvim): inline #hashtags become tags on save (§8.3)
Some checks failed
Build / validate (pull_request) Failing after 4m57s
On save, whitespace-prefixed `#hashtags` in a node's body are unioned
into its tag set (via `frontmatter.hashtags` + the existing tag diff), so
you can tag a note by writing `#kitchen` inline. A markdown `# heading`
has a space after the `#`, so it never matches. e2e covers it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:57:25 -07:00
d85ce3362f infra(nvim): add stylua formatter + prek hook; normalize heph.nvim Lua
A `.stylua.toml` (Spaces/2, else stylua defaults) + a `stylua-system`
prek hook make Lua whitespace formatter-enforced (the repo had no Lua
formatter, so style was hand-maintained and drifted). Normalized the
three non-conformant files in passing. 21 nvim e2e specs still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:55:07 -07:00
0e9cfc1fd7 feat(nvim): frontmatter edit surface — diff block into RPCs on save (§8.3)
Some checks failed
Build / validate (pull_request) Failing after 5m1s
Node buffers now open with the editable YAML frontmatter block on top
(node.get {frontmatter: true}); on :w, `frontmatter.lua` parses the
block, diffs it against what was rendered, and routes each changed field
to the right RPC:
- title → node.update rename
- attention → task.set_attention
- do_date/late_on/recurrence → task.set_schedule (YYYY-MM-DD → local-ms;
  a removed line clears via null)
- project → task.set_project (resolved by name)
- tags → tag.add / tag.remove
A mistyped state surfaces the daemon's validation error; a buffer with no
block edits no metadata (deleting the block can't wipe tags). Body rides
node.update as before (the store strips any echoed frontmatter).

Body-position features are content-relative, so the prepended block
doesn't disturb them; e2e specs that targeted absolute line 1 now locate
body lines by content via a new `h.find` helper. New frontmatter_spec
covers render + the full diff→RPC round-trip. 21 nvim e2e specs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:44:39 -07:00
ef56c5d5f2 feat(core,hephd): frontmatter projection — render on read, strip on write (§8.3)
The store-side half of the frontmatter edit surface:
- heph-core `frontmatter::strip` runs in `update_node` before the yrs
  diff, so frontmatter never enters the body or CRDT. Conservative (only
  a leading `---` block whose first line is a YAML key; a prose hrule
  survives) and idempotent → read→write round-trip is a no-op.
- hephd `frontmatter::render` (local-tz dates via new `datespec::fmt_iso`)
  behind `node.get {frontmatter: true}`: id/kind/title/tags, and for a
  task or its canonical-context doc the owning task's scalars + a `task:`
  ref. Subject-task + project-name resolution in dispatch.

Safe against any client (inbound frontmatter always stripped). Tests:
strip unit (incl. hrule/idempotency), render unit, socket round-trip +
task-context-doc projection. The heph.nvim diff-into-RPCs layer is next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:32:59 -07:00
4cdf0de64c feat(core): tags — canonical tag nodes + OR-set tagging (§4, §8.3)
Some checks failed
Build / validate (pull_request) Failing after 4m35s
A tag is a `tag`-kind node with a deterministic id in (owner, name)
(`tag:<owner>:<name>`, like the journal), so a name is one canonical tag
shared across nodes and replicas converge with no duplicates. Tagging is
an OR-set `tagged` link (mirroring in-project):

- heph-core: `nodes::open_or_create_tag` (bodyless, deterministic id),
  `tags::{add,remove,of}`, and `Store::{add_tag,remove_tag,tags_of}`.
  Enumerate all tags via the existing `list_nodes(Tag)`.
- hephd: `tag.add`/`tag.remove`/`tag.list` RPCs + RemoteStore forwarding.
- heph: `heph tag add|rm|list` (a node's tags, or every tag).

Names are trimmed; canonical case/spelling normalization is deferred to
the zk import. Unblocks the `tags:` line of the frontmatter surface.
Tests: core add/dedupe/remove/canonical-id/trim/missing-node + a socket
add/list/enumerate/remove test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:18:51 -07:00
9d84eb7427 feat(nvim): do/late date chip (+ ↻) on task-view rows (§8)
Some checks failed
Build / validate (pull_request) Failing after 1m17s
`:Heph next`/`list` rows now render a compact relative do/late date chip
(today/tomorrow/yesterday/MM-DD/YYYY-MM-DD, mirroring heph-tui's fmt) and
a recurrence ↻, so scheduling is visible at a glance. `<CR>` already jumps
to a row's canonical-context doc. e2e: a do-date-chip render assertion.

Completes the §14 item-2 task-list UX wave.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:05:02 -07:00
4f291ce373 feat(tui): s sort toggle — default vs project-grouped (§8.1)
`s` flips the task list between two orders:
- default: attention (red→orange→white→blue) → most-overdue (desc) →
  project name → created_at (FIFO)
- project: project first, with dimmed ──── Name ──── separators riding
  atop each group's first task (the cursor only lands on real tasks)

The view filter still runs before the sort. Pure comparator (`cmp_tasks`/
`sort_tasks`, today injected) with unit tests for both modes + a
navigation test for the toggle. `skip` moved from `s` to `S` to free `s`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:01:51 -07:00
ecfe64435c feat(tui): attention flag column + project-colored bullets + scrollbar (§8.1)
Each task row now leads with a colored attention flag (⚑ for
red/orange/blue, blank for white/none) and a project-colored bullet (●).
The bullet color is derived stably from the project id (FNV-1a → HSL →
truecolor RGB) so it survives projects being added/removed; a per-project
override on the model is a later refinement. The glyph shape is reserved
for future semantics.

The task list also gains a scrollbar and ListState-driven
scroll-to-visible so a selected task below the fold stays reachable.

Tests: fmt::project_color determinism unit; a flag-glyph render assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:53:19 -07:00