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
- Code of conduct
- Dev setup
- Project structure
- Agent instruction files
- Commit + PR rules
- Adding a new adapter
- Static-site error handling
- Privacy rules
- Markdown conventions
- Linting
- Testing
TL;DR rules of contribution
- One concern per PR. Don't mix a bug fix with a new feature.
- Commit prefixes:
feat:/fix:/docs:/chore:/test:— e.g.feat(v0.7): tool-calling bar chart (#65). - Never commit real session data.
raw/sessions/is gitignored. Fixtures must be synthetic or heavily redacted. - No new runtime deps. Stdlib +
markdownonly. Viewer loads highlight.js from a CDN — no server-side parser needed. - Tests must pass. Run
python3 -m pytest tests/ -qbefore pushing. CI verifies Python 3.12. - Every PR ships docs + CHANGELOG + release-note bullet. For every user-visible change update (a)
CHANGELOG.mdunder## [Unreleased], (b) anydocs/tutorials/*/docs/reference/*/README.md/ inline--helpthat describes the touched surface, and (c) a one-line release-note bullet either in the CHANGELOG entry or in the PR body sogh release createcan pick it up. PRs adding a new CLI subcommand, slash command, config key, or lint rule MUST add the matching row todocs/reference/*.mdin the same PR. CI enforces the CHANGELOG check; reviewers check the rest. - 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. - Open an issue first for anything bigger than a one-file fix. Keeps scope aligned.
- 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.
- 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.
- Lint before you push.
ruff check llmwiki tests scripts. A committedpre-pushhook checks the Python files in your push; see Linting. - 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. - Product PRs update AWOS notes. When a PR changes
llmwiki/,integrations/,tests/,.github/workflows/,docs/maintainers/, ordocs/reference/, it must also change something undercontext/. 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"inpyproject.toml). Installs on 3.13+ are accepted; CIlint-and-testverifies 3.12 only, so regressions on newer interpreters are untested until reported. markdown(required — the only runtime dep;graphis 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:
- 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.
- 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:
- One intent (no mixing concerns)
- CI green
- Linked issue via
Closes #N - Conventional-commit title
- Tests added/updated (happy path + edge case)
- CHANGELOG under Unreleased
- Breaking changes flagged + labeled
breaking - No new runtime deps (stdlib +
markdownonly) - AWOS context updated — PRs that change
llmwiki/,integrations/,tests/,.github/workflows/,docs/maintainers/, ordocs/reference/must also change something undercontext/(no label bypass; tutorials, scripts, and similar paths exempt) - No real session data in
raw/or fixtures - No machine-specific paths or secrets
- Docs updated for user-visible changes
- Release notes drafted — one line fit for the next
gh release create --notes - UI verified in light AND dark mode (for CSS/UI changes) — screenshots attached
- A11y verified — keyboard nav, focus rings, WCAG 2.1 AA (≥ 4.5:1 contrast)
- 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
mainbefore merge.
Adding a new adapter
See docs/framework.md §5.25 Adapter Flow for the full contract. Minimum requirements:
-
One file under
llmwiki/adapters/<agent>.pythat: - SubclassesBaseAdapter- Registers itself via@register("<agent>")- Setssession_store_pathto the agent's default location(s) - DeclaresSUPPORTED_SCHEMA_VERSIONS -
At least one fixture under
tests/fixtures/<agent>/minimal.jsonl— synthetic or heavily redacted. -
One snapshot test under
tests/snapshots/<agent>/minimal.md— the expected markdown output. -
One test under
tests/test_<agent>_adapter.pythat runs the converter against the fixture and diffs against the snapshot. -
One documentation page at
docs/adapters/<agent>.md. -
A CHANGELOG entry under
## [Unreleased]. -
One line in
README.mdunder "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:
- Dot-directory (
Path.home() / ".agent" / ...) -- works on all three platforms by default; a single entry is fine. - 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_ROOTScovers macOS + Linux + Windows (see above) - [ ] Fixture is under 50 KB and contains no real PII
- [ ] Snapshot test passes locally
- [ ]
docs/adapters/<agent>.mdexists 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):
- No silent
catch. Report throughwindow.__llmwikiReportError(context, err)— it logs and renders a dismissiblerole="alert"bar. Name the capability that broke ("Related pages unavailable"), not the function. - Say "broken", not "empty". An empty result list must never be the only signal that loading failed; the palette renders a
.palette-noterow instead. - Degrade partially. One missing search chunk drops that project and reports it rather than aborting the whole index.
- Keep the error surface independent of what it reports. The bar is styled inline, so it survives a broken stylesheet.
- Test the failure path — see
tests/test_file_protocol_search.py. - Never
fetchbuild-emitted data. Blocked overfile://, and users do double-clickindex.html. Emit a.jssidecar withwrite_js_sidecar()(llmwiki/render/data.py), load it viawindow.__llmwikiLoadData(url, key).
Privacy rules
llmwiki processes session transcripts that may contain PII, API keys, file paths, and secrets. These rules are non-negotiable:
- Redaction is on by default. Username, API keys, tokens, passwords, and emails are redacted before anything hits
raw/. - Never commit real session data.
raw/is gitignored. Fixtures undertests/fixtures/must be synthetic or heavily redacted. - Never commit machine-specific paths. No
.claude/settings.local.json, no.ingestion-state.json, no.framework/, no.temp/. - Privacy grep runs in the test suite (
tests/test_privacy_username.py): tracked.md/.pymust not contain the upstream maintainer's real username (fixtures useUSER). - No telemetry, ever. The tool never calls home.
- Localhost-only binding by default. The server binds to
127.0.0.1unless the user explicitly passes--host 0.0.0.0. - 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: PLC0415comment 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 --fixwill 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.
- After
git pushthat creates or updates a PR branch, poll checks until they finish (or fail). Do not stop at "pushed successfully." - Prefer:
gh pr checks <n> --watch(orgh run watchfor the latest run on the branch). For a one-shot status:gh pr checks <n>/gh run list --branch <branch> --limit 5. - Report the outcome to the user (green / red / cancelled). If anything required is red, fetch failing logs (
gh run view <id> --log-failedor the job URL), fix, push again, and wait again. - 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.