From 8f6e4c6b1bb39bc72bde82377c8a7237566d9f10 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 20:52:50 +0000 Subject: [PATCH 1/8] chore(env): add Cloud Agent environment config Adds .cursor/environment.json and an idempotent install script for Cursor Cloud Agents. The install script: - selects an nvm-managed Node whose bundled node:sqlite has FTS5 enabled (the daemon's default node has FTS5 disabled, which breaks @dripnex/mcp-server) and makes it the default node for all shells; - runs pnpm install --frozen-lockfile (postinstall rebuilds better-sqlite3 for Electron and materializes the Electron binary); - builds workspace packages so typecheck and the Playwright+Electron e2e suite are ready to run. Validated: pnpm test (19/19 tasks), typecheck, lint, build, and the Electron e2e suite (7 passed under xvfb). --- .cursor/environment.json | 4 ++ .cursor/install.sh | 114 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 .cursor/environment.json create mode 100755 .cursor/install.sh diff --git a/.cursor/environment.json b/.cursor/environment.json new file mode 100644 index 00000000..d68ad3fd --- /dev/null +++ b/.cursor/environment.json @@ -0,0 +1,4 @@ +{ + "name": "Dripnex", + "install": "bash .cursor/install.sh" +} diff --git a/.cursor/install.sh b/.cursor/install.sh new file mode 100755 index 00000000..77503df0 --- /dev/null +++ b/.cursor/install.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# +# Cloud Agent install script for Dripnex. +# +# Non-obvious constraints this script exists to satisfy: +# +# 1. node:sqlite FTS5 — @dripnex/mcp-server uses Node's built-in `node:sqlite` +# (DatabaseSync) and asserts FTS5 is compiled in. The node binary the +# Cursor exec daemon ships (/exec-daemon/node) has FTS5 DISABLED, so a +# plain `node`/`pnpm test` would fail. We select an nvm-managed Node whose +# bundled SQLite has FTS5 enabled and make it the default `node` for every +# shell by symlinking the toolchain into a PATH dir that precedes +# /exec-daemon. +# +# 2. better-sqlite3 (desktop) — apps/desktop's postinstall runs +# `electron-builder install-app-deps`, which rebuilds better-sqlite3 +# against Electron's ABI. That is required for `pnpm dev` and the +# Playwright+Electron e2e suite to launch. (This is why `pnpm test` +# excludes @dripnex/storage-sqlite — see CLAUDE.md.) +# +set -euo pipefail + +cd "$(dirname "$0")/.." +REPO_ROOT="$(pwd)" +echo "==> Dripnex install (repo: $REPO_ROOT)" + +# Dependabot-regenerated lockfiles can resolve GitHub git deps over SSH; CI +# rewrites them to HTTPS so the tarball install works without a deploy key. +git config --global 'url.https://github.com/.insteadOf' 'git@github.com:' || true + +# --- 1. Select an FTS5-capable Node and make it the default ---------------- +has_fts5() { + "$1" -e 'const{DatabaseSync}=require("node:sqlite");const d=new DatabaseSync(":memory:");process.exit(d.prepare("SELECT sqlite_compileoption_used(\x27ENABLE_FTS5\x27) v").get().v===1?0:1)' >/dev/null 2>&1 +} + +export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" +NODE_BIN="" +if [ -s "$NVM_DIR/nvm.sh" ]; then + # shellcheck disable=SC1091 + . "$NVM_DIR/nvm.sh" + # Prefer an already-installed Node >= 22 that has FTS5; otherwise install 22. + for ver in $(nvm ls --no-colors 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | sort -Vr | uniq); do + cand="$NVM_DIR/versions/node/$ver/bin/node" + if [ -x "$cand" ] && has_fts5 "$cand"; then NODE_BIN="$cand"; break; fi + done + if [ -z "$NODE_BIN" ]; then + echo "==> No FTS5-capable Node found; installing Node 22 via nvm" + nvm install 22 >/dev/null + cand="$(nvm which 22 2>/dev/null || true)" + if [ -n "$cand" ] && has_fts5 "$cand"; then NODE_BIN="$cand"; fi + fi +fi + +if [ -z "$NODE_BIN" ]; then + echo "ERROR: could not locate a Node build with node:sqlite FTS5 enabled." >&2 + exit 1 +fi +NODE_BIN_DIR="$(dirname "$NODE_BIN")" +echo "==> Using Node $("$NODE_BIN" -v) (FTS5 enabled) from $NODE_BIN_DIR" + +# Make the FTS5 node the default `node`/`npx` for every shell (login or not). +# The Cursor daemon's PATH places /exec-daemon (FTS5-less node) ahead of the +# nvm bin, so a plain `node` would be wrong. We inspect the *current* PATH +# (before we touch it): if the FTS5 node bin already precedes /exec-daemon, +# nothing is needed; otherwise we symlink the toolchain into the first writable +# PATH entry that precedes /exec-daemon (/usr/local/cargo/bin is world-writable +# in the base image and qualifies). Symlinks live on disk so they survive into +# environment builds/snapshots. +SHIM_DIR="" +NEED_SHIM=1 +IFS=':' read -r -a _path_entries <<< "$PATH" +for d in "${_path_entries[@]}"; do + case "$d" in */exec-daemon*) break ;; esac + if [ "$d" = "$NODE_BIN_DIR" ]; then NEED_SHIM=0; break; fi + if [ -z "$SHIM_DIR" ] && [ -d "$d" ] && [ -w "$d" ]; then SHIM_DIR="$d"; fi +done + +if [ "$NEED_SHIM" = "0" ]; then + echo "==> FTS5 node bin already precedes /exec-daemon on PATH; no shim needed" +else + [ -z "$SHIM_DIR" ] && SHIM_DIR="/usr/local/cargo/bin" + mkdir -p "$SHIM_DIR" 2>/dev/null || sudo mkdir -p "$SHIM_DIR" + echo "==> Linking node toolchain into $SHIM_DIR" + for b in node npm npx corepack pnpm pnpx yarn yarnpkg; do + src="$NODE_BIN_DIR/$b"; dest="$SHIM_DIR/$b" + [ -e "$src" ] && [ "$src" != "$dest" ] && ln -sfn "$src" "$dest" 2>/dev/null || true + done +fi + +# Ensure the rest of THIS script uses the FTS5 node too. +export PATH="$NODE_BIN_DIR:$PATH" +hash -r || true + +# --- 2. Install workspace dependencies (runs postinstall scripts) ---------- +# postinstall: lefthook install (git hooks) + electron-builder install-app-deps +# (rebuilds better-sqlite3 for Electron) + downloads the Electron binary. +echo "==> pnpm install --frozen-lockfile" +pnpm install --frozen-lockfile + +# Belt-and-suspenders: ensure the Electron binary is materialized for e2e. +if [ -f apps/desktop/node_modules/electron/install.js ]; then + echo "==> Materializing Electron binary" + ( cd apps/desktop && node node_modules/electron/install.js ) +fi + +# --- 3. Build workspace packages (source-derived; needed by typecheck/e2e) - +echo "==> pnpm build" +pnpm build + +# --- 4. Verify the critical invariant -------------------------------------- +echo "==> Verifying node:sqlite FTS5" +node -e 'const{DatabaseSync}=require("node:sqlite");const v=new DatabaseSync(":memory:").prepare("SELECT sqlite_compileoption_used(\x27ENABLE_FTS5\x27) v").get().v;if(v!==1){console.error("FTS5 missing");process.exit(1)}console.log("node",process.version,"FTS5 OK")' + +echo "==> Install complete." From b4e9eba7aae741ab65e89e431fb85fa5fad72711 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 21:00:37 +0000 Subject: [PATCH 2/8] fix(env): always create node toolchain shims regardless of install-time PATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install-time PATH can differ from the agent runtime PATH: during an environment build the nvm bin already precedes /exec-daemon, so the previous 'no shim needed' shortcut skipped creating the shims — leaving a fresh agent's default node pointing at the daemon's FTS5-less node. Always place the shims in every writable PATH dir preceding /exec-daemon (plus /usr/local/cargo/bin). --- .cursor/install.sh | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/.cursor/install.sh b/.cursor/install.sh index 77503df0..3352de5f 100755 --- a/.cursor/install.sh +++ b/.cursor/install.sh @@ -58,34 +58,38 @@ fi NODE_BIN_DIR="$(dirname "$NODE_BIN")" echo "==> Using Node $("$NODE_BIN" -v) (FTS5 enabled) from $NODE_BIN_DIR" -# Make the FTS5 node the default `node`/`npx` for every shell (login or not). -# The Cursor daemon's PATH places /exec-daemon (FTS5-less node) ahead of the -# nvm bin, so a plain `node` would be wrong. We inspect the *current* PATH -# (before we touch it): if the FTS5 node bin already precedes /exec-daemon, -# nothing is needed; otherwise we symlink the toolchain into the first writable -# PATH entry that precedes /exec-daemon (/usr/local/cargo/bin is world-writable -# in the base image and qualifies). Symlinks live on disk so they survive into -# environment builds/snapshots. -SHIM_DIR="" -NEED_SHIM=1 +# Make the FTS5 node the default `node`/`npx` for every shell. +# +# The Cursor daemon's runtime PATH places /exec-daemon (which ships an +# FTS5-less node) AHEAD of the nvm bin, so a plain `node` would be wrong at +# agent runtime. Crucially, the install-time PATH can differ from the agent +# runtime PATH (install may run with nvm already ahead), so we cannot skip +# based on the current ordering — we ALWAYS place shims. We symlink the +# toolchain into every writable PATH dir that precedes /exec-daemon, plus +# /usr/local/cargo/bin (world-writable in the base image and consistently +# ahead of /exec-daemon at runtime). Symlinks live on disk, so they survive +# into environment builds/snapshots and win over /exec-daemon's node. +declare -a SHIM_CANDIDATES=() IFS=':' read -r -a _path_entries <<< "$PATH" for d in "${_path_entries[@]}"; do case "$d" in */exec-daemon*) break ;; esac - if [ "$d" = "$NODE_BIN_DIR" ]; then NEED_SHIM=0; break; fi - if [ -z "$SHIM_DIR" ] && [ -d "$d" ] && [ -w "$d" ]; then SHIM_DIR="$d"; fi + SHIM_CANDIDATES+=("$d") done +SHIM_CANDIDATES+=("/usr/local/cargo/bin") -if [ "$NEED_SHIM" = "0" ]; then - echo "==> FTS5 node bin already precedes /exec-daemon on PATH; no shim needed" -else - [ -z "$SHIM_DIR" ] && SHIM_DIR="/usr/local/cargo/bin" - mkdir -p "$SHIM_DIR" 2>/dev/null || sudo mkdir -p "$SHIM_DIR" - echo "==> Linking node toolchain into $SHIM_DIR" +_shimmed="" +for d in "${SHIM_CANDIDATES[@]}"; do + [ "$d" = "$NODE_BIN_DIR" ] && continue # never clobber the node bin itself + case " $_shimmed " in *" $d "*) continue ;; esac # dedupe + mkdir -p "$d" 2>/dev/null || sudo mkdir -p "$d" 2>/dev/null || true + [ -d "$d" ] && [ -w "$d" ] || continue for b in node npm npx corepack pnpm pnpx yarn yarnpkg; do - src="$NODE_BIN_DIR/$b"; dest="$SHIM_DIR/$b" + src="$NODE_BIN_DIR/$b"; dest="$d/$b" [ -e "$src" ] && [ "$src" != "$dest" ] && ln -sfn "$src" "$dest" 2>/dev/null || true done -fi + echo "==> Linked node toolchain into $d" + _shimmed="$_shimmed $d" +done # Ensure the rest of THIS script uses the FTS5 node too. export PATH="$NODE_BIN_DIR:$PATH" From b378a93ec5519d7403cfe2f3f6ad38a8b24388cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Maritano?= Date: Sat, 22 Aug 2026 19:36:44 -0300 Subject: [PATCH 3/8] fix(api): give the migration tests a timeout that matches their work (#582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `tests/runMigrations.test.ts > records migrations that already exist in the schema` times out intermittently and blocks unrelated PRs. It blocked #581 until I reran the job, which then passed — a flake, not a break. ## Why it is slow Not a bug. Each test creates a real libsql file database in `/tmp` and drives the **entire** `MIGRATIONS` catalog through it. The failing one calls `applyMigrations` **twice**. CI measured: | Test | Duration | | --- | --- | | applies the catalog to an empty database, then no-ops | 3004 ms | | records migrations that already exist in the schema | 7376 ms | vitest's default is 5000 ms and there is no `testTimeout` configured anywhere in the repo. The suite has been living one runner hiccup away from red. ## Fix An explicit 30s budget on those two tests, with a comment saying why. Scoped deliberately: a global `testTimeout` bump would hide genuinely slow *unit* tests elsewhere. These two are the only ones doing real database I/O, so they are the only ones that should carry a longer budget. Verified locally: `pnpm --filter @dripnex/api test` → 59/59 pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) --- packages/api/tests/runMigrations.test.ts | 68 ++++++++++++++---------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/packages/api/tests/runMigrations.test.ts b/packages/api/tests/runMigrations.test.ts index 2fb7efa4..45c167a2 100644 --- a/packages/api/tests/runMigrations.test.ts +++ b/packages/api/tests/runMigrations.test.ts @@ -30,36 +30,50 @@ describe('applyMigrations', () => { } }); - it('applies the catalog to an empty database, then no-ops', async () => { - const path = `/tmp/dripnex-migrate-${randomUUID()}.db`; - paths.push(path); - const client = createClient({ url: `file:${path}` }); + // These two drive a real libsql file database through the entire migration + // catalog — the second one twice. That is legitimately slower than vitest's + // 5s default (3.0s and 7.4s on CI), so they carry their own budget rather + // than the suite carrying a global bump that would mask slow unit tests. + const MIGRATION_TIMEOUT_MS = 30_000; - const first = await applyMigrations(client); - expect(first.skipped).toEqual([]); - expect(first.applied.length + first.recordedExisting.length).toBe(MIGRATIONS.length); + it( + 'applies the catalog to an empty database, then no-ops', + async () => { + const path = `/tmp/dripnex-migrate-${randomUUID()}.db`; + paths.push(path); + const client = createClient({ url: `file:${path}` }); - const tables = await client.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name='plugin_versions'" - ); - expect(tables.rows).toHaveLength(1); + const first = await applyMigrations(client); + expect(first.skipped).toEqual([]); + expect(first.applied.length + first.recordedExisting.length).toBe(MIGRATIONS.length); - const second = await applyMigrations(client); - expect(second.applied).toEqual([]); - expect(second.skipped).toEqual(MIGRATIONS.map(m => m.id)); - }); + const tables = await client.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='plugin_versions'" + ); + expect(tables.rows).toHaveLength(1); - it('records migrations that already exist in the schema', async () => { - const path = `/tmp/dripnex-migrate-${randomUUID()}.db`; - paths.push(path); - const client = createClient({ url: `file:${path}` }); - await client.execute( - 'CREATE TABLE users (id text PRIMARY KEY, email text, created_at text, updated_at text)' - ); + const second = await applyMigrations(client); + expect(second.applied).toEqual([]); + expect(second.skipped).toEqual(MIGRATIONS.map(m => m.id)); + }, + MIGRATION_TIMEOUT_MS + ); - const report = await applyMigrations(client); - expect(report.recordedExisting).toContain('0000_chubby_zzzax'); - const second = await applyMigrations(client); - expect(second.skipped).toContain('0000_chubby_zzzax'); - }); + it( + 'records migrations that already exist in the schema', + async () => { + const path = `/tmp/dripnex-migrate-${randomUUID()}.db`; + paths.push(path); + const client = createClient({ url: `file:${path}` }); + await client.execute( + 'CREATE TABLE users (id text PRIMARY KEY, email text, created_at text, updated_at text)' + ); + + const report = await applyMigrations(client); + expect(report.recordedExisting).toContain('0000_chubby_zzzax'); + const second = await applyMigrations(client); + expect(second.skipped).toContain('0000_chubby_zzzax'); + }, + MIGRATION_TIMEOUT_MS + ); }); From f0835642a9fb0e5d5480cefbd6eb32d10527a92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Maritano?= Date: Sun, 23 Aug 2026 00:07:50 -0300 Subject: [PATCH 4/8] fix(ci): restore contents scope and narrow disable-automerge (#583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What removing `|| true` revealed The `disable-automerge` job fails with: ``` FORBIDDEN — Resource not accessible by integration ``` It had been failing silently behind `|| true` (see run [32602609796](https://github.com/dripnex/app/actions/runs/32602609796) on #571). The safety net that is supposed to stop a queued auto-merge from landing on an unprotected base **has never actually worked** — it just reported green. ## Honest note on cause I cannot prove this was pre-existing. The job lost `contents: write` earlier in #566 when I scoped workflow-level permissions per job on a least-privilege review, and `|| true` meant no run before or after ever surfaced a failure. Two live hypotheses: 1. The mutation needs `contents: write` and the least-privilege narrowing broke it. 2. It was already FORBIDDEN and the suppression hid it. This PR tests hypothesis 1 by granting the scope back. If FORBIDDEN returns with both scopes present, the cause is a repository or org restriction on `GITHUB_TOKEN` and needs a settings change or a PAT — the comment in the file says so, so the next person does not have to rediscover it. ## Scope narrowing The job fired on every PR whose base was not `develop`, which includes release promotions into `main`. `main` is protected; there is no unsafe queued merge to undo there. The risk this job exists for is landing on an **unprotected** base, so it now fires only on drafts, or on a base that is neither `develop` nor `main`. ## Not changed The loud failure stays. Going back to `|| true` would restore exactly the false confidence that hid this. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit * **Chores** * Updated automated merge handling for draft pull requests. * Protected pull requests targeting `main` and `develop` from automatic processing. * Enabled the required repository content permissions for the automation workflow. Co-authored-by: Claude Opus 5 (1M context) --- .github/workflows/automerge.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index e5b20356..135d5a6a 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -59,11 +59,21 @@ jobs: disable-automerge: runs-on: ubuntu-latest permissions: - # Only the auto-merge state changes here; no contents write needed. + # `pull-requests: write` alone returned FORBIDDEN ("Resource not + # accessible by integration") on PR #571. The narrower scope came from + # a least-privilege review; the mutation appears to want contents too. + # If FORBIDDEN comes back with both scopes granted, the cause is a + # repository or org restriction on GITHUB_TOKEN, not this file. + contents: write pull-requests: write + # Only where a queued merge could still land somewhere unprotected: a + # draft, or a base that is neither develop nor main. A PR retargeted onto + # main is a release promotion, and main is protected — there is nothing + # dangerous to undo, so firing there was pure noise. if: >- github.event.pull_request.draft == true || - github.event.pull_request.base.ref != 'develop' + (github.event.pull_request.base.ref != 'develop' && + github.event.pull_request.base.ref != 'main') steps: - name: Disable auto-merge for drafts and non-develop bases env: From 0441120f44de4c64685cefacc354abec9e7875d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Maritano?= Date: Sun, 23 Aug 2026 00:14:24 -0300 Subject: [PATCH 5/8] fix(ci): make sync-develop open a back-merge that can actually merge (#585) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context I enabled **Allow GitHub Actions to create and approve pull requests** at the org and repo level, which unblocked `sync-develop` — it had been failing with `GitHub Actions is not permitted to create or approve pull requests` since forever. Re-running it against the v0.17.0 build now succeeds. But it immediately produced #584, which **cannot merge**. ## The problem `sync-develop` opened a `develop ← main` PR. That shape deadlocks: `develop` requires the head branch to be up to date, and `main` falls behind `develop` the moment anything lands after the release — #582 and #583 did exactly that. #584 has been sitting at `BEHIND` since it was created. I hit the same wall by hand earlier tonight with #577. Even if it could merge, the squash auto-merge would replay the content as a fresh commit and **not** establish ancestry, which is the entire point of a back-merge. That is what went wrong in #578 and #579. ## The fix Push a branch descended from `develop` with `main` merged into it, then open that against `develop`. Same thing that finally worked manually in #581. - Named `chore/backmerge-main-`, which matches the exclusion added in #580 so `automerge.yml` leaves it alone. - The job arms auto-merge itself with `--merge`, so it lands as a merge commit. - Exits early when `main` is already an ancestor, or when the branch already exists, so re-runs are safe. ## Follow-up #584 should be closed — this replaces it. The next release will exercise this path for real. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 56 ++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 36bf914b..20ee3bc6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -218,23 +218,53 @@ jobs: if: needs.publish.result == 'success' runs-on: ubuntu-latest steps: - - name: Create sync PR + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + # A `develop <- main` PR cannot merge: develop requires the head branch + # to be up to date, and main is behind develop the moment anything lands + # after the release. Push a branch descended from develop with main + # merged into it instead — that satisfies the rule and, unlike a squash, + # actually makes main an ancestor of develop so the next promotion is + # not stuck at BEHIND. + - name: Open the back-merge PR env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG_NAME: ${{ needs.meta.outputs.tag }} run: | set -euo pipefail - if ! pr_count="$(gh pr list --base develop --head main --repo "$GITHUB_REPOSITORY" --json number --jq 'length')"; then - echo "::error::Unable to check for an existing sync PR." - exit 1 + git fetch origin main develop + + if git merge-base --is-ancestor origin/main origin/develop; then + echo "main is already an ancestor of develop; nothing to sync." + exit 0 fi - if [ "$pr_count" -eq 0 ]; then - gh pr create \ - --base develop \ - --head main \ - --title "chore: sync release $TAG_NAME back to develop" \ - --body "Auto sync of release commit and changelog from $TAG_NAME." \ - --repo "$GITHUB_REPOSITORY" - else - echo "Sync PR already open; nothing to create." + + # The chore/backmerge- prefix keeps this out of the squash + # auto-merge in automerge.yml. Squashing it would defeat the point. + branch="chore/backmerge-main-${TAG_NAME}" + + if git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then + echo "$branch already exists; leaving the open PR alone." + exit 0 fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "$branch" origin/develop + git merge origin/main -m "chore(release): merge main into develop" + git push origin "$branch" + + gh pr create \ + --base develop \ + --head "$branch" \ + --title "chore(release): merge main into develop" \ + --body "Back-merge of $TAG_NAME so main stays an ancestor of develop and the next promotion PR is not stuck at BEHIND. + + Merge this with a **merge commit**. A squash replays the content as a new commit and does not establish ancestry." \ + --repo "$GITHUB_REPOSITORY" + + # Explicitly a merge commit. automerge.yml skips chore/backmerge-*, + # so nothing else will arm this with --squash. + gh pr merge "$branch" --auto --merge --repo "$GITHUB_REPOSITORY" From 3d9b05165a1e75037668021c01f3f432f5c5ad97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Maritano?= Date: Sun, 23 Aug 2026 00:34:27 -0300 Subject: [PATCH 6/8] docs: track the dependency map and scope reconciliation (#587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both files were sitting **untracked** in the working tree. They failed `pnpm format:check` locally, so a clean run could not go green without deleting them — which would have thrown away real analysis. Prettier applied, **content unchanged**. - `dripnex-dependency-map.md` — the epic dependency DAG, the critical path, spike gates, and a two-person parallelization plan. The cross-cutting view the issue tree cannot show. - `dripnex-scope-reconciliation.md` — resolves where the vision doc, the issues, and the running code disagree (Tauri vs Electron, Rust vs TS, CRDT vs push/pull, the 5-Worker split), with the open founder calls spelled out. Both declare their own trust hierarchy and mark `plan.md` and `docs/ROADMAP.md` as stale, which is worth having in the repo rather than in one person's working tree. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) --- docs/dripnex-dependency-map.md | 153 +++++++++++++++++++++++++++ docs/dripnex-scope-reconciliation.md | 137 ++++++++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 docs/dripnex-dependency-map.md create mode 100644 docs/dripnex-scope-reconciliation.md diff --git a/docs/dripnex-dependency-map.md b/docs/dripnex-dependency-map.md new file mode 100644 index 00000000..2d0fbf1d --- /dev/null +++ b/docs/dripnex-dependency-map.md @@ -0,0 +1,153 @@ +# Dripnex — Dependency Map & Critical Path + +> **Status:** Living planning doc derived from the GitHub issue tree (E1–E18, 116 issues). +> The issues remain the source of truth; this is the cross-cutting view they can't show: +> the dependency DAG, the critical path, spike gates, and a 2-person parallelization plan. + +## TL;DR + +- **Three keystones:** **E1** (foundation — blocks everything), **E9** (knowledge substrate — + blocks all of Q3), **E10** (the MCP write-loop — _the_ value of the pivot). +- **The differentiator ships in Q3.** Q1–Q2 are prelude (stabilize + platform + rename). + There is no demonstrable "dripnex moment" until E9→E10→E12 land mid-year. +- **Longest dependency chain = the backend split** (E5→E8→E14→E16). It's also the highest + scope/capacity risk — see D3 in the reconciliation doc. +- **Two spike gates** decide Q3 subsystems: graph WebGL (#318→E11) and sqlite-vec (#319→E13). + +--- + +## 1. Epic dependency DAG + +```mermaid +graph TD + subgraph Q1["Q1 — Stabilize + Platform foundations"] + E1["E1 v0.15.4 patch
(stabilize + e2e signal)"] + E2["E2 Spike week
graph · sqlite-vec · Tauri"] + E3["E3 Plugin API v1"] + E4["E4 First satellites"] + E5["E5 Backend contracts"] + end + subgraph Q2["Q2 — Extraction + Rename + Split 1"] + E6["E6 Built-in extraction"] + E7["E7 Rename train
(highest risk)"] + E8["E8 Service split 1
auth + sync"] + E9["E9 Knowledge layer
frontmatter + link index"] + end + subgraph Q3["Q3 — AI notetaker core"] + E10["E10 MCP write surface"] + E11["E11 Graph view"] + E12["E12 Palette + Connect w/ Claude"] + E13["E13 Semantic layer"] + E14["E14 Service split 2"] + end + subgraph Q4["Q4 — Ecosystem + polish"] + E15["E15 UX hygiene"] + E16["E16 Plugin ecosystem"] + E17["E17 QA program"] + E18["E18 Positioning & launch"] + end + + E1 --> E3 & E5 & E9 & E7 + E3 --> E4 --> E6 + E3 --> E6 + E5 --> E8 --> E14 + E2 -. graph spike .-> E11 + E2 -. sqlite-vec spike .-> E13 + E9 --> E10 & E11 & E13 + E10 --> E12 + E7 -. deep-link .-> E12 + E7 --> E18 + E11 --> E18 + E12 --> E18 + E3 --> E16 + E6 --> E16 + E14 -- plugin-registry --> E16 + E8 & E14 --> E17 + E16 --> E18 +``` + +> **Note:** E1 is drawn feeding the four chain-heads it actually unblocks; in practice its +> e2e-signal task (#309) gates _everything_ — nothing should merge on a red e2e suite. + +--- + +## 2. The dependency chains, ranked + +| Chain | Depth | What it delivers | Risk | +| --------------------------------- | ----- | -------------------------------------------------- | -------------------------------------------------------- | +| **Backend:** E1→E5→E8→E14→E16→E18 | 6 | Microservices + marketplace + launch | ⚠️ Longest; biggest capacity cost (D3) | +| **Value loop:** E1→E9→E10→E12→E18 | 5 | The agent write-loop — the pivot's reason to exist | 🎯 Demo-critical | +| **Platform:** E1→E3→E4→E6→E16→E18 | 6 | Plugin API → satellites → marketplace | Steady, well-sequenced | +| **Graph:** E2⇢E11, E9→E11→E18 | — | The differentiator UI | Gated on spike (#318) | +| **Semantic:** E2⇢E13, E9→E13 | — | Embeddings / related notes / ContextBuilder v2 | Gated on spike (#319); degrades to link-only if it slips | +| **Rename:** E1→E7→E18 | — | Identity change | 🔴 Highest single-epic risk; ships alone | + +**Read this as:** the product's _value_ (value loop) is only 5 deep and mostly TS app work. +The _operational cost_ (backend chain) is 6 deep and the part most worth trimming for a +2-person team. + +--- + +## 3. Spike gates (decide before building) + +| Spike | Gates | Pass bar | If it fails | +| ---------------------------------- | ---------------- | ----------------------------------------------------------- | --------------------------------------------------------------- | +| #318 graph WebGL | E11 | 5k nodes / 20k edges ≥ 30 fps in Electron | E11 ships with degradation/ego-graph only, or library swap | +| #319 sqlite-vec | E13 | semantic beats FTS5 on a golden-query eval over the real DB | E13 ships link-only; ContextBuilder v2 works without embeddings | +| #320 Tauri | (stack decision) | paper PoC only — door stays closed regardless | Record decision (#321), kill the option | +| #380 Connect w/ Claude (Agent SDK) | E12 | official-path auth ergonomics acceptable | E12 falls back to demoted API-key path | + +**Run E2's spikes in Q1, in parallel with E1.** Their verdicts shape two whole Q3 epics, so +late spikes = late re-planning. + +--- + +## 4. Suggested re-sequencing + +1. **Pull frontmatter schema (#363) into late Q1.** E9 is the keystone for all of Q3 but + sits in Q2. The _schema_ (not the full link index) is small and pure-core; landing it + early de-risks E10/E11/E13 and lets the MCP tool contract stabilize sooner. Fold in D1 + (the `type` enum) while doing it. +2. **Gate E14 by trigger, not by calendar** (reconciliation D3). Keep `billing-webhooks`; + defer `ai-gateway` (desktop BYO doesn't use it) and `plugin-registry` (only when E16 is next). +3. **Protect the rename week (E7).** Its body already says "nothing else ships in its week" — + hold the line; the upgrade-path e2e (#354) is the only gate. +4. **Treat E17 (QA) as continuous, not Q4.** Its own body says charter items burn down + ~3/week across all quarters. Q4 is just where completion is _tracked_, not where QA starts. + +--- + +## 5. Parallelization for 2 people + +The `wf:` labels are already a clean work-breakdown into 9 tracks. Mapped to two owners +(adjust to reality — Alicio is referenced as the graph owner in E11): + +| Owner | Primary tracks | Rationale | +| ------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| **Founder** | `wf:stabilization`, `wf:knowledge-layer`, `wf:mcp-agents`, `wf:backend-services`, `wf:rename-brand` | The keystones + risky/identity work + backend; the spine of the value loop | +| **Collaborator (Alicio)** | `wf:graph`, `wf:palette-ux`, plus `wf:plugin-platform` extractions | Graph is explicitly theirs; palette + satellite extractions are isolatable, plugin-API-mediated units | +| **Shared / async** | `wf:qa-infra` | Continuous; both contribute, neither owns full-time | + +**Parallelism rules that fall out of the DAG:** + +- E2 spikes ∥ E1 (independent — start day 1). +- Once E3's RFC (#322) lands, E4 satellites ∥ E5 backend contracts (different tracks, no shared files). +- E9 (founder) ∥ E6 extraction (collaborator) in Q2 — but **E7 rename is a stop-the-world week** for both. +- In Q3, the value loop (E9→E10→E12, founder) runs ∥ graph (E11, collaborator) ∥ semantic + (E13, gated). E14 backend is the deferrable one. + +**Capacity reality check:** ~30 / 28 / 24 / 16 child tasks per quarter ≈ 2–2.5 tasks/week +sustained, with several large items (table WYSIWYG #342, WebGL graph #375, 5-Worker split). +This is aggressive but the sequencing is sound. The deferrals in §4.2 are the main lever if +velocity lags — cut backend surface before cutting the value loop. + +--- + +## 6. What "done" looks like per quarter + +| Q | Demonstrable outcome | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Q1 | Green e2e gate, signed v0.15.4 shipped, plugin-API RFC + npm publish, spike verdicts recorded | +| Q2 | App is "dripnex" (rename done, zero data loss), built-ins are real satellites, frontmatter+backlinks live | +| Q3 | **The pivot is real:** Claude Code/Desktop writes validated notes via MCP, graph renders, palette + Connect-with-Claude work, semantic search beats FTS | +| Q4 | Marketplace browses real registry, QA is a credible gate, dripnex.app launched around the MCP write-loop demo | diff --git a/docs/dripnex-scope-reconciliation.md b/docs/dripnex-scope-reconciliation.md new file mode 100644 index 00000000..84332566 --- /dev/null +++ b/docs/dripnex-scope-reconciliation.md @@ -0,0 +1,137 @@ +# Dripnex — Scope Reconciliation (Vision ↔ Issues ↔ Code) + +> **Status:** Living decision doc. Last reconciled against the open GitHub issue tree +> (E1–E18, 116 issues) and the working code at `v0.15.2`. +> +> **Source-of-truth hierarchy:** running code > GitHub issues (the real roadmap) > +> this doc > the "Agentic Software Memory System" vision doc > `plan.md` / `docs/ROADMAP.md` +> (both **stale, do not trust**). + +## Why this doc exists + +Four documents describe Dripnex and they disagree: + +| Doc | What it is | Trust | +| ------------------------------------------------- | ----------------------------------------------- | --------------------------------- | +| **GitHub issues E1–E18** | The real, sequenced, dependency-aware roadmap | ✅ Source of truth | +| **Vision doc** ("Agentic Software Memory System") | North-star narrative / concept | ⚠️ Concept only, not a stack spec | +| `plan.md` (root) | v1.0 "markdown-first, AI deferred" architecture | ❌ Stale — superseded by pivot | +| `docs/ROADMAP.md` | v0.5.0 "close the Inkdrop gap" plan | ❌ Stale — pre-pivot | + +This doc resolves the divergences so nobody (human or agent) builds against the wrong map. + +--- + +## 1. Divergence table + +Legend: **DECIDED** = issues already chose, build accordingly · **ALIGNED** = vision and +issues agree · **OPEN** = needs founder ratification (see §2). + +| # | Topic | Vision doc says | Issues + code reality | Status | +| --- | ----------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | +| 1 | Desktop runtime | **Tauri v2** | Electron, locked. E2 (#320) is a _half-day paper spike_ only — "Decision already locked: Electron base, no rewrite" | **DECIDED → Electron** | +| 2 | Core engine | **Rust** "the heart of the product" | TypeScript: `packages/core` + `ai-core` `ContextBuilder` + `storage` link index. No Rust anywhere (no `Cargo.toml`) | **DECIDED → TS** | +| 3 | MCP server | **Rust MCP server** | TypeScript `@dripnex/mcp-server`, extracted as public satellite (E4 #333) | **DECIDED → TS** | +| 4 | Primary unit | **Artifact-first** (not notes) | Notes + **typed frontmatter** carry the artifact logic (E9 #363). "Artifact" = note with a `type` + structured frontmatter | **OPEN** (see D1) | +| 5 | Artifact taxonomy | Plan / Decision / RFC / Investigation / Migration / Audit / Incident / Task | E9 frontmatter v1 ships generic `project / status / relations / dates / custom`; **no `type` enum yet** | **OPEN — gap** (see D1) | +| 6 | Semantic storage | SQLite + FTS5 + sqlite-vec | Exactly that — E13 (#384) embeddings table, gated on E2 spike (#319) | **ALIGNED** | +| 7 | Graph | Relations in SQLite, **no Neo4j** | E9 link index (#364) + E11 WebGL graph (#375). Same call | **ALIGNED** | +| 8 | Sync | **CRDTs (Yjs / Automerge)** | Existing AES-256-GCM E2E sync; E8 `sync-service` is **push/pull/conflict**, not CRDT | **OPEN** (see D2) | +| 9 | Model-agnostic | Claude / GPT / Gemini / Ollama / vLLM | `ai-core` has anthropic / openai / ollama via `fetch`; E14 `ai-gateway` adds quotas/keys. Gemini + vLLM not built | **ALIGNED** (subset shipped) | +| 10 | Local-first | Default, offline | Same — offline-first is a non-negotiable | **ALIGNED** | +| 11 | Backend | (silent) | E5/E8/E14: decompose Hono+Turso monolith into **5 Cloudflare Workers** | **OPEN — capacity** (see D3) | +| 12 | "Context Engine" | Rust, builds optimal agent context | `ContextBuilder v2` (E13 #387) — TS, graph traversal + semantic fill under token budget | **ALIGNED** (TS, not Rust) | + +**Bottom line:** the vision doc's _stack_ (Rust + Tauri + Rust-MCP) was rejected. Its +_ideas_ (artifact-centric memory, local-first, model-agnostic, context-before-generation, +SQLite graph, semantic retrieval) are all alive and map cleanly onto the TS/Electron +implementation. Treat the vision doc as **product narrative**, never as a build spec. + +--- + +## 2. Open decisions (need a founder call) + +### D1 — Encode the artifact-type taxonomy in frontmatter v1? **(recommend: YES, lightweight)** + +The vision's whole thesis is "artifact-first." Today E9 (#363) ships a generic frontmatter +schema with no `type`. If artifact-first is real, add a `type` enum now — it's cheap at +schema-design time and expensive to retrofit once notes exist in the wild. + +Recommendation: add an **open** `type` field to frontmatter v1: + +```yaml +--- +type: decision # plan | decision | rfc | investigation | migration | audit | incident | task | note +status: accepted +project: dripnex-core +relations: + supersedes: [adr-001] +--- +``` + +Keep it open-vocabulary (string, with a known set) so the MCP tools (E10) and ContextBuilder +(E13) can filter by artifact type without a migration later. This makes #363 the single +schema doc that becomes the MCP tool contract in Q3 — exactly as E9's body promises. + +### D2 — CRDT sync (Yjs/Automerge) vs keep push/pull? **(recommend: DEFER CRDT)** + +CRDTs only pay off for real-time multi-writer collaboration. Dripnex is single-user +local-first today, and `plan.md`'s own non-goals listed collaboration as "Never (v1)." +The shipped AES E2E push/pull/conflict model (E8 #359) is sufficient and far simpler. + +Recommendation: **defer CRDT** to a future "teams" epic; do not let the vision doc pull +Yjs/Automerge into the Q2 sync-service split. Revisit only if real-time collab becomes a +funded goal. + +### D3 — Full 5-Worker split vs slim monolith? **(recommend: PHASE IT — auth+sync only until traction)** + +This is the single largest scope/capacity risk. E5+E8+E14 decompose the backend into +auth / sync / ai-gateway / plugin-registry / billing-webhooks. The issues themselves admit +"contract-versioning overhead on a 2-person team." Five independently-deployed Workers, +five staging envs, five smoke suites, shared `@dripnex/contracts` versioning — that is a lot +of operational surface for two people pre-revenue. + +Recommendation: + +- **Do** E8 (auth + sync extraction) — gives a collaborator one ownable unit and isolates the + two cleanest boundaries. +- **Gate** E14 (ai-gateway / plugin-registry / billing split) on actual need: + - `plugin-registry` only when the marketplace (E16) is genuinely next. + - `billing-webhooks` isolation is cheap and worth it (smallest blast radius for Stripe). + - `ai-gateway` only matters for a **web/team** future — the desktop BYO-key path explicitly + does _not_ route through it (per E14's own body). So it can wait. +- Keep a **slim monolith** for everything not yet extracted; let traffic evidence decide the + monolith-retirement audit (#390), not architectural purity. + +### D4 — Tauri: close the door? **(recommend: keep #320 as a paper spike, then formally kill it)** + +E2 already treats Tauri as a non-starter ("Electron base, no rewrite"). Run #320 as the +half-day paper PoC, write the decision record (#321), and **close the option explicitly** so +it stops resurfacing. The vision doc should be updated to say "Electron" or annotated as +superseded. + +--- + +## 3. Concept → implementation map (for the vision doc readers) + +| Vision concept | Where it actually lives | +| --------------- | ---------------------------------------------------------------------------- | +| Artifact | Note + typed frontmatter (`packages/core`, E9) | +| Core Engine | `packages/core` + `ai-core/ContextBuilder` + `storage` link/embeddings index | +| Context Engine | `ContextBuilder v2` (#387) — graph traversal + semantic fill, token-budgeted | +| MCP Layer | `@dripnex/mcp-server` (TS), E10 write/query/graph tools | +| Embedding Layer | sqlite-vec + incremental indexing (E13 #384), offline-first source per spike | +| Graph Layer | `links` table in SQLite (E9 #364) + `@dripnex/plugin-graph` WebGL (E11) | +| Sync Layer | `sync-service` Worker push/pull/conflict (E8) — **not** CRDT (see D2) | +| Model-agnostic | `ai-core` providers + `ai-gateway` (E14) | + +--- + +## 4. Action items out of this reconciliation + +- [ ] Ratify D1–D4 (founder). +- [ ] If D1 = yes: amend #363 to include the `type` enum before building the schema. +- [ ] Annotate the vision doc header: "north-star concept; stack is Electron/TS, see scope-reconciliation." +- [ ] Mark `plan.md` and `docs/ROADMAP.md` as superseded (move to `docs/archived/`). +- [ ] If D3 = phase: re-label E14 children — keep `billing-webhooks`, gate `ai-gateway` and + `plugin-registry` behind explicit triggers. From f9f0b48ecbcb7571970e719c8093248d54de61f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Maritano?= Date: Mon, 24 Aug 2026 10:13:40 -0300 Subject: [PATCH 7/8] feat(theme): Harbor Dusk (#590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary First daily official theme pack: **Harbor Dusk** (`dripnex-harbor-dusk`). Coastal evening palette for long writing sessions — calm, low glare, readable markdown. Same contract as Parchment / Wave / Night / Fog: a token layer over `tokens.css`, registered in `OFFICIAL_THEMES`. No new fonts, no stylesheet fork, no plugin pack. Evening sibling to Fog (coastal gray morning). Merge needs a **Dripnex / Tomás** gate — draft only, do not merge from this PR. ## Concept Harbor after sunset: deep slate/navy water, mist over the docks, muted teal from the tide, warm amber from lanterns. Built for long markdown sessions, not OLED punch. ## Palette preview | Role | Token | Hex | | --- | --- | --- | | Canvas | `--bg-base` | `#141c26` | | Sidebar | `--bg-surface` | `#10161e` | | Raised chrome | `--bg-elevated` | `#1c2633` | | Inset / code | `--bg-inset` | `#0c1218` | | Mist text | `--text-primary` | `#cdd6de` | | Teal accent | `--accent` | `#5e9a92` | | Accent hover | `--accent-hover` | `#74aea6` | | Amber lanterns (links, on-hold) | `--cm-link` / `--status-on-hold` | `#d4a05a` | | Completed | `--status-completed` | `#7aad8a` | | Dropped | `--status-dropped` | `#c46b6b` | Hover, borders, and glass use mist at low opacity so chrome stays quiet. Typography is the existing `--font-sans` / `--font-mono` stack; this pack does not set font faces. `--cm-link` is the one extra vs other official palettes (allowed `--cm-*` extension, same as the community `theme.json` template) so editor links read as lanterns while chrome stays teal. Preview markdown links still follow `--accent` — `--link` is not in the theme whitelist. ## Files touched - `apps/desktop/src/renderer/themes/officialThemes.ts` — register Harbor Dusk - `apps/desktop/src/renderer/themes/__tests__/officialThemes.test.ts` — unique ids + token whitelist, including Harbor Dusk - `docs/themes/LOG.md` — daily id/topic log (required so later packs do not reuse this id) No lockfile, no site listing, no marketplace registry (`theme-parchment` satellite is a different shipping path). ## How to preview 1. `pnpm dev` (desktop) 2. Settings → Themes 3. Select **Harbor Dusk** 4. Check sidebar, note list, editor, command palette, and a markdown note with headings, links, lists, and a code block Restore Default (or another palette) to leave the session as you found it. ## Type of Change - [x] New feature ## Checklist - [x] PR targets `develop` (not `main`) - [x] Draft — merge needs Dripnex / Tomás gate - [x] Official theme unit tests pass (`@dripnex/desktop` 325 tests) - [x] Workspace typecheck passed on push - [ ] Full `pnpm test` / `pnpm build` (CI)
Open in Web Open in Cursor 
## Summary by CodeRabbit - **New Features** - Added the official **Harbor Dusk** dark theme, featuring coastal evening colors, muted teal accents, amber status tones, frosted surfaces, and improved code-link styling. - **Documentation** - Added Harbor Dusk to the official theme log. - **Tests** - Added validation covering theme metadata, color schemes, token values, unique identifiers, and accent consistency. --------- Co-authored-by: Cursor Agent --- .../themes/__tests__/officialThemes.test.ts | 32 +++++++++++++++++ .../src/renderer/themes/officialThemes.ts | 36 +++++++++++++++++++ docs/themes/LOG.md | 1 + 3 files changed, 69 insertions(+) create mode 100644 apps/desktop/src/renderer/themes/__tests__/officialThemes.test.ts diff --git a/apps/desktop/src/renderer/themes/__tests__/officialThemes.test.ts b/apps/desktop/src/renderer/themes/__tests__/officialThemes.test.ts new file mode 100644 index 00000000..e4df44a1 --- /dev/null +++ b/apps/desktop/src/renderer/themes/__tests__/officialThemes.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { validateThemeTokens } from '@dripnex/plugin-api'; +import { OFFICIAL_THEMES } from '../officialThemes'; + +describe('OFFICIAL_THEMES', () => { + it('registers unique ids with only valid tokens', () => { + const ids = OFFICIAL_THEMES.map(theme => theme.id); + expect(new Set(ids).size).toBe(ids.length); + + for (const theme of OFFICIAL_THEMES) { + expect(theme.name.length).toBeGreaterThan(0); + expect(['dark', 'light']).toContain(theme.colorScheme); + const valid = validateThemeTokens(theme.tokens, theme.id); + for (const [token, value] of Object.entries(theme.tokens)) { + expect(valid[token]).toBe(value); + } + expect(valid['--accent-primary']).toBe(theme.tokens['--accent']); + } + }); + + it('includes Harbor Dusk as a dark official palette', () => { + const harbor = OFFICIAL_THEMES.find(theme => theme.id === 'dripnex-harbor-dusk'); + expect(harbor).toMatchObject({ + name: 'Harbor Dusk', + colorScheme: 'dark', + pluginId: 'dripnex', + }); + expect(harbor?.tokens['--bg-base']).toBe('#141c26'); + expect(harbor?.tokens['--accent']).toBe('#5e9a92'); + expect(harbor?.tokens['--cm-link']).toBe('#d4a05a'); + }); +}); diff --git a/apps/desktop/src/renderer/themes/officialThemes.ts b/apps/desktop/src/renderer/themes/officialThemes.ts index eaed6475..5a36cbc7 100644 --- a/apps/desktop/src/renderer/themes/officialThemes.ts +++ b/apps/desktop/src/renderer/themes/officialThemes.ts @@ -464,6 +464,42 @@ export const OFFICIAL_THEMES: ThemeDefinition[] = [ '--status-dropped': '#c44b4b', }, }, + { + id: 'dripnex-harbor-dusk', + name: 'Harbor Dusk', + description: 'Coastal evening. Mist text, muted teal, amber lanterns.', + author: 'Dripnex', + colorScheme: 'dark', + pluginId: 'dripnex', + tokens: { + '--bg-base': '#141c26', + '--bg-surface': '#10161e', + '--bg-elevated': '#1c2633', + '--bg-inset': '#0c1218', + '--bg-hover': 'rgba(205, 214, 222, 0.06)', + '--bg-active': 'rgba(205, 214, 222, 0.1)', + '--text-primary': '#cdd6de', + '--text-secondary': 'rgba(205, 214, 222, 0.74)', + '--text-muted': 'rgba(205, 214, 222, 0.5)', + '--text-faint': 'rgba(205, 214, 222, 0.32)', + '--border': 'rgba(205, 214, 222, 0.1)', + '--border-subtle': 'rgba(205, 214, 222, 0.06)', + '--border-strong': 'rgba(205, 214, 222, 0.16)', + '--accent': '#5e9a92', + '--accent-hover': '#74aea6', + '--accent-muted': 'rgba(94, 154, 146, 0.2)', + '--accent-subtle': 'rgba(94, 154, 146, 0.1)', + '--glass-bg': 'rgba(20, 28, 38, 0.9)', + '--glass-border': 'rgba(205, 214, 222, 0.08)', + '--glass-bg-menu': 'rgba(28, 38, 51, 0.95)', + '--glass-border-menu': 'rgba(205, 214, 222, 0.08)', + '--status-active': '#5e9a92', + '--status-on-hold': '#d4a05a', + '--status-completed': '#7aad8a', + '--status-dropped': '#c46b6b', + '--cm-link': '#d4a05a', + }, + }, ]; export function registerOfficialThemes(): void { diff --git a/docs/themes/LOG.md b/docs/themes/LOG.md index fc9eb8ba..923def31 100644 --- a/docs/themes/LOG.md +++ b/docs/themes/LOG.md @@ -8,3 +8,4 @@ Format: `YYYY-MM-DD | id | name | topic | colorScheme` 2026-08-22 | dripnex-matcha | Matcha | green-tea paper, calm reading | light 2026-08-22 | dripnex-phosphor | Phosphor | amber CRT / terminal glow | dark 2026-08-22 | dripnex-fog | Fog | coastal gray morning, muted blue accent | light +2026-08-24 | dripnex-harbor-dusk | Harbor Dusk | coastal evening, mist text, muted teal, amber lanterns | dark From 0d31267b3b5fda002d38f9c555af81ca6874d7da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Maritano?= Date: Mon, 24 Aug 2026 14:17:50 -0300 Subject: [PATCH 8/8] docs(release): write the 0.18.0 What's New (#592) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Author `docs/releases/v0.18.0.md` so 0.18.0 can promote. Harbor Dusk is the user-facing story: a new official palette already in the desktop. Status stays **draft**. Flip to `published` when the GitHub release is undrafted. Promotion remains a separate `chore(release): promote 0.18.0` PR (`develop` → `main`, merge commit). Does not claim AI, self-building, or encryption (AES-KW shipped in 0.17.0). ## Type of Change - [x] Documentation update ## Checklist - [x] PR targets `develop` (not `main`) - [x] Read `docs/releases/v0.18.0.md` — no PR numbers, feels like a product note - [x] Does not claim attachments, marketplace, AI/self-building, or new encryption
Open in Web Open in Cursor 
## Summary by CodeRabbit * **New Features** * Added the Harbor Dusk dark theme, featuring a coastal-inspired color palette. * The theme is available under **Settings → Themes**. * **Documentation** * Added draft release notes for version 0.18.0. Co-authored-by: Cursor Agent --- docs/releases/v0.18.0.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/releases/v0.18.0.md diff --git a/docs/releases/v0.18.0.md b/docs/releases/v0.18.0.md new file mode 100644 index 00000000..3f0d0286 --- /dev/null +++ b/docs/releases/v0.18.0.md @@ -0,0 +1,17 @@ +--- +version: 0.18.0 +date: 2026-08-24 +title: Harbor Dusk +status: draft +--- + +A coastal evening palette for long writing sessions. Harbor Dusk joins +the official themes already in the desktop — same Settings → Themes +list, same token contract as Parchment, Wave, Night, and Fog. No new +fonts, no plugin pack. + +## Themes + +- **Harbor Dusk.** Official dark palette. Deep slate water, mist text, + muted teal on chrome, amber lanterns on editor links. Evening sibling + to Fog (coastal gray morning). Pick it on Settings → Themes.