Skip to content

Latest commit

 

History

History
171 lines (138 loc) · 7.97 KB

CONTRIBUTING.md

File metadata and controls

171 lines (138 loc) · 7.97 KB

Contributing to omind

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.

Mirrors

omind lives on two forges, kept in sync:

Issues and pull requests on either are welcome. Commits land on both.

Development setup

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).

Quality gates

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.

Project layout

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.

Conventions

  • 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]) — use TypeVar.
  • Path safety. Anything that reads or writes a note must go through OmiStore.safe_name so 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, or No-agent: true if 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. A Co-authored-by: Claude … should carry Claude-Session: too, so the transcript stays reachable. Enforced in CI, and locally once you enable the repo's hooks:
    git config core.hooksPath .githooks

Versioning

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.

Releasing

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.

  1. Merge the PR that bumped the version (pyproject.toml, src/omind/__init__.py, uv.lock, and its CHANGELOG.md heading) to main.
  2. Cut the release from main, with the tag set to exactly v + the pyproject.toml version. Use --generate-notes, or pass the CHANGELOG.md section for that version with --notes-file:
    gh release create v<version> --target main --title "omind <version>" --generate-notes
  3. .github/workflows/publish.yml fires on release: published. It builds the sdist and wheel with uv build, fails if the tag does not match the packaged version, runs twine check --strict, and uploads to PyPI with the PYPI_API_TOKEN secret, which lives only in the pypi environment (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.

Pull requests

  1. Branch off main with a conventional name: feat/…, fix/…, test/…, docs/…, chore/…, refactor/….
  2. Make the change; keep the three quality gates green.
  3. Use conventional-commit subjects (feat:, fix:, docs:, …).
  4. Open the PR against main on 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.

License

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/