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>
Record what's built and the resume order for the next context session:
heph-core + hephd local mode + CLI/export + local query surface + the
sync engine (HLC, op-log, converging merge/conflict-queue) are done;
resume at yrs body-CRDT → network sync → OIDC auth → heph.nvim.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First sync slice. Ratifies yrs for body merge in the tech-spec.
- hlc module: Hlc (physical, counter, origin) with a fixed-width encoding
whose lexical order equals causal order; HlcClock generator (tick/update)
— clock-injected, strictly monotonic. Unit + 2 proptests.
- meta table (migration v3) holds the stable per-device `origin` and the
last HLC. next_hlc() does a read-modify-write inside the caller's
transaction (store is single-writer, so no race), replacing the
timestamp placeholder. Every node write is now stamped with a real,
monotonic, causally-ordered HLC.
4 stamping integration tests (monotonic under stalled/regressed clock;
origin shared + persists across reopen; HLC resumes from persisted state).
89 tests green.
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 4 — the flagship Tactical blank-slate engine. Pure and
clock-injected, two stages:
- Candidacy filter: committed ∧ outstanding ∧ ¬tombstoned ∧ ≠blue ∧
actionable (do_date NULL or ≤ now) ∧ in scope. do_date is used ONLY
here — a boolean "can I do this now?" gate, never urgency.
- Order: an ordered list of named Dimensions applied lexicographically
(PastLateOn → LateOverdueAmount → Attention band → CreatedAt FIFO),
with node_id as final tiebreak for a total order. Reorder RANKING in
one place to retune. late_on is the sole urgency signal (global tier);
age never becomes urgency. blue hidden; red always shown past limit.
Storage `Store::next` loads candidates via a SQL join (project +
canonical-context links) and runs the pure engine with the store clock.
13 table-driven unit cases + 3 proptests (antisymmetry, sorted output
fully ordered, equality ⇒ identity) + 2 end-to-end. 38 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 3 (tech-spec §4.2–§4.3, §6). Refactor the SQLite layer into focused
submodules (nodes/tasks/links) behind a thin delegating Store impl so a
transaction can span several.
- Model: Attention (white/orange/red/blue), TaskState
(outstanding/done/dropped), LinkType, Link, Task, NewTask.
- create_task: in one transaction mints the task node + tasks row, the
canonical context doc, the canonical-context link, and an optional
in-project link. get_task / set_task_state / set_task_attention.
- Links CRUD: add_link, outgoing_links, backlinks (non-tombstoned).
- update_node: a body change re-runs extraction and reconciles this
node's wiki links — diff-based and idempotent, resolved via alias then
exact title (owner-scoped); unresolved targets link on a later edit
once the target exists; dropped targets are tombstoned.
20 tests green (12 unit + 8 integration).
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>
Resolve the open tensions surfaced in the pre-Phase-1 second pass over
design.md and tech-spec.md:
- Context items: Fork A index model — body markdown is the source of
truth; context items are a locally-derived, non-synced index;
identity is pinned at promotion. Dissolves the body-CRDT vs.
extraction convergence problem.
- Recurrence: roll-forward in place; drop task_occurrences and
is_template; advance to next RRULE instance after now (skip misses).
- Identity: deterministic ids for journal/tag (offline-convergent);
ULID for content nodes and project.
- Mode/sync: orthogonal hub_url spoke capability; everyday device is
local + hub_url, not server.
- Auth/owner: nullable oidc_sub, friction-free local user, hub-
authoritative identity, one-time pre-first-sync adoption rewrite.
- Ranking: do_date is a boolean candidacy filter only; late_on is the
sole urgency signal (global tier); FIFO tiebreak; order expressed as
a reorderable named-dimension list.
- Modes are plugin-side compositions; add list() and log.tail().
- Frame v1 as a single deliberate C1; misc cleanups (export, health,
CI nvim runner, README license).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace "distributed from day one" with a targetable Store backend that
supports the full spectrum from local-only to distributed by configuration:
- Store trait with LocalStore (direct SQLite, exclusive lock) and
RemoteStore (RPC to a server)
- three hephd runtime modes: local / server / client
- exclusive-lock handoff so the same SQLite file can pass between local and
server mode; client mode is thin and online-only
- offline remains a property of local-backed replicas syncing via the hub
Local-only is now a first-class configuration. Build order starts at local
mode. Updates docs/reference/tech-spec.md (§1, §3.1, §11) and
docs/explanation/design.md (§9, §11) to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Customize the generated repo (rename Dagger module to hephaestus_ci /
HephaestusCi, set docs baseUrl, add All-Rights-Reserved LICENSE, update
README/AGENTS), and add the project's foundational design documentation:
- docs/explanation/design.md — rationale + decision-history record
- docs/reference/tech-spec.md — implementation-ready technical spec
These define hephaestus as a self-hosted, client/server + offline-first
system unifying a markdown knowledge base with task management: typed node
graph, the lived priority discipline ("what is next?"), recurrence with
fresh-per-occurrence checklists, op-log/CRDT sync with conflict resolution,
OIDC/Authentik auth, the heph.nvim surface, and a TDD strategy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>