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>
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>
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>
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>
New crate crates/heph-tui: a ratatui terminal agenda, thin client of the
hephd unix socket (never touches SQLite, same as heph.nvim). The next big
surface — the interactive triage UI the §6.2.1 Todoist study calls for.
- 3-pane layout: sidebar (the five §8.2 filter views + projects), task list
(attention-colored rows with compact human do/late dates), and a preview
pane (the highlighted task's canonical-context doc body + log tail).
- App state is generic over a `Backend` seam, so navigation/selection logic
is unit-testable without a terminal or daemon; `ClientBackend` forwards to
the socket. Rendering is a pure `ui::render(frame, &app)`.
- Navigation: j/k within the focused pane, Tab / h / l to move focus,
selecting a sidebar source reloads the list, moving the task cursor
refreshes the preview. r refresh, q quit.
- Socket resolution: --socket flag, then $HEPH_SOCKET, then the standard
runtime path (the TUI honors the env var the CLI doesn't).
Tests: a headless TestBackend render against a real spawned daemon (asserts
views/projects/tasks/preview paint, and Top of Mind excludes blue), plus
in-memory navigation unit tests. 8 heph-tui tests; clippy/fmt clean.
Mutations (add/done/attention/reschedule/blue) + nvim handoff land in T2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Close the auth loop: clients obtain a bearer token and present it to the
hub (tech-spec §13).
- oauth module: DeviceFlow (RFC 8628 — discover, start, poll handling
authorization_pending/slow_down, refresh) + StoredToken + TokenStore
(OS keyring via `keyring`, in-memory for tests) + current_bearer (loads
and refreshes-on-expiry).
- heph auth login/logout: runs the device flow, prints the verification
URL + user code, caches the token in the keyring.
- sync_once gains a bearer arg; the daemon (Daemon::spawn_sync_loop +
sync.now) obtains it via current_bearer; RemoteStore attaches it to /rpc.
--oidc-issuer/--oidc-client-id configure the spoke/client.
- Fix a latent panic: reqwest::blocking spins its own runtime and panics
inside the daemon's spawn_blocking pool. All blocking auth/proxy HTTP
(OidcVerifier JWKS, DeviceFlow, RemoteStore) now uses runtime-free `ureq`;
async reqwest remains only for sync_once. (Caught by the new e2e test.)
- Tests (offline): device flow + refresh + token store vs a mock OAuth
server; a full spoke->authenticated-hub loop (valid token accepted,
missing token rejected) signed by a runtime-generated RSA key.
112 tests green; clippy -D warnings + fmt + prek clean. Slice 10 (auth)
complete; next is heph.nvim.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Authenticate op exchange at the network boundary (tech-spec §13). The hub
now requires a valid OIDC bearer token on /sync/* and /rpc; local mode is
unchanged (no auth).
- heph-core: Store::authorize_owner_sub — single-tenant gate that claims the
owner's oidc_sub on first sight, then authorizes only that sub (403 for any
other identity). LocalStore impl over users.oidc_sub; RemoteStore stub.
- hephd auth module: TokenVerifier trait (mockable seam) + OidcVerifier
(jsonwebtoken, rust_crypto). Strict validation: RS256 pinned, exact iss +
aud, exp/nbf, required sub; JWKS discovered + cached, refetched on unknown
kid (rotation). Claims/AuthError.
- Hub router takes Option<verifier>; an axum middleware on every route
extracts the Bearer token, verifies it off the async worker, and runs the
owner gate — 401 missing/invalid, 403 wrong identity, 503 IdP-unreachable.
Open (no auth) when unconfigured, for local dev.
- main: --oidc-issuer/--oidc-audience enable the hub verifier (server mode).
- Security tests, all offline: stub-verifier middleware (missing/bad/valid +
owner gate) and an adversarial battery driving OidcVerifier against an
in-process mock IdP — rejects expired, wrong iss/aud, unknown kid, tampered
signature, alg confusion (HS256/none), and missing sub. The RSA key + JWKS
are generated at runtime (rsa/rand/base64 dev-deps) so no key is committed.
- tech-spec: add an end-of-v1 dependency-refresh pass to the roadmap.
108 tests green; clippy -D warnings + fmt + prek clean. Next: client-side
device-code login + keyring (10b).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the online-only escape hatch — a no-replica daemon that proxies every
Store call to a server over HTTP (tech-spec §3.1).
- Daemon is now generic over the backing store (Arc<Mutex<dyn Store +
Send>>), so the same unix-socket surface fronts either a LocalStore
(local/server) or a RemoteStore (client). sync::router/sync_once and the
Ctx follow suit.
- New POST /rpc route on the hub router runs the full rpc::dispatch over
HTTP (result-xor-error body, always 200). dispatch gains task.get and
links.add so the proxied API is complete.
- RemoteStore (hephd): implements heph_core::Store by forwarding each call
to /rpc via a blocking reqwest client (Store is sync; the daemon only
calls it from the blocking pool). Error::Remote for transport failures;
NOT_FOUND is preserved as Error::NodeNotFound. Sync primitives are
stubbed (a client keeps no op-log).
- main: --mode client + --server-url; client skips the file lock and opens
no LocalStore.
- tests/client_mode.rs: a RemoteStore drives node/task/search/list/health
against a real HTTP server, and not-found maps back correctly.
102 tests green; clippy -D warnings + fmt + prek clean. Next: OIDC auth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the existing merge engine over the network so the everyday config
(local + hub_url) syncs through a hub. Transport ratified = axum HTTP/JSON
(tech-spec §6.1, §12).
- heph-core: SyncCursors model + Store::sync_state/record_sync over the
sync_state table (per-peer push/pull HLC cursors). Incremental, so each
exchange transfers only the tail.
- hephd::sync: the hub router (POST /sync/push, GET /sync/pull?after=<hlc>)
served from the shared LocalStore, and sync_once — a spoke's pull-then-
merge, then push-tail exchange, advancing the cursors. Idempotent: a
re-pushed op the hub already has is a no-op.
- Daemon carries optional hub config; sync.now/sync.status handled at the
daemon (they need the hub transport the store can't reach). conflicts.
list/resolve now reachable over the unix socket too.
- main: --mode local|server, --hub-url, --http-addr. server mode binds the
hub HTTP endpoint on the same store; a local+hub_url spoke background-
syncs on a 30s interval.
- tests/sync_http.rs: two spokes converge through a real-HTTP hub on an
ephemeral port — node propagation and a divergent-scalar conflict.
Unauthenticated/single-owner for now; OIDC + per-user scoping is slice 10,
client mode + RemoteStore is 9b. 100 tests green; clippy -D warnings + fmt
+ prek clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace last-writer-wins for node bodies with the yrs text CRDT, so
concurrent edits to different regions of a body merge instead of one
clobbering the other (tech-spec §5, §12).
- New crate::crdt module wraps yrs: a device authors under a stable
client_id derived from its sync origin; a whole-buffer write is diffed
(common prefix/suffix, char-boundary safe) into the doc and the yrs
delta is captured; merge is commutative/idempotent.
- nodes::create/update/journal maintain the body_crdt BLOB and put the
yrs delta in the node.create/node.set op payload (body_crdt field).
Recurrence's local checklist reset goes through the same path to keep
body and body_crdt consistent (still records no op, as before).
- apply::node_upsert merges the body delta through the CRDT regardless of
HLC order and drops body-conflict recording; titles + task scalars stay
LWW with the conflict queue.
- convergence test now asserts disjoint concurrent body edits both survive
and enqueue no conflict.
97 tests green; clippy -D warnings + fmt + prek clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 6 (tech-spec §3, §6, §10). First async component — the per-device
daemon in local mode.
- `LockGuard`: exclusive advisory flock on a sidecar `<db>.lock`; a second
acquire fails and releases on drop (the §3.1 lock handoff).
- JSON-RPC (line-delimited): `rpc::dispatch` maps node/task/next/links/log
methods onto the heph-core Store; `Daemon::serve` accepts unix-socket
connections and runs dispatch on tokio's blocking pool behind an
Arc<Mutex<LocalStore>> (DB never touches an async worker).
- Synchronous `Client` for surfaces/CLI; `hephd` binary (clap) opens the
store under lock and serves the default socket.
- heph-core model/ranking types are now serde-(de)serializable; added
node.tombstone + Store::tombstone_node.
Tests: 2 lock unit tests + 5 real-socket e2e (round-trip with clock
injection, next, error paths, recurring roll-forward over RPC, 8-client
concurrency). 60 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 5 (tech-spec §4.4). Completing a recurring task rolls it forward in
place instead of marking it done — the Todoist-corner-avoiding model.
Pure recurrence module:
- next_occurrence(rrule, anchor, after): lazy RRULE expansion (rrule +
chrono/UTC) returning the next instance strictly after `after`,
skipping missed occurrences; None when a finite series is exhausted.
- reset_checkboxes(body): the fresh-checklist transform — unchecks every
`- [x]`, idempotent, preserves indentation/bullet/line-endings.
Storage roll-forward (one transaction, on set_state(done) of a recurring
task): reset the canonical context doc's checklist, append the completed
occurrence to the task's log, advance do_date to the next instance after
now (skipping misses); finite series finally goes done. `skip` advances
the same way without logging. Non-recurring done is unchanged.
Per-task append-only log (`log-of` doc): log_append / log_tail — the
resumption breadcrumb + recurring-completion narrative ([[design]] §6.4).
Tests: 7 recurrence unit + 2 proptests (no checked marker survives reset;
reset idempotent for any body) + 6 end-to-end incl. five-occurrence
no-carry-forward and missed-collapse-to-one. 53 tests green. This
completes the heph-core library layer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 2 (tech-spec §5). Pure, deterministic derivation from a body:
- `[[wiki-links]]` → wiki-link targets, in first-seen order, deduped,
honoring `[[target|display]]`. Scans the raw body (CommonMark mangles
`[[ ]]` brackets in inline parsing) and excludes matches inside code,
whose byte ranges come from pulldown-cmark's offset iterator.
- GFM `- [ ]` / `- [x]` task items → the local context-item index
(Fork A): label keeps raw markdown (for promotion) + checked state.
- Code blocks are correctly skipped for both.
10 extraction unit tests incl. idempotency; 14 total green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kick off Phase 1 (v1 prototype) per tech-spec §11.1. Sets up the Cargo
workspace and the first TDD slice of heph-core:
- Migration runner + §4.5 SQLite schema (nodes, tasks, links, aliases,
users, oplog, sync_state, conflicts), versioned via PRAGMA user_version.
- Clock-injected `Clock` trait (no ambient wall-clock reads; §2).
- `Store` trait + `LocalStore` SQLite backend with node create/get,
bootstrapping the single local user (oidc_sub NULL, §13).
- Node model (kinds: doc/task/project/tag/journal).
Repo housekeeping: fill AGENTS.md Project Structure (last template TODO),
ignore /target, add self-bootstrapping .forgejo/scripts/build that runs
cargo fmt/clippy/test in CI (§9), changelog fragment.
Tests green: 4 unit tests (migration version, local-user idempotency,
create/get round-trip, missing-node None).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>