Metadata-Version: 2.5
Name: ryni
Version: 0.1.0
Summary: An opinionated linter for agent harnesses
Requires-Python: >=3.14
Requires-Dist: pydantic>=2.13.5
Requires-Dist: pyyaml>=6
Requires-Dist: typer>=0.20
Description-Content-Type: text/markdown

# Rýni

**An opinionated linter for agent harnesses, written in Python.**

[Getting started](#getting-started) · [Usage](#usage) · [Rules](#rules) · [Plugins](#plugins) · [Contributing](#contributing)

Agent instructions and skills are part of your codebase. Rýni helps you keep them
consistent as your project grows, enforcing conventions for how harnesses are
structured, shared, and maintained, with actionable diagnostics when they drift.

- **Validate skills.** Check `SKILL.md` frontmatter for required metadata.
- **Keep instructions in sync.** Check that `CLAUDE.md` links to its sibling `AGENTS.md`.
- **Check skill organization.** Find misplaced skill sources, tracked installations,
  and missing or inconsistent local installations.
- **Choose your rules.** Run the full catalog or select individual checks by ID.
- **Use it in automation.** Get structured JSON output and predictable exit codes.
- **Add your own conventions.** Distribute custom rules as Python packages.

Rýni's built-in checks run locally, without a model or API key, and do not modify
files. The project is in early development; the CLI and plugin API may change.

## Getting started

Rýni requires **Python 3.14 or later**. Git is required for the installed-skills
ignore check (`SKILL002`).

Install from PyPI with uv:

```bash
uv tool install ryni
```

Or with pip:

```bash
pip install ryni
```

Then run Rýni in the project you want to check:

```bash
ryni check .
```

## Usage

Check files, directories, or multiple paths:

```bash
ryni check .
ryni check skills/ AGENTS.md CLAUDE.md
```

List available rules:

```bash
ryni rule
```

Produce JSON for scripts and CI:

```bash
ryni check . --output-format json
```

The JSON report contains `checked_files`, `findings`, and `errors`. Each finding
includes a path, line number, rule ID, and message. For repository-wide checks,
`checked_files` also includes repository roots.

| Exit code | Meaning |
| --- | --- |
| `0` | Checks completed without findings. |
| `1` | Checks completed with findings. |
| `2` | Invalid invocation, execution error, or incomplete checks. |

Reports preserve findings when another check fails. Runs with no applicable targets
explicitly report zero checked targets.

### Configuration and discovery

All registered rules, including installed plugins, run by default. Use `--select`
to choose a comma-separated list of rule IDs. Configuration is currently through
CLI options; Rýni does not read a configuration file.

File rules search the supplied paths recursively and select files by name.
Discovery skips `.git`, `.venv`, `venv`, `node_modules`, `__pycache__`, `.agents`,
`.claude`, `.codex`, and `.github`. You can check those locations by passing them
explicitly. Directory symlinks are not traversed, and repeated paths are deduplicated
without resolving symlinks. File discovery does not apply `.gitignore` patterns.

Repository rules inspect the nearest enclosing Git checkout for each supplied
path, once per repository. Outside a Git checkout, they use the supplied directory
or the file's parent directory. **Selecting a single file does not narrow the scope
of repository rules.** Select only file rules when you want checks limited to those paths.

## Rules

Rýni includes five deterministic rules:

| ID | Name | Checks |
| --- | --- | --- |
| `SKILL001` | `skill-frontmatter` | `SKILL.md` starts with valid YAML frontmatter containing non-empty string `name` and `description` fields. |
| `AGENT001` | `claude-agents-symlink` | Each existing `CLAUDE.md` is a symlink resolving to an existing sibling `AGENTS.md`. |
| `SKILL002` | `installed-skills-gitignored` | Installed skill directories for configured agents are covered by repository `.gitignore` rules and are not tracked. |
| `SKILL003` | `local-skills-source-location` | Locally owned skills live under a directory named `skills/`, with nested categories allowed. |
| `SKILL004` | `local-skills-installed` | Local skills have a matching local-source `skills-lock.json` entry and matching installed `SKILL.md` files for agents configured in their source scope. |

`SKILL001` allows additional frontmatter fields and checks structure only.
`AGENT001` allows `AGENTS.md` without a corresponding `CLAUDE.md`. `SKILL004`
accepts copies and symlinks and checks installation evidence, not command history.

These rules express Rýni’s opinions about maintaining agent harnesses. They do not assess
instruction quality or agent behavior. Semantic and hybrid rule kinds are reserved
for future use; agent execution is not implemented.

## Plugins

Extend Rýni with rules for your team's conventions. A Python package can export a
`ryni.models.Rule` instance through the `ryni.rules` entry-point group.

In the plugin's `pyproject.toml`:

```toml
[project.entry-points."ryni.rules"]
example = "my_rules:EXAMPLE"
```

In `my_rules.py`:

```python
from pathlib import Path

from ryni.models import Finding, Rule, RuleKind


def check_readme(path: Path) -> list[Finding]:
    if not path.read_text(encoding="utf-8").strip():
        return [Finding(str(path), 1, "CUSTOM001", "README must not be empty.")]
    return []


EXAMPLE = Rule(
    id="CUSTOM001",
    name="nonempty-readme",
    kind=RuleKind.DETERMINISTIC,
    description="README.md must contain text.",
    filename="README.md",
    evaluate=check_readme,
)
```

Install the plugin in the same Python environment as Rýni. Both `check` and `rule`
load installed plugins; duplicate rule IDs are rejected. Plugins execute Python
code, so only install packages you trust.

Evaluators receive a path and return a list of findings. They must not print or
modify files. An evaluator exception produces an execution error and exit code `2`,
while other checks continue.

## Contributing

Bug reports, rule proposals, documentation improvements, and pull requests are
welcome. For a bug report, include the command you ran, a minimal example, and the
expected and actual output. For a new rule, describe the convention it checks and
show examples that should pass and fail.

Rýni currently lives in `tools/ryni` in this repository. From the repository root:

```bash
uv run ryni --help
uv run pytest tools/ryni/tests
uv run ruff check tools/ryni
```

The [CLI](src/ryni/cli.py) handles commands and output, the
[engine](src/ryni/engine.py) discovers targets and runs checks, and the
[rule catalog](src/ryni/rules/__init__.py) registers built-in rules and loads plugins.
Add built-in rules under `src/ryni/rules/`, register them in `BUILTINS`, and include
tests under `tests/`.
