Contributing

The 8 rules + review bar for contributing to llmwiki.

Thanks for wanting to contribute. This project follows strict rules about commits, PRs, and privacy — please read this before opening a PR.

Table of contents

TL;DR rules of contribution

  1. One concern per PR. Don't mix a bug fix with a new feature.
  2. Commit prefixes: feat: / fix: / docs: / chore: / test: — e.g. feat(v0.7): tool-calling bar chart (#65).
  3. Never commit real session data. raw/sessions/ is gitignored. Fixtures must be synthetic or heavily redacted.
  4. No new runtime deps. Stdlib + markdown only. Viewer loads highlight.js from a CDN — no server-side parser needed.
  5. Tests must pass. Run python3 -m pytest tests/ -q before pushing. CI verifies Python 3.12.
  6. Every PR ships docs + CHANGELOG + release-note bullet. For every user-visible change update (a) CHANGELOG.md under ## [Unreleased], (b) any docs/tutorials/* / docs/reference/* / README.md / inline --help that describes the touched surface, and (c) a one-line release-note bullet either in the CHANGELOG entry or in the PR body so gh release create can pick it up. PRs adding a new CLI subcommand, slash command, config key, or lint rule MUST add the matching row to docs/reference/*.md in the same PR. CI enforces the CHANGELOG check; reviewers check the rest.
  7. Verify old issues before fixing them. Issues accumulate; some are fixed via side-effect, some describe problems that no longer reproduce, some refer to modules that have since been refactored. Before changing code for a stale issue: (a) reproduce the problem on current main — shell command, click-path, or test that fails; (b) re-read the issue's linked code paths to confirm they still exist. If the bug is gone, close with a one-line comment citing the commit that resolved it (gh issue close N --reason completed --comment "resolved in <sha>"); if the description is wrong but there's a real bug nearby, file a new precise issue and link to the old one. Never ship a speculative fix — if you can't reproduce, say so in the PR body.
  8. Open an issue first for anything bigger than a one-file fix. Keeps scope aligned.
  9. Never fail silently in the browser. Every runtime failure in the static site must be visible on the page, not just in the console — see Static-site error handling.
  10. No personal vault details in PR text. Absolute home paths, OS usernames, vault roots, and personal session examples stay out of PR bodies / commits / CHANGELOG — use placeholders. See Privacy rules.
  11. Lint before you push. ruff check llmwiki tests scripts. A committed pre-push hook checks the Python files in your push; see Linting.
  12. Wait for CI after every push. Local green is not enough — after git push (new PR or update), watch GitHub Actions on that head SHA, report the result, and fix/repush if anything failed. See After you push.
  13. Product PRs update AWOS notes. When a PR changes llmwiki/, integrations/, tests/, .github/workflows/, docs/maintainers/, or docs/reference/, it must also change something under context/. Path filters alone decide when notes are required — there is no label escape hatch. Tutorials, scripts, packaging, guides, examples, and similar areas stay exempt. CI enforces this on every PR.

That's it. If you follow those thirteen rules your PR is 90% of the way through review.

Code of conduct

Be kind. Respect privacy. Prefer plain English to jargon. No scope creep.

Dev setup

git clone https://github.com/AlexanderMakarov/llm-wiki.git
cd llm-wiki
./setup.sh                # installs markdown, scaffolds raw/ wiki/ site/,
                          # wires the pre-push lint hook
python3 -m pytest tests/ -q

setup.sh points core.hooksPath at the committed .githooks/ directory. If you set the repo up by hand, enable the hook yourself:

git config core.hooksPath .githooks

Requirements:

  • Python ≥ 3.12 (requires-python = ">=3.12" in pyproject.toml). Installs on 3.13+ are accepted; CI lint-and-test verifies 3.12 only, so regressions on newer interpreters are untested until reported.
  • markdown (required — the only runtime dep; graph is an optional extra)
  • ruff (dev — lint)
  • pytest (dev — tests)

No other runtime deps. That's a hard rule. Syntax highlighting runs in the browser via highlight.js loaded from a CDN, so the build pipeline stays stdlib-only.

Project structure

See docs/architecture.md for the full breakdown. TL;DR:

llmwiki/              # Python package
├── cli.py            # argparse entry (`llmwiki --help`)
├── convert.py        # .jsonl → markdown
├── build.py          # markdown → HTML
├── render/           # emitted site assets (css.py, js.py, data.py)
├── agent_kit/        # packaged /wiki-* commands + user skills (`install-agent-kit`)
├── adapters/         # session-store adapters (one per agent)
└── mcp/              # MCP server (12 tools, stdio transport)

.claude/              # contributor commands, skills, rules (awos, release, …)
.cursor/rules/        # Cursor project rules
.kiro/steering/       # always-loaded rules
.githooks/            # committed git hooks (pre-push lint)
docs/                 # user-facing + framework docs
tests/                # fixtures + snapshot tests

context/ is contributor tooling. It holds the AWOS product definition, roadmap, and the per-feature specifications and delivery records that drive /awos:*. It is not part of the llmwiki product, is not shipped in the package, and never appears in a user's vault.

The repository root is not a vault. It carries a .llmwiki-source-checkout marker, and the vault-writing subcommands (init, sync, synth, synthesize, add, build, all, watch) refuse to run against a directory carrying it unless you name a vault — --vault <path> on the command line, or vault.default_path in your gitignored config.json. Use --vault demo for the example vault that ships with the repo, and a throwaway path such as --vault .worktree-vault for scratch runs. Installed packages carry no marker, so this never affects users.

Agent instruction files

This repo is two things at once, and the instruction files split along that seam. Getting the two confused is the most common way an agent goes wrong here.

CLAUDE.md and AGENTS.md at the repo root are the product schema. They describe how a coding agent maintains a user's knowledge vault — the raw/wiki/site/ pipeline, page formats, ingest and query workflows. They ship to users. Never put repo, PR, or process rules in them.

CONTRIBUTING.md — this file — governs work on llmwiki itself. Because Claude Code auto-loads only CLAUDE.md and Cursor auto-loads only AGENTS.md plus .cursor/rules/, neither agent would otherwise ever see this file. Three surfaces route them here:

Surface Tool Loads when
.claude/rules/contributing.md Claude Code agent reads a file under llmwiki/, tests/, scripts/, docs/, or the root build files (paths: frontmatter)
.cursor/rules/contributing.mdc Cursor every session (alwaysApply: true)
.kiro/steering/contributing-rules.md Kiro every session (load: always)

Plus a short pointer block at the top of CLAUDE.md and AGENTS.md, so an agent that only ever reads the root schema still finds its way here.

Two rules for maintaining them:

  1. They are pointers, not copies. Each one distils the same handful of non-negotiables and links back here. An earlier version of the Kiro file restated the rules in full and drifted — it ended up mandating commit types this guide doesn't accept and a branch name that no longer matched. Process rules change here first; the pointers only change when the summary is wrong.
  2. Keep them free of machine-specific detail. .cursor/ is gitignored except for an explicit allowlist in .gitignore, so a local rule naming your own vault path or directory layout stays on your machine. Add a new shared Cursor rule by allowlisting it there deliberately.

User-facing /wiki-* slash commands and skills live in llmwiki/agent_kit/ and ship in the package. Contributors who want them locally (so Claude Code discovers /wiki-sync from this clone) run:

python3 -m llmwiki install-agent-kit --dest .claude

That copies commands/ and skills/ under .claude/. Re-run after pulling an upgrade; a file you edited that now differs from the packaged version is saved as <file>.bak beside it.

Optional: AWOS (spec → hire → implement)

Maintainers who want the AWOS loop in Cursor or Claude Code: see docs/maintainers/AWOS-CURSOR.md. Install/update with ./scripts/update-awos.sh (Layer A); add --plugin for marketplace extras as /awos-flow etc. This is contributor tooling, not the llmwiki product schema.

Commit + PR rules

Adapted from the parent Open Source Project Framework:

Identity

  • git config user.name "Your Name" (use your own GitHub identity)
  • Never add Co-authored-by: Claude, Co-authored-by: AI, or similar AI attribution lines. Commits from this project are human-authored.

PR size

  • One intent per PR. Don't mix "add a new adapter" with "fix a CSS bug". Split before opening.
  • ≤500 lines of diff. If the PR gets larger than that, the reviewer will ask you to split.
  • Atomic commits. Each commit tells a clear story; renames isolated from behavior changes.

PR title format

Conventional Commits. Types we accept:

Type When Version bump
feat New user-visible capability minor
fix Bug fix patch
chore Maintenance, deps, CI, version bumps patch
docs Docs only patch
test Tests only patch
refactor Internal restructuring, no behavior change patch
perf Performance improvement patch
security Security fix or hardening patch
release Version bump + CHANGELOG promotion

Optionally scope with a version: feat(v0.8): tool chart. Include the issue number: Closes #65 in the body.

PR body — 16-box pre-merge checklist

Every box must be checked (or have a one-line waiver). .github/PULL_REQUEST_TEMPLATE.md is the authoritative list; it covers:

  1. One intent (no mixing concerns)
  2. CI green
  3. Linked issue via Closes #N
  4. Conventional-commit title
  5. Tests added/updated (happy path + edge case)
  6. CHANGELOG under Unreleased
  7. Breaking changes flagged + labeled breaking
  8. No new runtime deps (stdlib + markdown only)
  9. AWOS context updated — PRs that change llmwiki/, integrations/, tests/, .github/workflows/, docs/maintainers/, or docs/reference/ must also change something under context/ (no label bypass; tutorials, scripts, and similar paths exempt)
  10. No real session data in raw/ or fixtures
  11. No machine-specific paths or secrets
  12. Docs updated for user-visible changes
  13. Release notes drafted — one line fit for the next gh release create --notes
  14. UI verified in light AND dark mode (for CSS/UI changes) — screenshots attached
  15. A11y verified — keyboard nav, focus rings, WCAG 2.1 AA (≥ 4.5:1 contrast)
  16. Reviewer has read every changed line (no rubber-stamping)

Branch protection

  • Default branch is main; never push directly — PR required.
  • CI must pass before merge.
  • Required check display name AWOS context updated (job in .github/workflows/pr-lint.yml) must be added once in repo settings → branch protection so a red gate blocks merge — maintainers do this after the job ships.
  • Signed commits required.
  • Branch must be up-to-date with main before merge.

Adding a new adapter

See docs/framework.md §5.25 Adapter Flow for the full contract. Minimum requirements:

  1. One file under llmwiki/adapters/<agent>.py that: - Subclasses BaseAdapter - Registers itself via @register("<agent>") - Sets session_store_path to the agent's default location(s) - Declares SUPPORTED_SCHEMA_VERSIONS

  2. At least one fixture under tests/fixtures/<agent>/minimal.jsonl — synthetic or heavily redacted.

  3. One snapshot test under tests/snapshots/<agent>/minimal.md — the expected markdown output.

  4. One test under tests/test_<agent>_adapter.py that runs the converter against the fixture and diffs against the snapshot.

  5. One documentation page at docs/adapters/<agent>.md.

  6. A CHANGELOG entry under ## [Unreleased].

  7. One line in README.md under "Works with".

Cross-platform path requirement

DEFAULT_ROOTS (or DEFAULT_VAULT_PATHS / session_store_path) must work on macOS, Linux, and Windows. Two patterns are acceptable:

  1. Dot-directory (Path.home() / ".agent" / ...) -- works on all three platforms by default; a single entry is fine.
  2. OS-specific directories (e.g. ~/Library/Application Support/..., ~/.config/..., ~/AppData/Roaming/...) -- you need at least one entry per platform. Use inline comments to label which path is for which OS.

Always use Path.home() -- never hardcode /Users/, /home/, or C:\Users\. The test in tests/test_cross_platform_paths.py enforces these rules.

Adapters with no default paths (like pdf, where the user must configure roots) are exempt.

Review checklist for adapter PRs

  • [ ] Adapter declares SUPPORTED_SCHEMA_VERSIONS
  • [ ] DEFAULT_ROOTS covers macOS + Linux + Windows (see above)
  • [ ] Fixture is under 50 KB and contains no real PII
  • [ ] Snapshot test passes locally
  • [ ] docs/adapters/<agent>.md exists and is linked from README
  • [ ] Graceful degradation: unknown record types are skipped, not crashed on
  • [ ] No new runtime deps introduced

Static-site error handling

The viewer is vanilla JS with no error boundary and no telemetry — a swallowed failure is one nobody ever hears about. #20 sat open for months because a broken search index and an empty corpus rendered identically. Rules for any JS the build emits (llmwiki/render/js.py):

  1. No silent catch. Report through window.__llmwikiReportError(context, err) — it logs and renders a dismissible role="alert" bar. Name the capability that broke ("Related pages unavailable"), not the function.
  2. Say "broken", not "empty". An empty result list must never be the only signal that loading failed; the palette renders a .palette-note row instead.
  3. Degrade partially. One missing search chunk drops that project and reports it rather than aborting the whole index.
  4. Keep the error surface independent of what it reports. The bar is styled inline, so it survives a broken stylesheet.
  5. Test the failure path — see tests/test_file_protocol_search.py.
  6. Never fetch build-emitted data. Blocked over file://, and users do double-click index.html. Emit a .js sidecar with write_js_sidecar() (llmwiki/render/data.py), load it via window.__llmwikiLoadData(url, key).

Privacy rules

llmwiki processes session transcripts that may contain PII, API keys, file paths, and secrets. These rules are non-negotiable:

  1. Redaction is on by default. Username, API keys, tokens, passwords, and emails are redacted before anything hits raw/.
  2. Never commit real session data. raw/ is gitignored. Fixtures under tests/fixtures/ must be synthetic or heavily redacted.
  3. Never commit machine-specific paths. No .claude/settings.local.json, no .ingestion-state.json, no .framework/, no .temp/.
  4. Privacy grep runs in the test suite (tests/test_privacy_username.py): tracked .md / .py must not contain the upstream maintainer's real username (fixtures use USER).
  5. No telemetry, ever. The tool never calls home.
  6. Localhost-only binding by default. The server binds to 127.0.0.1 unless the user explicitly passes --host 0.0.0.0.
  7. No local vault / personal machine details in PRs or commits. PR bodies, commit messages, issue comments, and CHANGELOG entries must not include absolute home paths, OS usernames, vault roots, or personal session examples. Use placeholders (/home/USER/…, <vault>, <user>).

Markdown conventions

Never hard-wrap prose at a fixed column. One paragraph is one line, however long. Line width is the renderer's job, not the file's, and a hard-wrapped paragraph turns a one-word edit into a diff that reflows every following line — which buries the actual change and causes needless merge conflicts. This applies to every .md file in the repo, including CLAUDE.md, AGENTS.md, and the agent rule files under .claude/rules/, .cursor/rules/, and .kiro/steering/.

Wrapping is fine inside fenced code blocks, tables, and anywhere the line is not prose.

Linting

ruff check llmwiki tests scripts        # lint (must exit 0)
ruff check --fix llmwiki tests scripts  # only with an explicit --select for safe families — see below

Ruff config lives in pyproject.toml under [tool.ruff]: line length 120, target py312, selecting E, F, I, B, UP, and PLC0415 (import-outside-top-level). E501 and E402 stay ignored. PLC0415 is enforced everywhere except scripts/** (one-off maintenance scripts; exempt via per-file-ignores).

Run lint before you push. The committed pre-push hook checks the Python files in your push and rejects it on violations; git push --no-verify bypasses it, but say why in the PR. CI runs ruff check llmwiki tests scripts and fails the build on findings (#58).

Do not run bare ruff check --fix. F401 deletes deliberate package-surface re-exports. Prefer mechanical families first (--select UP,I,F541,…), and only run --select F401 after every intentional re-export carries # noqa: F401. Two conventions the linter can't fully check on its own:

  • Imports belong at the top of the module. Deferred imports inside a function are legitimate only for (1) an optional extra (trafilatura, markitdown, graphifyy, networkx, …) or (2) a proven import cycle — and the reason goes in a # noqa: PLC0415 comment on the line. Stdlib modules are never deferred.
  • A deliberate re-export needs # noqa: F401. Prefer importing from the owning module over growing facade re-exports. ruff --fix will otherwise delete an import that looks unused in its own module but is part of that module's public surface.
  • Prefer low-level imports from the owning module. Do not reach for helpers via a high-level facade (from llmwiki.build import md_to_html) when the symbol lives in a dedicated module.

After you push

Local ruff + pytest are necessary but not sufficient. Coding agents (and humans) working on this repository must wait for GitHub Actions on the pushed head SHA before treating a PR as ready or ending the turn after a push.

  1. After git push that creates or updates a PR branch, poll checks until they finish (or fail). Do not stop at "pushed successfully."
  2. Prefer: gh pr checks <n> --watch (or gh run watch for the latest run on the branch). For a one-shot status: gh pr checks <n> / gh run list --branch <branch> --limit 5.
  3. Report the outcome to the user (green / red / cancelled). If anything required is red, fetch failing logs (gh run view <id> --log-failed or the job URL), fix, push again, and wait again.
  4. Do not declare the PR merge-ready, ask for review as "done," or move on to unrelated work while required checks are still pending — unless the user explicitly tells you not to wait.

Testing

python3 -m pytest tests/ -q             # all tests
python3 -m pytest tests/test_convert.py # one file
python3 -m llmwiki build                # smoke test build
python3 -m llmwiki --version            # version check

Suite autouse in tests/conftest.py isolates the default vault and neutralizes repo-root config.json for in-process merges (#142). Tests that intentionally exercise the user-config overlay must monkeypatch _USER_CONFIG / USER_CONFIG_FILE themselves.

Every adapter must ship with:

  • A fixture (synthetic or heavily redacted)
  • A snapshot test
  • A graceful-degradation test (passes an unknown record type)

Questions?

Open an issue with the question label on AlexanderMakarov/llm-wiki.

Keyboard shortcuts

⌘K / Ctrl+KOpen command palette
/Focus search
g hGo to home
g pGo to projects
g sGo to sessions
j / kNext / prev row (tables)
?Show this help
EscClose dialogs

Structured queries

Mix key:value filters with free text in the palette:

type:sessionOnly session pages
project:llm-wikiFilter by project name (substring)
model:claudeFilter by model name (substring)
date:>2026-03-01Sessions after a date
date:<2026-04-01Sessions before a date
tags:rustPages mentioning a tag/topic
sort:dateSort results by date (newest first)

Example: type:session project:llm-wiki date:>2026-04 sort:date