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>
Run rustfmt over files landed in earlier commits this session that weren't
fmt-checked (heph-quickadd, the heph-tui undo/move wave, the hephd quickadd
supervisor). Pure formatting (struct/if-else expansion, line wrapping); no
behavior change. Restores `cargo fmt --check` clean for CI.
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>
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>
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>