agent-rules-lint 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_rules_lint-0.1.1/.gitattributes +1 -0
- agent_rules_lint-0.1.1/.github/ISSUE_TEMPLATE/rule_request.yml +27 -0
- agent_rules_lint-0.1.1/.github/PULL_REQUEST_TEMPLATE.md +11 -0
- agent_rules_lint-0.1.1/.github/workflows/ci.yml +45 -0
- agent_rules_lint-0.1.1/.github/workflows/release.yml +40 -0
- agent_rules_lint-0.1.1/.gitignore +12 -0
- agent_rules_lint-0.1.1/AGENTS.md +31 -0
- agent_rules_lint-0.1.1/CHANGELOG.md +23 -0
- agent_rules_lint-0.1.1/CONTRIBUTING.md +34 -0
- agent_rules_lint-0.1.1/LICENSE +21 -0
- agent_rules_lint-0.1.1/PKG-INFO +194 -0
- agent_rules_lint-0.1.1/README.md +167 -0
- agent_rules_lint-0.1.1/ROADMAP.md +29 -0
- agent_rules_lint-0.1.1/agent_rules_lint/__init__.py +3 -0
- agent_rules_lint-0.1.1/agent_rules_lint/__main__.py +5 -0
- agent_rules_lint-0.1.1/agent_rules_lint/cli.py +72 -0
- agent_rules_lint-0.1.1/agent_rules_lint/linter.py +284 -0
- agent_rules_lint-0.1.1/pyproject.toml +39 -0
- agent_rules_lint-0.1.1/tests/test_cli.py +38 -0
- agent_rules_lint-0.1.1/tests/test_linter.py +53 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Rule request
|
|
2
|
+
description: Suggest a new lint rule
|
|
3
|
+
title: "[Rule] "
|
|
4
|
+
labels:
|
|
5
|
+
- rule-request
|
|
6
|
+
body:
|
|
7
|
+
- type: textarea
|
|
8
|
+
id: problem
|
|
9
|
+
attributes:
|
|
10
|
+
label: Problem
|
|
11
|
+
description: What agent instruction problem should this catch?
|
|
12
|
+
validations:
|
|
13
|
+
required: true
|
|
14
|
+
- type: textarea
|
|
15
|
+
id: example
|
|
16
|
+
attributes:
|
|
17
|
+
label: Example
|
|
18
|
+
description: Provide a short instruction snippet that should trigger the rule.
|
|
19
|
+
validations:
|
|
20
|
+
required: true
|
|
21
|
+
- type: textarea
|
|
22
|
+
id: expected
|
|
23
|
+
attributes:
|
|
24
|
+
label: Expected finding
|
|
25
|
+
description: What should the tool report?
|
|
26
|
+
validations:
|
|
27
|
+
required: true
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
18
|
+
python-version: ["3.10", "3.14"]
|
|
19
|
+
runs-on: ${{ matrix.os }}
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7.0.0
|
|
22
|
+
- uses: actions/setup-python@v6.3.0
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
- name: Install package
|
|
26
|
+
run: python -m pip install -e .
|
|
27
|
+
- name: Run tests
|
|
28
|
+
run: python -m unittest discover -s tests -v
|
|
29
|
+
- name: Compile source
|
|
30
|
+
run: python -m compileall -q agent_rules_lint tests
|
|
31
|
+
- name: Run linter on itself
|
|
32
|
+
run: python -m agent_rules_lint .
|
|
33
|
+
|
|
34
|
+
package:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v7.0.0
|
|
38
|
+
- uses: actions/setup-python@v6.3.0
|
|
39
|
+
with:
|
|
40
|
+
python-version: "3.12"
|
|
41
|
+
- run: python -m pip install build twine
|
|
42
|
+
- run: python -m build
|
|
43
|
+
- run: python -m twine check dist/*
|
|
44
|
+
- run: python -m pip install dist/*.whl
|
|
45
|
+
- run: agent-rules-lint --version
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v7.0.0
|
|
15
|
+
- uses: actions/setup-python@v6.3.0
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- run: python -m pip install build
|
|
19
|
+
- run: python -m build
|
|
20
|
+
- uses: actions/upload-artifact@v7.0.1
|
|
21
|
+
with:
|
|
22
|
+
name: python-package
|
|
23
|
+
path: dist/
|
|
24
|
+
if-no-files-found: error
|
|
25
|
+
|
|
26
|
+
publish:
|
|
27
|
+
needs: build
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
environment:
|
|
30
|
+
name: pypi
|
|
31
|
+
url: https://pypi.org/p/agent-rules-lint
|
|
32
|
+
permissions:
|
|
33
|
+
id-token: write
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/download-artifact@v8.0.0
|
|
36
|
+
with:
|
|
37
|
+
name: python-package
|
|
38
|
+
path: dist/
|
|
39
|
+
- name: Publish distributions to PyPI
|
|
40
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Agent Instructions
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Maintain `agent-rules-lint`, a dependency-free Python CLI for linting AI agent instruction files.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
These instructions apply to code, tests, documentation, and GitHub workflow files in this repository.
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
Run the standard checks before publishing:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python -m unittest discover -s tests
|
|
17
|
+
python -m agent_rules_lint .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Safety
|
|
21
|
+
|
|
22
|
+
- Do not add network calls to the default lint path.
|
|
23
|
+
- Do not add model calls to the default lint path.
|
|
24
|
+
- Do not commit API keys, access tokens, cookies, private credentials, or generated build output.
|
|
25
|
+
- Keep checks deterministic so they are safe for CI.
|
|
26
|
+
|
|
27
|
+
## Style
|
|
28
|
+
|
|
29
|
+
- Prefer standard-library Python.
|
|
30
|
+
- Keep rule messages short and actionable.
|
|
31
|
+
- Add tests when changing lint behavior.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `agent-rules-lint` are documented here.
|
|
4
|
+
|
|
5
|
+
## [0.1.1] - 2026-07-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- PyPI installation instructions for `uvx`, `pipx`, and `pip`.
|
|
10
|
+
- Credential-free PyPI publishing with GitHub Trusted Publishing.
|
|
11
|
+
- Cross-platform CI for Python 3.10 and 3.14.
|
|
12
|
+
- Package build, metadata, wheel installation, and CLI smoke checks.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Project metadata links now point to the current `Uky0Yang` repository.
|
|
17
|
+
|
|
18
|
+
## [0.1.0] - 2026-07-01
|
|
19
|
+
|
|
20
|
+
- Initial public release.
|
|
21
|
+
|
|
22
|
+
[0.1.1]: https://github.com/Uky0Yang/agent-rules-lint/compare/v0.1.0...v0.1.1
|
|
23
|
+
[0.1.0]: https://github.com/Uky0Yang/agent-rules-lint/releases/tag/v0.1.0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for helping improve `agent-rules-lint`.
|
|
4
|
+
|
|
5
|
+
## Good Contributions
|
|
6
|
+
|
|
7
|
+
- New checks for common agent instruction mistakes
|
|
8
|
+
- Better false-positive handling
|
|
9
|
+
- Tests for real-world `AGENTS.md`, `CLAUDE.md`, Cursor, or Copilot instruction patterns
|
|
10
|
+
- Documentation examples
|
|
11
|
+
- CI integration examples
|
|
12
|
+
|
|
13
|
+
## Before Opening a PR
|
|
14
|
+
|
|
15
|
+
Run:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
python -m unittest discover -s tests -v
|
|
19
|
+
python -m compileall -q agent_rules_lint tests
|
|
20
|
+
python -m agent_rules_lint .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If your change adds or changes a lint rule, include tests.
|
|
24
|
+
|
|
25
|
+
## Rule Design
|
|
26
|
+
|
|
27
|
+
Rules should be:
|
|
28
|
+
|
|
29
|
+
- Easy to explain
|
|
30
|
+
- Low noise
|
|
31
|
+
- Useful without sending data to a model
|
|
32
|
+
- Safe for public and private repositories
|
|
33
|
+
|
|
34
|
+
Avoid checks that require personal taste or project-specific assumptions.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ukyo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-rules-lint
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Lint AI agent instruction files such as AGENTS.md, CLAUDE.md, Cursor rules, and Copilot instructions.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Uky0Yang/agent-rules-lint
|
|
6
|
+
Project-URL: Repository, https://github.com/Uky0Yang/agent-rules-lint
|
|
7
|
+
Project-URL: Issues, https://github.com/Uky0Yang/agent-rules-lint/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Uky0Yang/agent-rules-lint/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Ukyo
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai-agents,claude-code,copilot,cursor,developer-tools,lint,llm
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# agent-rules-lint
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/agent-rules-lint/)
|
|
31
|
+
[](https://pypi.org/project/agent-rules-lint/)
|
|
32
|
+
[](https://github.com/Uky0Yang/agent-rules-lint/actions/workflows/ci.yml)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
|
|
35
|
+
Lint AI agent instruction files before they confuse your coding agent.
|
|
36
|
+
|
|
37
|
+
`agent-rules-lint` is a small, dependency-free Python CLI that scans files such as `AGENTS.md`, `CLAUDE.md`, Cursor rules, and GitHub Copilot instructions for common quality and safety problems.
|
|
38
|
+
|
|
39
|
+
## Why
|
|
40
|
+
|
|
41
|
+
Recent high-growth developer repos show a clear pattern: teams are adding more agent skills, rule files, memory files, and coding-agent playbooks. The missing piece is a quick quality gate for those instructions.
|
|
42
|
+
|
|
43
|
+
Bad agent instructions are expensive. They can be too long, vague, contradictory, unsafe, or accidentally include secrets. This tool gives you a fast local and CI check before those rules affect real work.
|
|
44
|
+
|
|
45
|
+
## What It Checks
|
|
46
|
+
|
|
47
|
+
- Common instruction files:
|
|
48
|
+
- `AGENTS.md`
|
|
49
|
+
- `CLAUDE.md`
|
|
50
|
+
- `GEMINI.md`
|
|
51
|
+
- `.cursorrules`
|
|
52
|
+
- `.cursor/rules/*.md`
|
|
53
|
+
- `.github/copilot-instructions.md`
|
|
54
|
+
- `.github/instructions/*.md`
|
|
55
|
+
- Secret-like values
|
|
56
|
+
- Risky shell and Git commands
|
|
57
|
+
- Prompt-injection-like language
|
|
58
|
+
- Missing title
|
|
59
|
+
- Missing purpose, scope, command, or safety guidance
|
|
60
|
+
- Vague language
|
|
61
|
+
- Files that are too long for practical agent use
|
|
62
|
+
- A small set of obvious contradictory rules
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
Run it without installing:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uvx agent-rules-lint .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Or install it as an isolated CLI:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pipx install agent-rules-lint
|
|
76
|
+
agent-rules-lint .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Standard pip installation also works:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
python -m pip install agent-rules-lint
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
For local development:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python -m pip install -e .
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
Scan the current repository:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
agent-rules-lint .
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Return JSON for CI or custom reports:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
agent-rules-lint . --format json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Fail CI on warnings:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
agent-rules-lint . --warnings-as-errors
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Change the long-file threshold:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
agent-rules-lint . --max-chars 8000
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Example Output
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
agent-rules-lint checked 1 file(s)
|
|
121
|
+
errors=0 warnings=1 info=2
|
|
122
|
+
|
|
123
|
+
Files:
|
|
124
|
+
- AGENTS.md
|
|
125
|
+
|
|
126
|
+
Findings:
|
|
127
|
+
[warning] AGENTS.md:42 risky-command: Risky command should include explicit safeguards or approval rules.
|
|
128
|
+
[info] AGENTS.md missing-safety: Consider adding a clear safety section.
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## CI
|
|
132
|
+
|
|
133
|
+
Add this to GitHub Actions:
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
name: Lint agent rules
|
|
137
|
+
|
|
138
|
+
on:
|
|
139
|
+
pull_request:
|
|
140
|
+
push:
|
|
141
|
+
branches:
|
|
142
|
+
- main
|
|
143
|
+
|
|
144
|
+
jobs:
|
|
145
|
+
lint-agent-rules:
|
|
146
|
+
runs-on: ubuntu-latest
|
|
147
|
+
steps:
|
|
148
|
+
- uses: actions/checkout@v7.0.0
|
|
149
|
+
- uses: actions/setup-python@v6.3.0
|
|
150
|
+
with:
|
|
151
|
+
python-version: "3.12"
|
|
152
|
+
- run: pipx run agent-rules-lint . --warnings-as-errors
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Design Principles
|
|
156
|
+
|
|
157
|
+
- No model calls
|
|
158
|
+
- No network calls
|
|
159
|
+
- No dependencies
|
|
160
|
+
- Safe to run in CI
|
|
161
|
+
- Findings should be specific enough to act on
|
|
162
|
+
- Warnings should improve instruction quality, not enforce one writing style
|
|
163
|
+
|
|
164
|
+
## Current Limitations
|
|
165
|
+
|
|
166
|
+
- Conflict detection is intentionally conservative and only catches obvious pairs.
|
|
167
|
+
- It does not estimate real tokenizer counts; it uses character length as a practical proxy.
|
|
168
|
+
- It does not validate whether an instruction is correct for a specific tool.
|
|
169
|
+
|
|
170
|
+
## Development
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
python -m pip install -e .
|
|
174
|
+
python -m unittest discover -s tests -v
|
|
175
|
+
python -m agent_rules_lint .
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Agent OSS Toolkit
|
|
179
|
+
|
|
180
|
+
This project is part of a small toolkit for building and launching agent-ready open-source repositories:
|
|
181
|
+
|
|
182
|
+
- [agent-repo-kit](https://github.com/Uky0Yang/agent-repo-kit): scaffold launch-ready, AI-agent-friendly repositories
|
|
183
|
+
- [oss-launch-check](https://github.com/Uky0Yang/oss-launch-check): audit whether a repository is ready to launch as open source
|
|
184
|
+
- [repo-context-card](https://github.com/Uky0Yang/repo-context-card): generate compact repository context cards for coding agents
|
|
185
|
+
- [agent-rules-lint](https://github.com/Uky0Yang/agent-rules-lint): lint AGENTS.md, CLAUDE.md, Cursor rules, and Copilot instructions
|
|
186
|
+
- [awesome-ai-agents-zh](https://github.com/Uky0Yang/awesome-ai-agents-zh): Chinese AI Agents / MCP / AI DevTools directory
|
|
187
|
+
|
|
188
|
+
## Contributing
|
|
189
|
+
|
|
190
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# agent-rules-lint
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/agent-rules-lint/)
|
|
4
|
+
[](https://pypi.org/project/agent-rules-lint/)
|
|
5
|
+
[](https://github.com/Uky0Yang/agent-rules-lint/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Lint AI agent instruction files before they confuse your coding agent.
|
|
9
|
+
|
|
10
|
+
`agent-rules-lint` is a small, dependency-free Python CLI that scans files such as `AGENTS.md`, `CLAUDE.md`, Cursor rules, and GitHub Copilot instructions for common quality and safety problems.
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
Recent high-growth developer repos show a clear pattern: teams are adding more agent skills, rule files, memory files, and coding-agent playbooks. The missing piece is a quick quality gate for those instructions.
|
|
15
|
+
|
|
16
|
+
Bad agent instructions are expensive. They can be too long, vague, contradictory, unsafe, or accidentally include secrets. This tool gives you a fast local and CI check before those rules affect real work.
|
|
17
|
+
|
|
18
|
+
## What It Checks
|
|
19
|
+
|
|
20
|
+
- Common instruction files:
|
|
21
|
+
- `AGENTS.md`
|
|
22
|
+
- `CLAUDE.md`
|
|
23
|
+
- `GEMINI.md`
|
|
24
|
+
- `.cursorrules`
|
|
25
|
+
- `.cursor/rules/*.md`
|
|
26
|
+
- `.github/copilot-instructions.md`
|
|
27
|
+
- `.github/instructions/*.md`
|
|
28
|
+
- Secret-like values
|
|
29
|
+
- Risky shell and Git commands
|
|
30
|
+
- Prompt-injection-like language
|
|
31
|
+
- Missing title
|
|
32
|
+
- Missing purpose, scope, command, or safety guidance
|
|
33
|
+
- Vague language
|
|
34
|
+
- Files that are too long for practical agent use
|
|
35
|
+
- A small set of obvious contradictory rules
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
Run it without installing:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uvx agent-rules-lint .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or install it as an isolated CLI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pipx install agent-rules-lint
|
|
49
|
+
agent-rules-lint .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Standard pip installation also works:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python -m pip install agent-rules-lint
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For local development:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python -m pip install -e .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
Scan the current repository:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
agent-rules-lint .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Return JSON for CI or custom reports:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
agent-rules-lint . --format json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Fail CI on warnings:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
agent-rules-lint . --warnings-as-errors
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Change the long-file threshold:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
agent-rules-lint . --max-chars 8000
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Example Output
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
agent-rules-lint checked 1 file(s)
|
|
94
|
+
errors=0 warnings=1 info=2
|
|
95
|
+
|
|
96
|
+
Files:
|
|
97
|
+
- AGENTS.md
|
|
98
|
+
|
|
99
|
+
Findings:
|
|
100
|
+
[warning] AGENTS.md:42 risky-command: Risky command should include explicit safeguards or approval rules.
|
|
101
|
+
[info] AGENTS.md missing-safety: Consider adding a clear safety section.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## CI
|
|
105
|
+
|
|
106
|
+
Add this to GitHub Actions:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
name: Lint agent rules
|
|
110
|
+
|
|
111
|
+
on:
|
|
112
|
+
pull_request:
|
|
113
|
+
push:
|
|
114
|
+
branches:
|
|
115
|
+
- main
|
|
116
|
+
|
|
117
|
+
jobs:
|
|
118
|
+
lint-agent-rules:
|
|
119
|
+
runs-on: ubuntu-latest
|
|
120
|
+
steps:
|
|
121
|
+
- uses: actions/checkout@v7.0.0
|
|
122
|
+
- uses: actions/setup-python@v6.3.0
|
|
123
|
+
with:
|
|
124
|
+
python-version: "3.12"
|
|
125
|
+
- run: pipx run agent-rules-lint . --warnings-as-errors
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Design Principles
|
|
129
|
+
|
|
130
|
+
- No model calls
|
|
131
|
+
- No network calls
|
|
132
|
+
- No dependencies
|
|
133
|
+
- Safe to run in CI
|
|
134
|
+
- Findings should be specific enough to act on
|
|
135
|
+
- Warnings should improve instruction quality, not enforce one writing style
|
|
136
|
+
|
|
137
|
+
## Current Limitations
|
|
138
|
+
|
|
139
|
+
- Conflict detection is intentionally conservative and only catches obvious pairs.
|
|
140
|
+
- It does not estimate real tokenizer counts; it uses character length as a practical proxy.
|
|
141
|
+
- It does not validate whether an instruction is correct for a specific tool.
|
|
142
|
+
|
|
143
|
+
## Development
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
python -m pip install -e .
|
|
147
|
+
python -m unittest discover -s tests -v
|
|
148
|
+
python -m agent_rules_lint .
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Agent OSS Toolkit
|
|
152
|
+
|
|
153
|
+
This project is part of a small toolkit for building and launching agent-ready open-source repositories:
|
|
154
|
+
|
|
155
|
+
- [agent-repo-kit](https://github.com/Uky0Yang/agent-repo-kit): scaffold launch-ready, AI-agent-friendly repositories
|
|
156
|
+
- [oss-launch-check](https://github.com/Uky0Yang/oss-launch-check): audit whether a repository is ready to launch as open source
|
|
157
|
+
- [repo-context-card](https://github.com/Uky0Yang/repo-context-card): generate compact repository context cards for coding agents
|
|
158
|
+
- [agent-rules-lint](https://github.com/Uky0Yang/agent-rules-lint): lint AGENTS.md, CLAUDE.md, Cursor rules, and Copilot instructions
|
|
159
|
+
- [awesome-ai-agents-zh](https://github.com/Uky0Yang/awesome-ai-agents-zh): Chinese AI Agents / MCP / AI DevTools directory
|
|
160
|
+
|
|
161
|
+
## Contributing
|
|
162
|
+
|
|
163
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
## Near Term
|
|
4
|
+
|
|
5
|
+
- Add more real-world examples
|
|
6
|
+
- Add markdown output
|
|
7
|
+
- Add line excerpts in JSON output
|
|
8
|
+
- Support config in `pyproject.toml`
|
|
9
|
+
- Keep PyPI releases automated through Trusted Publishing
|
|
10
|
+
|
|
11
|
+
## Mid Term
|
|
12
|
+
|
|
13
|
+
- Add rule IDs with documentation pages
|
|
14
|
+
- Add SARIF output for GitHub code scanning
|
|
15
|
+
- Add pre-commit hook documentation
|
|
16
|
+
- Add optional token estimation adapters
|
|
17
|
+
- Add fixture corpus from public agent instruction files
|
|
18
|
+
|
|
19
|
+
## Long Term
|
|
20
|
+
|
|
21
|
+
- Build a benchmark of high-quality agent instruction files
|
|
22
|
+
- Add project-specific profiles for Codex, Claude Code, Cursor, Copilot, and Gemini CLI
|
|
23
|
+
- Provide a GitHub Action wrapper
|
|
24
|
+
|
|
25
|
+
## Non-Goals
|
|
26
|
+
|
|
27
|
+
- No model-based scoring by default
|
|
28
|
+
- No network calls by default
|
|
29
|
+
- No automatic rewriting of user instructions without explicit user action
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .linter import LintResult, lint_path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="agent-rules-lint",
|
|
14
|
+
description="Lint AI agent instruction files such as AGENTS.md, CLAUDE.md, Cursor rules, and Copilot instructions.",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("path", nargs="?", default=".", help="Repository or directory to scan.")
|
|
17
|
+
parser.add_argument("--format", choices=("text", "json"), default="text", help="Output format.")
|
|
18
|
+
parser.add_argument("--max-chars", type=int, default=12000, help="Warn when a file exceeds this many characters.")
|
|
19
|
+
parser.add_argument("--warnings-as-errors", action="store_true", help="Exit non-zero when warnings are found.")
|
|
20
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
21
|
+
return parser
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def render_text(result: LintResult) -> str:
|
|
25
|
+
lines = [
|
|
26
|
+
f"agent-rules-lint checked {len(result.files_checked)} file(s)",
|
|
27
|
+
f"errors={result.error_count} warnings={result.warning_count} info={result.info_count}",
|
|
28
|
+
]
|
|
29
|
+
if result.files_checked:
|
|
30
|
+
lines.append("")
|
|
31
|
+
lines.append("Files:")
|
|
32
|
+
for file in result.files_checked:
|
|
33
|
+
lines.append(f" - {file}")
|
|
34
|
+
|
|
35
|
+
if result.findings:
|
|
36
|
+
lines.append("")
|
|
37
|
+
lines.append("Findings:")
|
|
38
|
+
for finding in result.findings:
|
|
39
|
+
location = finding.file
|
|
40
|
+
if finding.line is not None:
|
|
41
|
+
location = f"{location}:{finding.line}"
|
|
42
|
+
lines.append(f" [{finding.severity}] {location} {finding.rule}: {finding.message}")
|
|
43
|
+
else:
|
|
44
|
+
lines.append("")
|
|
45
|
+
lines.append("No findings.")
|
|
46
|
+
|
|
47
|
+
return "\n".join(lines)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def exit_code(result: LintResult, warnings_as_errors: bool) -> int:
|
|
51
|
+
if result.error_count:
|
|
52
|
+
return 1
|
|
53
|
+
if warnings_as_errors and result.warning_count:
|
|
54
|
+
return 1
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main(argv: list[str] | None = None) -> int:
|
|
59
|
+
parser = build_parser()
|
|
60
|
+
args = parser.parse_args(argv)
|
|
61
|
+
result = lint_path(Path(args.path), max_chars=args.max_chars)
|
|
62
|
+
|
|
63
|
+
if args.format == "json":
|
|
64
|
+
print(result.to_json())
|
|
65
|
+
else:
|
|
66
|
+
print(render_text(result))
|
|
67
|
+
|
|
68
|
+
return exit_code(result, warnings_as_errors=args.warnings_as_errors)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import fnmatch
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from typing import Iterable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULT_PATTERNS = (
|
|
12
|
+
"AGENTS.md",
|
|
13
|
+
"CLAUDE.md",
|
|
14
|
+
"GEMINI.md",
|
|
15
|
+
".cursorrules",
|
|
16
|
+
".cursor/rules/*.md",
|
|
17
|
+
".github/copilot-instructions.md",
|
|
18
|
+
".github/instructions/*.md",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
DEFAULT_IGNORE_DIRS = {
|
|
22
|
+
".git",
|
|
23
|
+
".hg",
|
|
24
|
+
".svn",
|
|
25
|
+
".venv",
|
|
26
|
+
"venv",
|
|
27
|
+
"node_modules",
|
|
28
|
+
"__pycache__",
|
|
29
|
+
"dist",
|
|
30
|
+
"build",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
SECRET_PATTERNS = (
|
|
34
|
+
re.compile(r"\bghp_[A-Za-z0-9_]{20,}\b"),
|
|
35
|
+
re.compile(r"\bgho_[A-Za-z0-9_]{20,}\b"),
|
|
36
|
+
re.compile(r"\bgithub_pat_[A-Za-z0-9_]{20,}\b"),
|
|
37
|
+
re.compile(r"\bsk-[A-Za-z0-9_-]{20,}\b"),
|
|
38
|
+
re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
|
|
39
|
+
re.compile(r"-----BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----"),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
RISKY_COMMAND_PATTERNS = (
|
|
43
|
+
re.compile(r"\brm\s+-rf\s+[/~$*]", re.IGNORECASE),
|
|
44
|
+
re.compile(r"\bgit\s+reset\s+--hard\b", re.IGNORECASE),
|
|
45
|
+
re.compile(r"\bgit\s+clean\s+-fdx\b", re.IGNORECASE),
|
|
46
|
+
re.compile(r"\bRemove-Item\b.+\b-Recurse\b.+\b-Force\b", re.IGNORECASE),
|
|
47
|
+
re.compile(r"\bcurl\b.+\|\s*(bash|sh)\b", re.IGNORECASE),
|
|
48
|
+
re.compile(r"\bwget\b.+\|\s*(bash|sh)\b", re.IGNORECASE),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
PROMPT_INJECTION_PATTERNS = (
|
|
52
|
+
re.compile(r"ignore (all )?(previous|prior|above) instructions", re.IGNORECASE),
|
|
53
|
+
re.compile(r"reveal (the )?(system|developer) (prompt|message)", re.IGNORECASE),
|
|
54
|
+
re.compile(r"bypass (safety|policy|guardrails)", re.IGNORECASE),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
SECTION_HINTS = {
|
|
58
|
+
"purpose": ("purpose", "goal", "objective", "what this does", "overview", "目标", "目的"),
|
|
59
|
+
"scope": ("scope", "when to use", "applies to", "boundaries", "范围", "适用"),
|
|
60
|
+
"commands": ("commands", "scripts", "verification", "tests", "test", "build", "命令", "验证", "测试"),
|
|
61
|
+
"safety": ("safety", "security", "secrets", "do not", "never", "安全", "密钥", "不要"),
|
|
62
|
+
"style": ("style", "formatting", "conventions", "tone", "风格", "格式", "约定"),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Finding:
|
|
68
|
+
file: str
|
|
69
|
+
severity: str
|
|
70
|
+
rule: str
|
|
71
|
+
message: str
|
|
72
|
+
line: int | None = None
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> dict:
|
|
75
|
+
data = {
|
|
76
|
+
"file": self.file,
|
|
77
|
+
"severity": self.severity,
|
|
78
|
+
"rule": self.rule,
|
|
79
|
+
"message": self.message,
|
|
80
|
+
}
|
|
81
|
+
if self.line is not None:
|
|
82
|
+
data["line"] = self.line
|
|
83
|
+
return data
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class LintResult:
|
|
88
|
+
root: str
|
|
89
|
+
files_checked: list[str] = field(default_factory=list)
|
|
90
|
+
findings: list[Finding] = field(default_factory=list)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def error_count(self) -> int:
|
|
94
|
+
return sum(1 for finding in self.findings if finding.severity == "error")
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def warning_count(self) -> int:
|
|
98
|
+
return sum(1 for finding in self.findings if finding.severity == "warning")
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def info_count(self) -> int:
|
|
102
|
+
return sum(1 for finding in self.findings if finding.severity == "info")
|
|
103
|
+
|
|
104
|
+
def to_dict(self) -> dict:
|
|
105
|
+
return {
|
|
106
|
+
"root": self.root,
|
|
107
|
+
"files_checked": self.files_checked,
|
|
108
|
+
"summary": {
|
|
109
|
+
"errors": self.error_count,
|
|
110
|
+
"warnings": self.warning_count,
|
|
111
|
+
"info": self.info_count,
|
|
112
|
+
},
|
|
113
|
+
"findings": [finding.to_dict() for finding in self.findings],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def to_json(self) -> str:
|
|
117
|
+
return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def discover_instruction_files(root: Path, patterns: Iterable[str] = DEFAULT_PATTERNS) -> list[Path]:
|
|
121
|
+
root = root.resolve()
|
|
122
|
+
matches: list[Path] = []
|
|
123
|
+
normalized_patterns = tuple(pattern.replace("\\", "/") for pattern in patterns)
|
|
124
|
+
|
|
125
|
+
for path in root.rglob("*"):
|
|
126
|
+
if not path.is_file():
|
|
127
|
+
continue
|
|
128
|
+
if any(part in DEFAULT_IGNORE_DIRS for part in path.relative_to(root).parts):
|
|
129
|
+
continue
|
|
130
|
+
relative = path.relative_to(root).as_posix()
|
|
131
|
+
if any(fnmatch.fnmatch(relative, pattern) for pattern in normalized_patterns):
|
|
132
|
+
matches.append(path)
|
|
133
|
+
|
|
134
|
+
return sorted(matches, key=lambda item: item.relative_to(root).as_posix())
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def lint_path(root: Path, max_chars: int = 12000) -> LintResult:
|
|
138
|
+
root = root.resolve()
|
|
139
|
+
result = LintResult(root=str(root))
|
|
140
|
+
files = discover_instruction_files(root)
|
|
141
|
+
|
|
142
|
+
if not files:
|
|
143
|
+
result.findings.append(
|
|
144
|
+
Finding(
|
|
145
|
+
file=".",
|
|
146
|
+
severity="warning",
|
|
147
|
+
rule="no-instruction-files",
|
|
148
|
+
message="No known agent instruction files found.",
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
for path in files:
|
|
154
|
+
relative = path.relative_to(root).as_posix()
|
|
155
|
+
result.files_checked.append(relative)
|
|
156
|
+
result.findings.extend(lint_file(path, root, max_chars=max_chars))
|
|
157
|
+
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def lint_file(path: Path, root: Path, max_chars: int = 12000) -> list[Finding]:
|
|
162
|
+
relative = path.relative_to(root).as_posix()
|
|
163
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
164
|
+
lines = text.splitlines()
|
|
165
|
+
findings: list[Finding] = []
|
|
166
|
+
|
|
167
|
+
if not text.strip():
|
|
168
|
+
findings.append(Finding(relative, "error", "empty-file", "Instruction file is empty."))
|
|
169
|
+
return findings
|
|
170
|
+
|
|
171
|
+
if len(text) > max_chars:
|
|
172
|
+
findings.append(
|
|
173
|
+
Finding(
|
|
174
|
+
relative,
|
|
175
|
+
"warning",
|
|
176
|
+
"too-long",
|
|
177
|
+
f"Instruction file is {len(text)} characters; consider splitting or tightening it.",
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if not re.search(r"^#\s+\S+", text, flags=re.MULTILINE):
|
|
182
|
+
findings.append(Finding(relative, "warning", "missing-title", "Add a top-level heading."))
|
|
183
|
+
|
|
184
|
+
present_sections = detect_sections(text)
|
|
185
|
+
for section in ("purpose", "scope", "commands", "safety"):
|
|
186
|
+
if section not in present_sections:
|
|
187
|
+
findings.append(
|
|
188
|
+
Finding(
|
|
189
|
+
relative,
|
|
190
|
+
"info",
|
|
191
|
+
f"missing-{section}",
|
|
192
|
+
f"Consider adding a clear {section} section.",
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
findings.extend(scan_patterns(relative, lines, SECRET_PATTERNS, "error", "secret-like-value", "Looks like a secret or private key."))
|
|
197
|
+
findings.extend(scan_patterns(relative, lines, RISKY_COMMAND_PATTERNS, "warning", "risky-command", "Risky command should include explicit safeguards or approval rules."))
|
|
198
|
+
findings.extend(scan_patterns(relative, lines, PROMPT_INJECTION_PATTERNS, "warning", "prompt-injection", "Instruction resembles prompt-injection language."))
|
|
199
|
+
findings.extend(scan_weak_language(relative, lines))
|
|
200
|
+
findings.extend(scan_absolute_claims(relative, lines))
|
|
201
|
+
findings.extend(scan_conflicts(relative, lines))
|
|
202
|
+
|
|
203
|
+
return findings
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def detect_sections(text: str) -> set[str]:
|
|
207
|
+
lowered = text.lower()
|
|
208
|
+
present: set[str] = set()
|
|
209
|
+
for section, hints in SECTION_HINTS.items():
|
|
210
|
+
if any(hint in lowered for hint in hints):
|
|
211
|
+
present.add(section)
|
|
212
|
+
return present
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def scan_patterns(
|
|
216
|
+
relative: str,
|
|
217
|
+
lines: list[str],
|
|
218
|
+
patterns: Iterable[re.Pattern[str]],
|
|
219
|
+
severity: str,
|
|
220
|
+
rule: str,
|
|
221
|
+
message: str,
|
|
222
|
+
) -> list[Finding]:
|
|
223
|
+
findings: list[Finding] = []
|
|
224
|
+
for line_number, line in enumerate(lines, start=1):
|
|
225
|
+
if any(pattern.search(line) for pattern in patterns):
|
|
226
|
+
findings.append(Finding(relative, severity, rule, message, line_number))
|
|
227
|
+
return findings
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def scan_weak_language(relative: str, lines: list[str]) -> list[Finding]:
|
|
231
|
+
findings: list[Finding] = []
|
|
232
|
+
pattern = re.compile(r"\b(try to|maybe|if possible|do your best|尽量|可能的话)\b", re.IGNORECASE)
|
|
233
|
+
for line_number, line in enumerate(lines, start=1):
|
|
234
|
+
if pattern.search(line):
|
|
235
|
+
findings.append(
|
|
236
|
+
Finding(
|
|
237
|
+
relative,
|
|
238
|
+
"info",
|
|
239
|
+
"weak-language",
|
|
240
|
+
"Vague language can make agent behavior inconsistent.",
|
|
241
|
+
line_number,
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
return findings
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def scan_absolute_claims(relative: str, lines: list[str]) -> list[Finding]:
|
|
248
|
+
findings: list[Finding] = []
|
|
249
|
+
pattern = re.compile(r"\b(always|never|must|禁止|必须|永远|绝不)\b", re.IGNORECASE)
|
|
250
|
+
for line_number, line in enumerate(lines, start=1):
|
|
251
|
+
if pattern.search(line) and len(line) > 180:
|
|
252
|
+
findings.append(
|
|
253
|
+
Finding(
|
|
254
|
+
relative,
|
|
255
|
+
"info",
|
|
256
|
+
"long-absolute-rule",
|
|
257
|
+
"Long absolute rules are hard to follow; split into short, testable bullets.",
|
|
258
|
+
line_number,
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
return findings
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def scan_conflicts(relative: str, lines: list[str]) -> list[Finding]:
|
|
265
|
+
text = "\n".join(lines).lower()
|
|
266
|
+
findings: list[Finding] = []
|
|
267
|
+
conflict_pairs = (
|
|
268
|
+
("always ask", "never ask"),
|
|
269
|
+
("always commit", "never commit"),
|
|
270
|
+
("always browse", "never browse"),
|
|
271
|
+
("must use python", "never use python"),
|
|
272
|
+
("必须询问", "不要询问"),
|
|
273
|
+
)
|
|
274
|
+
for left, right in conflict_pairs:
|
|
275
|
+
if left in text and right in text:
|
|
276
|
+
findings.append(
|
|
277
|
+
Finding(
|
|
278
|
+
relative,
|
|
279
|
+
"warning",
|
|
280
|
+
"possible-conflict",
|
|
281
|
+
f"Possible conflicting instructions: '{left}' and '{right}'.",
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
return findings
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agent-rules-lint"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Lint AI agent instruction files such as AGENTS.md, CLAUDE.md, Cursor rules, and Copilot instructions."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Ukyo" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ai-agents", "llm", "developer-tools", "lint", "claude-code", "cursor", "copilot"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
28
|
+
"Topic :: Utilities"
|
|
29
|
+
]
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
agent-rules-lint = "agent_rules_lint.cli:main"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/Uky0Yang/agent-rules-lint"
|
|
37
|
+
Repository = "https://github.com/Uky0Yang/agent-rules-lint"
|
|
38
|
+
Issues = "https://github.com/Uky0Yang/agent-rules-lint/issues"
|
|
39
|
+
Changelog = "https://github.com/Uky0Yang/agent-rules-lint/blob/main/CHANGELOG.md"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
from contextlib import redirect_stdout
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from agent_rules_lint.cli import main
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CliTests(unittest.TestCase):
|
|
13
|
+
def test_clean_file_exits_zero(self) -> None:
|
|
14
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
15
|
+
root = Path(directory)
|
|
16
|
+
(root / "AGENTS.md").write_text(
|
|
17
|
+
"# Rules\n\n"
|
|
18
|
+
"## Purpose\n\nHelp agents.\n\n"
|
|
19
|
+
"## Scope\n\nThis repo.\n\n"
|
|
20
|
+
"## Commands\n\nRun tests.\n\n"
|
|
21
|
+
"## Safety\n\nDo not commit secrets.\n",
|
|
22
|
+
encoding="utf-8",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
with redirect_stdout(StringIO()):
|
|
26
|
+
self.assertEqual(main([str(root)]), 0)
|
|
27
|
+
|
|
28
|
+
def test_warnings_as_errors_exits_nonzero(self) -> None:
|
|
29
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
30
|
+
root = Path(directory)
|
|
31
|
+
(root / "AGENTS.md").write_text("# Rules\n\nRun `git reset --hard`.", encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
with redirect_stdout(StringIO()):
|
|
34
|
+
self.assertEqual(main([str(root), "--warnings-as-errors"]), 1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
unittest.main()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from agent_rules_lint.linter import discover_instruction_files, lint_path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LinterTests(unittest.TestCase):
|
|
11
|
+
def test_discovers_common_instruction_files(self) -> None:
|
|
12
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
13
|
+
root = Path(directory)
|
|
14
|
+
(root / "AGENTS.md").write_text("# Agent\n\nPurpose section.", encoding="utf-8")
|
|
15
|
+
(root / ".cursor" / "rules").mkdir(parents=True)
|
|
16
|
+
(root / ".cursor" / "rules" / "ui.md").write_text("# UI\n\nScope section.", encoding="utf-8")
|
|
17
|
+
(root / "README.md").write_text("# Ignore", encoding="utf-8")
|
|
18
|
+
|
|
19
|
+
resolved_root = root.resolve()
|
|
20
|
+
discovered = [path.relative_to(resolved_root).as_posix() for path in discover_instruction_files(root)]
|
|
21
|
+
|
|
22
|
+
self.assertEqual(discovered, [".cursor/rules/ui.md", "AGENTS.md"])
|
|
23
|
+
|
|
24
|
+
def test_flags_secret_like_values(self) -> None:
|
|
25
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
26
|
+
root = Path(directory)
|
|
27
|
+
(root / "CLAUDE.md").write_text("# Rules\n\nUse token sk-abcdefghijklmnopqrstuvwxyz123456.", encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
result = lint_path(root)
|
|
30
|
+
|
|
31
|
+
self.assertEqual(result.error_count, 1)
|
|
32
|
+
self.assertTrue(any(finding.rule == "secret-like-value" for finding in result.findings))
|
|
33
|
+
|
|
34
|
+
def test_flags_risky_commands_without_failing_by_default(self) -> None:
|
|
35
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
36
|
+
root = Path(directory)
|
|
37
|
+
(root / "AGENTS.md").write_text("# Rules\n\nRun `git reset --hard` when stuck.", encoding="utf-8")
|
|
38
|
+
|
|
39
|
+
result = lint_path(root)
|
|
40
|
+
|
|
41
|
+
self.assertEqual(result.error_count, 0)
|
|
42
|
+
self.assertTrue(any(finding.rule == "risky-command" for finding in result.findings))
|
|
43
|
+
|
|
44
|
+
def test_warns_when_no_instruction_files(self) -> None:
|
|
45
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
46
|
+
result = lint_path(Path(directory))
|
|
47
|
+
|
|
48
|
+
self.assertEqual(result.warning_count, 1)
|
|
49
|
+
self.assertEqual(result.findings[0].rule, "no-instruction-files")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
unittest.main()
|