# Havemind, agent instructions Havemind is a private synchronisation layer for Obsidian (see `README.md`). This file applies within this repository and overrides conflicting upstream context (global CLAUDE.md, skills other than those listed below) to the extent that `plan/01-rules-and-glossary.md` rule 8-9 explicitly describes it (access to `sapserver`, local commit within the `/loop` loop). ## Repo structure - `specs/001-mvp.md`, product requirements (canonical, approved 2026-07-15). - `plans/001-technical-plan.md`, architecture and engineering contract (canonical). - `plans/004-006`, follow-up plans. 005 and 006 shipped; 004 (E2EE) is a design study, not scheduled. - `plan/`, planning package (generated by the `loopstart` skill) closing out T018-T033 and the pilot on `sapserver`. If you're continuing this project, start with `plan/README.md`. ## Canonical data Do not guess requirements or architecture. Everything is in `specs/` and `plans/`. Where `plan/` conflicts with these files → `specs/`/`plans/` win (see `plan/01-rules-and-glossary.md` rule 1), except for explicitly documented deviations in the `plan/README.md` "Open decisions" section. ## Commands ```bash npm ci npm run build npm run typecheck npm run lint npm test npm run test:coverage npm run dev:server npm run dev:plugin npm run compose:smoke npm run test:e2e ``` ## Hard-rules summary (full version: `plan/01-rules-and-glossary.md`) 1. TDD red-green-refactor, no exceptions. 2. The server is opaque, it never computes diff/provenance/merge. 3. Zero silent overwrites, zero trust in a client-supplied `actor_id`. 4. Secrets never go into the repo, logs, or subagent reports. 5. The agent has `ssh sapserver` access and may modify it (routine operations), but steps that require the `sudo` password, Tailscale Funnel, the `docker` group, or are irreversible, always ask the user first. A local `git commit` within the `/loop` loop, no need to ask. `git push`, always ask first. 6. Zero home-grown cryptography, zero Kubernetes/Portainer/Watchtower/databases "just in case". 7. All repository content (docs, comments, commit messages, plans) must be written in English. 8. One selector, one declaration, one place in `styles.css`. Never append a second block for a selector that already exists, edit the existing rule. A property set from a `--havemind-*` design token must not also be set from a generic variable elsewhere. 9. Never edit a plugin copy inside a vault. Edit the repository, build, then deploy to **every** installed copy and verify by hash. ## Pane styling The pane's geometry is transcribed from a Claude Design handoff that uses inline styles and no classes, so nothing but a test can keep the two in agreement: - `scripts/extract-design-tokens.mjs` reads the recurring values into `tokens.json` (`--check` in CI fails when it goes stale). - `src/ui/design-tokens.test.ts` fails when `styles.css` disagrees with those tokens. - `src/ui/stylesheet-conflicts.test.ts` fails when a selector sets one property to two different values, or styles a class nothing renders. When the pane "doesn't look like the design", check in this order: (1) does the stylesheet reach the screen at all, add a marker rule to the *installed* file and reload the plugin; (2) does the sheet contradict itself; (3) only then, are the values wrong. Rounds of value edits on a sheet that contradicts itself change nothing, because the winner is decided by source order. ## Dependencies: the lockfile is resolved on LINUX, never on macOS `package-lock.json` records a tree that differs by platform. `@emnapi/core` and `@emnapi/runtime` 1.11.3 exist as TOP-LEVEL entries only on Linux; on macOS the tree carries just 1.11.1, nested under rolldown. A lockfile regenerated on a Mac is therefore missing entries the Docker build needs, and `npm ci` fails with `Missing: @emnapi/core@1.11.3 from lock file`. The server then cannot be rebuilt at all, which is how this rule got written (2026-09-05, three failed attempts on the wrong platform). `npm ci --dry-run` on macOS PASSES against that broken lockfile, so it proves nothing. Only a real `npm ci` inside the build image does. To change a dependency: 1. Edit `package.json` as usual. 2. Regenerate inside the image the Dockerfile builds from, not on the host: ```bash docker run --rm -v "$PWD":/w -w /w \ node:22.23.1-bookworm-slim npm install --package-lock-only ``` 3. Verify `npm ci` exits 0 locally too, so the file is not Linux-only. 4. Confirm the Docker build succeeds before committing. Never run `rm package-lock.json && npm install` to fix a dependency problem. It silently drops every platform-optional package the host cannot resolve. ## Vault notes, update on every push The project's running notes live in the user's Obsidian vault, in `~/ObsidianVaults/Mikolaj Private/Havemind/`: | Note | Holds | |---|---| | `Havemind - indeks` | entry point, quick facts, repo map, the checking-off convention | | `Havemind - roadmapa` | phases F0-F9, what closed, what is left, suggested order | | `Havemind - 10 otwartych pozycji` | every open item: what it is, where in the code, what to do, how to verify | | `Havemind - stan backlogu` | review and any disagreement between the backlog and the code | | `Havemind - dziennik wydan` | release log, newest first | ## Releasing: TWO repositories, both or the release is invisible This project has two repos and a release is not finished until it lands in both. | Repo | Role | |---|---| | `MikolajSapek/havemind` | source. All work happens here. | | `mikolajsapek/obsidian-havemind` | distribution. Built artefacts in the root; **this is what the Obsidian catalogue reads.** | The catalogue entry for `havemind-sync` points at the DISTRIBUTION repo. Tagging only the source repo publishes a release nobody can install: 1.2.2 through 1.4.0 all shipped that way and users stayed on 1.2.1 for two weeks, which is how this rule got written (2026-09-04). After tagging the source repo, mirror it: 1. Clone or pull `mikolajsapek/obsidian-havemind`. 2. Copy from `apps/obsidian-plugin/`: `main.js`, `styles.css`, `manifest.json`, `manifest-beta.json`, and `versions.json` from the repo root. 3. Commit, push, tag with the SAME version, push the tag. Its own release workflow builds the GitHub release from there. 4. Verify with `gh release list --repo mikolajsapek/obsidian-havemind`, not just the source repo's. 5. Give the distribution release its notes: `gh release edit X.Y.Z --repo mikolajsapek/obsidian-havemind --notes-file docs/release-notes/X.Y.Z.md`. Its workflow creates the release without a body, and the catalogue review flags a release with no description (2026-10-14). A release is reported as done only after step 5. ### The README is part of the release, not documentation about it Bump the version in `apps/obsidian-plugin/manifest.json` and `manifest-beta.json` AND in the README's `**Version X.Y.Z` line, in the same commit. The README's first paragraph is the first thing a visitor reads, and it sat at "Version 1.2.3" while 1.4.7 was live in the catalogue: seven releases, every one of them announcing the wrong version (2026-09-10). `tests/readme-version.test.ts` fails when the README version and the manifest disagree, so `npm run verify` catches it before the tag exists. Do not delete that test to make a release pass, fix the README. Before tagging, re-read the README for anything the release made untrue: - the version line, including its short description of what the release is; - **What it does**, when the release added or removed a user-visible capability; - the platform claims, when support changes; - screenshots and `design/brand/*.png`, when the pane's appearance changed. ### A push is not done until CI is green **After every `git push`, watch the run to completion before reporting the push as finished:** ```bash gh run list --repo MikolajSapek/havemind --limit 3 \ --json status,conclusion,name --jq '.[] | "\(.status) \(.conclusion // "-") \(.name)"' ``` A green `npm run verify` locally does NOT mean CI passes. The property battery in `diff3.property.test.ts` draws fresh random cases on every run: the merge fix in 1.4.9 passed 25 local draws three times in a row and failed in CI after 437, having silently dropped a line. Anything random, timing-dependent, or platform-dependent can only be caught by looking at the run that actually ran (2026-09-14). When CI fails, pull the counterexample out of the log rather than guessing: ```bash gh run view --repo MikolajSapek/havemind --log-failed | grep -aE "Counterexample|seed:" ``` Reproduce it in a scratch test first, fix, then re-run the property file several times before pushing again. **After every push to `origin/main`, append an entry to `Havemind - dziennik wydan`** before reporting the push as done: the date, the commits with one line each, what changed, and the state of `npm run verify`. A push that changed the backlog also updates `Havemind - 10 otwartych pozycji` and `Havemind - roadmapa`, and a release adds its tag, the release URL and whether the attestation verified. Rules for these notes: - Polish, matching the vault. Code, paths and identifiers stay in English. - No em-dashes, the vault follows the same rule as this repository. - Cite a file and line only after reading it in the current tree; a stale line number is worse than none. - Never check an item off in `plan/11-BACKLOG.md` before its AC passes, and record the evidence next to it. An item that no longer applies is closed as out of scope with a reason, never checked off (see T033, SRV-04/05). - Every note links at least one other note in the folder, and the folder is reachable from `HOME.md`. ## Ignore conflicting upstream context If other context (an older project, another CLAUDE.md higher up the directory tree) suggests a different stack, a different architecture, or different commit rules than the above, this file and `plan/` win within this repository.