Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
dist
coverage
.git
docs-tmp
docs
*.log
.DS_Store
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: yarn
- run: yarn install --frozen-lockfile
- run: yarn lint
- run: yarn typecheck
- run: yarn test
- run: yarn build
- run: yarn smoke
- run: yarn pack:smoke
51 changes: 51 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Publish

# Publishes @casa/agent-rules to npm on every merge to main, but only when the
# version in package.json has not already been published. Bump the version in a
# PR; the merge then triggers the release. Requires the NPM_TOKEN repo secret.

on:
push:
branches: [main]

permissions:
contents: read
id-token: write # npm provenance

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
scope: '@casa'
cache: yarn

- run: yarn install --frozen-lockfile
- run: yarn lint
- run: yarn typecheck
- run: yarn test
- run: yarn build
- run: yarn pack:smoke

- name: Check whether this version is already published
id: check
run: |
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
echo "Already published: $NAME@$VERSION — skipping."
echo "publish=false" >> "$GITHUB_OUTPUT"
else
echo "Will publish: $NAME@$VERSION"
echo "publish=true" >> "$GITHUB_OUTPUT"
fi

- name: Publish to npm
if: steps.check.outputs.publish == 'true'
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
dist
*.log
.DS_Store
docs-tmp
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dist
node_modules
coverage
yarn.lock
docs-tmp
7 changes: 7 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"printWidth": 100,
"singleQuote": true,
"semi": true,
"trailingComma": "all",
"arrowParens": "always"
}
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Changelog

All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.1.0] - 2026-06-26

### Added

- Rule discovery and parsing for `.md`/`.mdc` files with YAML front-matter
(`description`, `globs`, `reviewSkip`); inline and YAML-list glob formats.
- Glob matcher with `*`, `**` (multi-segment), negation, and `*.ext` support.
- Diff utilities: `getDiff` (working-tree incl. untracked, staged, range),
`extractChangedFiles`, `extractDiffSections`, `buildDiffLineMap`.
- `runReview` orchestrator with bounded concurrency, per-rule diff scoping,
Zod-validated findings, dedup / diff-line / priority filtering, and per-rule
failure isolation.
- `agent-rules` CLI: exec-only transport delegating to a local agent
(`claude`/`codex`), resolution order `--exec` → launching agent → PATH → fail,
text/JSON output, exit codes `0`/`1`/`2`.
- `--transport claude|codex` flag to pin the agent when both are installed.
- Recursion guard: refuses to run inside an agent that agent-rules spawned.
- `--list` mode: discover the rules applicable to a diff without calling a model.
- Slash-command templates for Claude Code and Codex (`examples/integrations/`) that
use `--list` so the host agent does the review without spawning a nested agent.
- Docker image (`claude` + `codex` installed) with sample removal rules and a
demo; hermetic smoke and packaged-install (`npm pack`) smoke tests.

[Unreleased]: https://example.com/agent-rules/compare/v0.1.0...HEAD
[0.1.0]: https://example.com/agent-rules/releases/tag/v0.1.0
37 changes: 37 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Reproducible environment for running agent-rules against a real agent CLI.
#
# Both `claude` and `codex` are installed. Credentials are NEVER baked into the
# image — pass them at run time (see docker/README.md):
# docker run --rm -e CLAUDE_CODE_OAUTH_TOKEN agent-rules verify
# docker run --rm -e OPENAI_API_KEY agent-rules verify --transport codex
FROM node:22-slim

RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates bash \
&& rm -rf /var/lib/apt/lists/*

# Pin yarn 1.x via corepack (honours the package.json packageManager field).
RUN corepack enable

# Agent CLIs used by the exec transport.
RUN npm install -g @anthropic-ai/claude-code @openai/codex

WORKDIR /app

# Dependencies first for better layer caching.
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

# Build the package.
COPY tsconfig.json tsconfig.build.json ./
COPY src ./src
RUN yarn build

# Scripts, sample rules, and entrypoint.
COPY scripts ./scripts
COPY examples ./examples
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh scripts/*.sh

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["smoke"]
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Agent Rules contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
170 changes: 169 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,171 @@
<p align="center">
<img src="./assets/banner.svg" alt="agent-rules — Markdown rules, applied to diffs by an agent" width="100%">
</p>

# Agent Rules

A framework for agent centric rules for codebases
Apply Markdown-defined coding rules to a diff using an LLM.

Rules are plain Markdown files with YAML front-matter. Each rule declares the file
globs it cares about; at review time the tool discovers the rules that apply to a
diff, asks a model to check each one against its scoped changes, and returns
findings anchored to specific lines.

The package is **LLM-agnostic** (you supply an adapter, or the CLI delegates to a
local agent like `claude`/`codex`) and **platform-agnostic** (it returns findings;
you decide how to surface them). It ships as a library (`runReview`, `getDiff`, and
the building blocks) and a CLI (`agent-rules`).

## Why not just a linter?

A linter only ever sees the code as it exists now, so it's the right tool for
pattern-matchable facts about the current tree (unused vars, formatting, `console.log`).

agent-rules reviews the **diff** — including removed lines — and reasons about the
_change_. That makes it suited to things a linter structurally can't catch:

- **Removals** — a deleted authorization check or `try`/`catch` is invisible in the
final code; only the diff shows it. (See `examples/rules/`.)
- **Intent and contracts** — breaking a shared type, dropping a test, semantic review.

Write rules for the change; keep your linter for the snapshot.

## Install

```sh
yarn add @casa/agent-rules
```

Requires Node ≥22. Pure ESM.

## Rule files

Put rules under a directory (e.g. `.agent/rules/`), one per file (`.md` or `.mdc`):

```markdown
---
description: No console.log
globs:
- 'src/**/*.ts'
- '!src/**/*.test.ts'
---

