Thanks for your interest. omind is small and the bar to contribute is low — file an issue, send a patch, or open a pull request. This page covers the dev setup and the checks a change has to pass.
Working through an AI agent? Point it at AGENTS.md as well — it carries the subsystem invariants, in-flight handoffs, and known dead ends that this page deliberately leaves out.
omind lives on two forges, kept in sync:
- GitHub — https://github.com/CryptoJones/omind
- Codeberg — https://codeberg.org/CryptoJones/omind
Issues and pull requests on either are welcome. Commits land on both.
Requires Python 3.11+ (CI runs 3.11–3.14 on Linux, plus 3.11 and 3.14 on Windows and macOS — omind is supported on all three).
git clone https://github.com/CryptoJones/omind.git
cd omind
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"The editable install puts the omind command on your PATH and pulls in the dev
tools (pytest, ruff, mypy, httpx, pip-audit, types-PyYAML).
Every change must keep all four green (the same gates CI runs on every PR):
ruff check . # lint (line length 100; rules E F I N W UP B SIM)
mypy src # static types, --strict
pip-audit # dependency vulnerability scan
pytest -v # tests in tests/ruff format is fine to run, but the lint pass is the gate. Type-check src
(the --strict settings live in pyproject.toml). Tests should pass on every
supported Python version; if you only have one locally, CI covers the matrix.
src/omind/
├── cli.py argparse entry point: one `_run_*` handler per subcommand
├── provision.py `omind setup`/`doctor` — idempotent MCP + mesh wiring
├── server.py `omind node` — the local mesh-node MCP server (mcp SDK)
├── mesh.py `omind mesh` — git replication: init/sync/daemon/peers
├── merge.py the field-level 3-way note merge driver (git merge=omi)
├── clock.py per-note Lamport revisions (cross-node ordering truth)
├── agents.py Hermes/OpenClaw provisioners (subclass provision.py's)
├── quickstart.py `omind quickstart` — the manual steps `setup` automates
├── backup.py `omind backup` — encrypted restic backup + systemd timer
├── hooks.py `omind hook` — auto-journal hook handlers + SessionStart priming
├── journal.py journal migration + weekly `omind rollup`
├── transfer.py `omind export`/`import` — json / tar.gz dataset bundles
├── notes.py `upsert_note` — the single write entry point for external writers
├── store.py framework-free note CRUD + template parse/render + index
├── seeds.py seed content: captured .obsidian JSON + note templates
├── paths.py canonical filenames (single source of truth for names)
├── proc.py shared subprocess runner: capture, timeouts, Windows shims
├── filelock.py portable flock shim (fcntl on POSIX, msvcrt on Windows)
└── web/
├── app.py FastAPI routes (JSON API) + static mount
└── static/ the single-page UI (index.html, app.js, app.css)
tests/ pytest suites mirroring the modules above (+ conftest.py
isolating XDG_STATE_HOME for every test)
e2e/ opt-in end-to-end suite against real disposable nodes;
every test skips unless OMIND_E2E_PROVIDER=podman|runpod
is set (excluded from the default `pytest` run — see
e2e/README.md)
store.py has no web dependency — it is the file-I/O core that both the CLI and
the web app build on. Keep it framework-free.
- License header. Every source file starts with the two-line SPDX block:
# SPDX-License-Identifier: Apache-2.0 # Copyright 2026 Aaron K. Clark
- Python 3.11 floor. No PEP 695 generic syntax (
def f[T]) — useTypeVar. - Path safety. Anything that reads or writes a note must go through
OmiStore.safe_nameso path traversal stays impossible. There are tests that enforce this; don't route around them. - Tests with changes. New behavior comes with a test. Bug fixes come with a regression test.
- Declare authorship. Every commit carries either a
Co-authored-by:trailer naming the agent (or person) that helped write it, orNo-agent: trueif it was hand-written. Both are fine; silence is not — attribution is how a regression gets traced back to what caused it, and 8.9.0–8.10.1 shipped on five commits with no trailer at all, so those two releases are permanently unattributable. ACo-authored-by: Claude …should carryClaude-Session:too, so the transcript stays reachable. Enforced in CI, and locally once you enable the repo's hooks:git config core.hooksPath .githooks
omind follows SemVer. Start a fix at the patch level — a
bug fix, a CI/test fix, a docs touch-up bumps the patch (3.0.0 → 3.0.1), never
the minor. New backward-compatible features bump the minor (3.0.x → 3.1.0);
breaking changes or a flagship release bump the major. When in doubt, prefer the
smaller bump.
The version is declared in three places that must stay in lockstep — pyproject.toml,
src/omind/__init__.py, and uv.lock — and a test asserts __version__ matches
pyproject.toml, so a release can't leave one behind. Every version bump gets a
CHANGELOG.md entry under its number.
A release is a GitHub release on a v<version> tag that points at merged main.
Publishing it is what ships the package to PyPI.
- Merge the PR that bumped the version (
pyproject.toml,src/omind/__init__.py,uv.lock, and itsCHANGELOG.mdheading) tomain. - Cut the release from
main, with the tag set to exactlyv+ thepyproject.tomlversion. Use--generate-notes, or pass theCHANGELOG.mdsection for that version with--notes-file:gh release create v<version> --target main --title "omind <version>" --generate-notes
.github/workflows/publish.ymlfires onrelease: published. It builds the sdist and wheel withuv build, fails if the tag does not match the packaged version, runstwine check --strict, and uploads to PyPI with thePYPI_API_TOKENsecret, which lives only in thepypienvironment (only the upload job can read it).
To check a build without uploading, run the workflow by hand
(Actions → publish → Run workflow). The dry_run box is checked by default. To
re-publish a release whose upload failed, run the workflow by hand from that tag with
dry_run unchecked; PyPI never accepts the same version twice. A manual run skips the
tag-vs-version check (that check only runs on release: published), so it uploads
whatever version the chosen ref holds: always pick the release tag as the ref, and run
it once with dry_run checked before the real upload.
If you rename the pypi environment, move the PYPI_API_TOKEN secret with it, or the
upload fails with a 403. To rotate the token, mint a new one on pypi.org and run
gh secret set PYPI_API_TOKEN --env pypi.
- Branch off
mainwith a conventional name:feat/…,fix/…,test/…,docs/…,chore/…,refactor/…. - Make the change; keep the three quality gates green.
- Use conventional-commit subjects (
feat:,fix:,docs:, …). - Open the PR against
mainon either mirror. Describe the what and why, and reference any issue it closes.
A maintainer squash-merges and reconciles the two mirrors so their main
branches stay at the same commit.
By contributing, you agree your contributions are licensed under the project's Apache 2.0 license.
Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/