description: How Karni's agent guidance works: rules as structured data in one module, the context, rules, check and explain commands, the generated adapters and the MCP server.¶
Karni for AI agents¶
The CLI is the mechanism, not the file. Karni's agent guidance lives in one
place, karni.agent.rules (packages/karni/karni/agent/rules.py), as
structured data: every rule has a code, a category, a severity, a rationale,
a fix and the enforcement rule it maps to. Agents read it through the karni
command (or the MCP server). The files tools look for (AGENTS.md,
CLAUDE.md, .cursor/rules/karni.mdc, …) are thin generated adapters that
all say the same thing: run karni context and karni rules first, then
follow what they return.
CLI surface¶
| Command | What it returns |
|---|---|
karni context |
Live inventory: APPS, MODELS, VIEWSETS, COMPONENTS, HOOKS, PAGES, MODULES, BASES |
karni rules |
Every convention as CODE summary (about 400 tokens for Karni's own rules, measured) |
karni rules --explain K101 |
One rule in full: statement, why, fix, enforcing rule id |
karni rules --procedure new-model |
The steps of a standard procedure |
karni rules --check FILE [--check FILE…] |
Static violations with code, line, message and fix; exit 1 if any |
karni rules --json |
The raw data (rules, procedures, adapter overrides) |
karni rules sync |
Regenerate every adapter and slash-command file |
karni rules sync --check |
Exit 1 when any generated file is stale (CI gate) |
karni explain E104 |
Any code (error E/W, command C, module PAY, rule K): why Karni objects and the exact fix |
karni mcp |
The same surface as MCP tools: context, rules, check |
karni context and karni rules together are enough to start work without
reading files. karni rules --check is a fast single-file subset of the real
enforcement (AST for Python: model-base, viewset-base, viewset-access (direct
BaseViewSet subclasses only), access-override, unpaginated-action,
admin-convention, raw-sql, http-client, redundant-serializer, opt-out reasons;
a comment- and string-aware scan for TS/JS: raw elements, the page cap, deep
imports, direct HTTP, eslint-disable reasons). karni check and the @karni/ui ESLint preset
stay authoritative; cross-file rules (duplicates) are not checked per file.
Path allowances use the documented defaults (*/sql/*, */services/*,
src/api/), matched relative to the project root.
None of this imports Django or project code. karni.agent is stdlib-only;
the CLI finds it, in order: in its own environment (local), through the
project's interpreter like karni context does (project: .venv, else the
running compose service; this answers with the project's karni version), or
from the copy bundled into karni-cli (bundled, what karni new uses when
only karni-cli is installed). Force one with
KARNI_AGENT_SOURCE=local|project|bundled.
Error codes¶
Every error Karni prints starts with a stable code and ends with a link:
E104: EventViewSet: Owned('organiser') does not lead from Event to the user
model ... See …/errors/#e104. The message names the class, field or setting
to change; karni explain E104 (offline, no project needed) adds why Karni
objects and the general fix. System checks (karni.E101, …) run on every
manage.py command, so a missing access or a broken schedule fails on boot,
not on the first request. All codes: errors.md, generated from
the registries.
Rule codes¶
Stable; a code is never reused for a different rule.
| Range | Category |
|---|---|
K0xx |
workflow: how to work in a Karni project |
K1xx |
backend: Python/Django |
K2xx |
frontend: React/TypeScript |
K3xx |
architecture: what to build at all, and where |
P001… |
your project's rules (karni.rules.py) |
Severity error (shown as K101!) means enforced: the rule maps onto an
enforcement id (model-base, viewset-base, viewset-access,
access-override, unpaginated-action, query-scaling,
admin-convention, raw-sql, http-client,
redundant-serializer, duplicate, opt-out-reason,
karni/no-raw-elements, karni/max-page-lines, karni/no-deep-imports,
karni/no-direct-http, jscpd) and fails the build. must is a mandatory
convention that is not machine-checked; should is a strong default.
karni rules --json lists the mapping.
Project rules: karni.rules.py¶
An optional file at the project root. It is parsed, never executed: only literal assignments to these names are allowed.
# karni.rules.py
ADD = [
{"code": "P001", "rule": "Money is always a Decimal, never a float.",
"summary": "Money is Decimal, never float.", "severity": "must"},
]
OVERRIDE = {"K007": {"summary": "Commands: just up, just migrate, karni types/check/context."}}
DISABLE = ["K304"]
ADAPTERS = {"disable": ["copilot-prompts"], "paths": {"windsurf": ".devin/rules/karni.md"}}
ADD entries take code (any 1–3 capitals but a bare K, plus three
digits) and rule, optionally summary, category (default project),
severity (error/must/should, default must), rationale, fix.
OVERRIDE may change summary, rule, rationale, fix, severity,
category of a Karni rule; codes and enforcement mappings are fixed.
DISABLE hides rules from karni rules and from --check; it does not turn
off karni check or ESLint (configure those where they live). ADD += [...]
works too. Unknown names, codes or fields are errors that name the line.
Adapters¶
karni new generates them; karni rules sync refreshes them. The registry
is packages/karni-cli/karni_cli/agent_adapters.toml: a renamed file is a
config change, not a code change, and each entry records the documentation
it was verified against (2026-10-02).
| Tool | File | Format |
|---|---|---|
| AGENTS.md readers (Codex, Jules, Gemini CLI, Zed, Warp, Devin; also Cursor, Copilot, Windsurf, Cline, Junie) | AGENTS.md |
Markdown, no frontmatter. Canonical; carries module guidance |
| Claude Code | CLAUDE.md |
@AGENTS.md import (a CLAUDE.md is read instead of AGENTS.md, so it imports it) |
| Cursor | .cursor/rules/karni.mdc |
frontmatter description, globs, alwaysApply: true |
| Windsurf / Devin Desktop | .windsurf/rules/karni.md |
frontmatter trigger: always_on |
| GitHub Copilot | .github/copilot-instructions.md |
Markdown, no frontmatter |
| Cline | .clinerules/karni.md |
Markdown in the .clinerules/ directory |
| Aider | CONVENTIONS.md |
Markdown; load with aider --read CONVENTIONS.md or read: in .aider.conf.yml |
| JetBrains Junie | .junie/AGENTS.md |
Markdown; carries module guidance |
Slash commands for new-model, new-page, add-module and new-module
come from the same procedure data:
| Tool | Files |
|---|---|
| Claude Code | .claude/commands/<name>.md (description, argument-hint, $ARGUMENTS) |
| Cursor | .cursor/commands/<name>.md (plain Markdown) |
| Windsurf | .windsurf/workflows/<name>.md (description) |
| GitHub Copilot (VS Code) | .github/prompts/<name>.prompt.md (description, argument-hint, agent: agent) |
Every generated file is at most 100 lines and carries a
Generated by `karni rules sync` marker. Sync only overwrites files with
that marker: a hand-written file in an adapter's place is reported and left
alone unless you pass --force, which backs it up under .karni/backup/
first. Generated files that are no longer produced (a disabled adapter, a
moved path) are removed.
Conventions that changed from the names originally planned, each following
the tool's current documentation: Cline reads a .clinerules/ directory
(not a single file), Windsurf's .windsurfrules is legacy (rules live in
.windsurf/rules/, or .devin/rules/), Cursor's .cursorrules is no longer
documented (.cursor/rules/*.mdc), and Junie's .junie/guidelines.md is
legacy and never read while an AGENTS.md exists, so .junie/AGENTS.md is
generated. Cline workflows are not generated: their documentation page could
not be verified.
Module guidance¶
A module ships AGENTS.fragment.md (module contract).
karni add <x> reads it through the project's interpreter, records it as
.karni/modules/<x>.md (commit it) and re-runs sync. From there it appears
in karni rules output ([module:<x>]) and in AGENTS.md and
.junie/AGENTS.md between <!-- karni:module:<x> --> markers. If inline
module guidance would push those files past 100 lines, they list the
modules and point at karni rules instead.
CI staleness check¶
Exits 1 and lists every stale file when an adapter differs from what the
registry, karni.rules.py and .karni/modules/ produce. The scaffold's
make check runs it after karni check.
MCP¶
Tools: context(), rules(code="", procedure=""), check(path). Each
returns the same text as the CLI.
Claude Code:
or commit a project-scoped .mcp.json:
Cursor, .cursor/mcp.json:
The server is built on the official MCP Python SDK (MCPServer in mcp 2.x,
FastMCP in mcp 1.x).
Migrating an existing project¶
Projects scaffolded before this change have hand-written AGENTS.md,
CLAUDE.md, .cursorrules and .claude/commands/*.md. karni upgrade
runs a codemod for them:
- module blocks move to
.karni/modules/<x>.md; - every line you added to Karni's text becomes a
karni.rules.pyADDentry, verbatim (one per bullet or paragraph, codesP001…); - every line you removed from Karni's text is listed as a
# REVIEW:comment inkarni.rules.pyand printed, because only you know whether it meant "disable this rule"; - originals are backed up under
.karni/backup/, then the adapters are generated.
Files that do not look like Karni's are left untouched and reported.
karni upgrade --dry-run shows the diff. This is one entry in Karni's
changelog; see migrating to 0.2.
Plain-language prompts¶
For tools without slash commands, these prompts do the same as /new-model,
/new-page, /add-module and /new-module. The block below is generated
from the procedure data (a test fails when it is stale).
new-model — Add a domain model the Karni way (model + viewset, nothing else)¶
Prompt for tools without slash commands (replace the placeholder):
Run
karni rules --procedure new-modeland follow every step for:<ModelName> [in <app>] [fields...]
What karni rules --procedure returns:
Add a new domain model: <ModelName> [in <app>] [fields...]
1. Run `karni context` to see existing apps, models and viewsets.
2. `karni make model <Name> <field:type ...> --with viewset --access <who> [--search f1,f2] [--app <app>]` writes the model on `karni.models.BaseModel` and its viewset (`-a` also adds list/detail/form pages). Field types: `karni make model --help`. `--access`: authenticated (default), public-read, owned:<fk to user>, staff, role:<name>; edit the generated `access` for anything richer (`Owned(...) | HasRole(...)`, `Where(...)`). Do NOT write an admin, serializer or router: the auto-router registers it; admin, filters, schema are derived.
3. Add `class APIMeta:` / `class AdminMeta:` only for fields that must be hidden, read-only or write-only.
4. `karni make migration && karni migrate && karni types`
5. Verify: `karni context` lists the model and viewset; GET /api/<plural>/ works for a caller `access` admits and is refused (401/403) or empty for one it does not.
Total new code should be well under 20 lines. If it isn't, stop — you are reimplementing something Karni provides.
Rules that apply: K009, K101, K102, K109, K108, K302, K305, K003
new-page — Add a frontend page composing @karni/ui (50 lines or fewer)¶
Prompt for tools without slash commands (replace the placeholder):
Run
karni rules --procedure new-pageand follow every step for:<page purpose> [resource]
What karni rules --procedure returns:
Add a page: <page purpose> [resource]
1. Run `karni context` for available components, hooks, resources and pages.
2. `karni make page <name> --resource <resource> --kind list|detail|form` writes `frontend/src/pages/<name>.tsx` composing @karni/ui and does the rest a person would do by hand: register the route alongside the existing pages (frontend/src/routes.tsx). Without `--resource` it is a bare `<Page>`. Never hand-roll tables, forms or fetching.
3. Custom behaviour goes through props (`columns`, `fields`, `rowActions`, slots, custom cell/field renderers) — extend, never fork a variant.
4. Keep the file at 50 lines or fewer. Data access only via @karni/ui hooks (`useResourceList`, `useResourceItem`, `useResourceMutations`) and the typed hooks generated by `karni types`.
5. `karni rules --check <the page file>` must report no violations.
Rules that apply: K009, K201, K202, K204, K303, K306
add-module — Install an optional Karni module and wire it into settings¶
Prompt for tools without slash commands (replace the placeholder):
Run
karni rules --procedure add-moduleand follow every step for:<module> (e.g. payments)
What karni rules --procedure returns:
Install Karni module: <module> (e.g. payments)
1. Run `karni add <module>` — it adds the `karni-<module>` dependency to pyproject.toml, wires `karni_<module>` into the INSTALLED_APPS block marked `# karni:apps` in config/settings.py, and (once installed) records the module's agent guidance.
2. Rebuild/install: `docker compose build` (or `uv pip install -r pyproject.toml` in a host venv), then `make migrate`; run `karni add <module>` again if it asked you to.
3. Run `karni context` — the module must appear under MODULES; `karni rules` now includes its guidance.
4. Configure only settings the module documents; do not patch its internals.
5. To create a new reusable module instead: `karni make module <name>` (modules/karni-<name>/, docs/modules.md).
Rules that apply: K009, K308
new-module — Create a new local Django app following Karni conventions¶
Prompt for tools without slash commands (replace the placeholder):
Run
karni rules --procedure new-moduleand follow every step for:<app_name>
What karni rules --procedure returns:
Create a local app: <app_name>
1. Run `karni context` first — confirm the domain isn't already covered.
2. `karni make app <app_name>` writes `__init__.py`, `apps.py`, `models.py`, `viewsets.py`, `migrations/`, `tests/` and adds the app to the `# karni:apps` block in config/settings.py. No admin.py, serializers.py, urls.py, forms.py — Karni derives all of that.
3. Add models with `karni make model <Name> <field:type ...> --app <app_name> --with viewset --access <who>` (procedure new-model).
4. `karni make migration && karni migrate && karni types`, then `karni context` to verify.
5. Anything reusable across projects belongs in a Karni module: `karni make module <name>`, not in this project — say so instead of building it.
Rules that apply: K009, K305, K307