Use the project logger instead of `console.log` in non-test source files.
```

| Field | Description |
| ------------- | ----------------------------------------------------------------------------- |
| `description` | Display name (falls back to the filename) |
| `globs` | Inline list or YAML list; `!` negates. A rule with no globs is never applied. |
| `reviewSkip` | If `true`, the rule is parsed but excluded from review |

## CLI

The CLI delegates to a local agent CLI for model access — no API key needed. It
resolves a transport in order: `--exec` → `--transport <claude|codex>` →
the launching agent (e.g. `$CLAUDE_CODE_EXECPATH`) → `claude`/`codex` on `PATH`.
If none is found it fails (exit 2).

```sh
# Review uncommitted changes against the default rules dir (.agent/rules)
agent-rules --working-tree

# Review a range, emit JSON
agent-rules --diff origin/main...HEAD --output json

# Pin a specific installed agent
agent-rules --working-tree --transport codex

# Force an explicit transport command (any stdin->stdout program)
agent-rules --working-tree --exec "claude -p --output-format json"
```

Diff sources (exactly one): `--working-tree`, `--staged`, `--diff <range>`.
Run `agent-rules --help` for all options. Exit codes: `0` clean, `1` blocking
findings, `2` error.

## Library

```ts
import { runReview, getDiff, type LLMAdapter } from '@casa/agent-rules';

const llm: LLMAdapter = {
async run(prompt) {
// call any model and return its text response
return await myModel(prompt);
},
};

const diff = await getDiff({ type: 'range', range: 'origin/main...HEAD' });

const result = await runReview({
rulesDir: '.agent/rules',
diff,
llm,
minSuggestionImpact: 7, // default
concurrency: 3, // default
});

for (const f of result.findings) {
console.log(`${f.path}:${f.line} [${f.severity}] ${f.body}`);
}
```

`runReview` owns no timeout/retry policy — that belongs to your `LLMAdapter`. A
rejected `run` drops that one rule into `result.skipped` instead of aborting the run.

## Agent integration (slash command)

Add a `/agent-rules` slash command to Claude Code or Codex. The command runs
`agent-rules --list` to fetch the rules that apply to your current changes, then
the agent you're already talking to reviews the diff against them. `--list` only
**discovers** rules — it doesn't call a model — so there's no nested agent, no
extra cost, and no separate auth.

Templates live in [`examples/integrations/`](./examples/integrations/).

**Claude Code** — copy the command into your project (or `~/.claude/commands/` for all projects):

```sh
mkdir -p .claude/commands
cp node_modules/@casa/agent-rules/examples/integrations/claude-code/agent-rules.md .claude/commands/
# (or copy from this repo if you're not installing the package)
```

**Codex** — copy the prompt into your Codex prompts directory:

```sh
mkdir -p ~/.codex/prompts
cp node_modules/@casa/agent-rules/examples/integrations/codex/agent-rules.md ~/.codex/prompts/
```

Then run `/agent-rules` in a session. It reviews your working-tree changes against
the rules in `.agent/rules`. Edit the copied command to change the rules directory
or the diff source (e.g. `--staged`).

## Transport notes (CLI)

The CLI delegates to a local agent in headless mode, so the agent must be usable
non-interactively:

- **claude** must be logged in (`claude /login`). A spawned `claude -p` that isn't
authenticated surfaces as a per-rule error in `skipped`.
- **codex** is invoked with `--skip-git-repo-check` and a read-only sandbox, and
authenticates the same way as your interactive `codex`.

If neither resolves (and no `--exec` is given), the CLI exits 2 with guidance.

## Development

```sh
yarn install
yarn build # compile to dist/ (pure ESM + .d.ts)
yarn typecheck
yarn test # 36 unit tests (hermetic)
yarn smoke # end-to-end CLI test via a fake transport (hermetic, CI-safe)
yarn verify:transport # live check against a real claude/codex (manual, makes a model call)
```

## License

MIT
Loading
Loading