Skip to main content

agentlint/rules/
mod.rs

1use crate::Severity;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum RuleCategory {
5    Structure,
6    References,
7    Context,
8    Security,
9    Commands,
10    Consistency,
11}
12
13impl std::fmt::Display for RuleCategory {
14    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        write!(
16            formatter,
17            "{}",
18            match self {
19                Self::Structure => "structure",
20                Self::References => "references",
21                Self::Context => "context",
22                Self::Security => "security",
23                Self::Commands => "commands",
24                Self::Consistency => "consistency",
25            }
26        )
27    }
28}
29
30#[derive(Debug, Clone, Copy)]
31pub struct RuleDefinition {
32    pub id: &'static str,
33    pub name: &'static str,
34    pub description: &'static str,
35    pub default_severity: Severity,
36    pub category: RuleCategory,
37    pub why: &'static str,
38    pub bad_example: &'static str,
39    pub good_example: &'static str,
40}
41
42macro_rules! rule {
43    ($id:literal, $name:literal, $description:literal, $severity:expr, $category:expr, $why:literal, $bad:literal, $good:literal) => {
44        RuleDefinition {
45            id: $id,
46            name: $name,
47            description: $description,
48            default_severity: $severity,
49            category: $category,
50            why: $why,
51            bad_example: $bad,
52            good_example: $good,
53        }
54    };
55}
56
57pub static RULES: &[RuleDefinition] = &[
58    rule!(
59        "AGL001",
60        "invalid-syntax",
61        "Configuration syntax is malformed.",
62        Severity::Error,
63        RuleCategory::Structure,
64        "Malformed configuration cannot be loaded reliably by an agent.",
65        "{ \"mcpServers\":",
66        "{ \"mcpServers\": {} }"
67    ),
68    rule!(
69        "AGL002",
70        "invalid-frontmatter",
71        "Markdown frontmatter is malformed or uses unsupported fields.",
72        Severity::Error,
73        RuleCategory::Structure,
74        "Broken metadata can silently disable scoped instructions and skills.",
75        "---\ndescription: [\n---",
76        "---\ndescription: Backend rules\n---"
77    ),
78    rule!(
79        "AGL003",
80        "duplicate-key",
81        "A configuration object contains a duplicate key.",
82        Severity::Error,
83        RuleCategory::Structure,
84        "Parsers disagree about which duplicate value wins.",
85        "{ \"command\": \"a\", \"command\": \"b\" }",
86        "{ \"command\": \"a\" }"
87    ),
88    rule!(
89        "AGL101",
90        "invalid-mcp",
91        "An MCP configuration or server definition is invalid.",
92        Severity::Error,
93        RuleCategory::Structure,
94        "Invalid MCP servers fail at agent startup or tool invocation.",
95        "{ \"mcpServers\": { \"fs\": {} } }",
96        "{ \"mcpServers\": { \"fs\": { \"command\": \"npx\", \"args\": [] } } }"
97    ),
98    rule!(
99        "AGL102",
100        "unknown-field",
101        "A configuration contains an obviously unsupported field.",
102        Severity::Warning,
103        RuleCategory::Structure,
104        "Misspelled fields are usually ignored without an error.",
105        "commandd: npx",
106        "command: npx"
107    ),
108    rule!(
109        "AGL103",
110        "invalid-url",
111        "An MCP URL is malformed or uses an unsafe scheme.",
112        Severity::Error,
113        RuleCategory::Structure,
114        "Malformed endpoints cannot connect; non-HTTP schemes are unexpected here.",
115        "url: ht!tp://host",
116        "url: https://mcp.example.com"
117    ),
118    rule!(
119        "AGL104",
120        "missing-executable",
121        "A referenced executable is unavailable.",
122        Severity::Error,
123        RuleCategory::Structure,
124        "The configured command will fail immediately.",
125        "command: npxx",
126        "command: npx"
127    ),
128    rule!(
129        "AGL105",
130        "undocumented-environment",
131        "An MCP environment variable is not documented in an environment template.",
132        Severity::Warning,
133        RuleCategory::Structure,
134        "Undocumented variables make MCP setup difficult to reproduce.",
135        "env: { API_TOKEN: ${API_TOKEN} } with no .env.example",
136        "Document API_TOKEN= in .env.example."
137    ),
138    rule!(
139        "AGL201",
140        "invalid-glob",
141        "An instruction glob is malformed.",
142        Severity::Error,
143        RuleCategory::References,
144        "A malformed glob can prevent instructions from applying.",
145        "globs: '[src/**'",
146        "globs: 'src/**'"
147    ),
148    rule!(
149        "AGL202",
150        "missing-path",
151        "A repository path referenced by instructions does not exist.",
152        Severity::Warning,
153        RuleCategory::References,
154        "Agents waste time or make wrong assumptions when guidance names stale paths.",
155        "Read `src/removed/auth.ts` first.",
156        "Read `src/auth.ts` first."
157    ),
158    rule!(
159        "AGL203",
160        "missing-script",
161        "A referenced script file does not exist.",
162        Severity::Error,
163        RuleCategory::References,
164        "Required validation or deployment commands cannot run.",
165        "Run scripts/check.sh",
166        "Run scripts/verify.sh"
167    ),
168    rule!(
169        "AGL301",
170        "large-context",
171        "An instruction file consumes excessive context.",
172        Severity::Warning,
173        RuleCategory::Context,
174        "Large instruction files reduce context available for useful work.",
175        "An instruction file over 6,000 approximate tokens",
176        "Keep durable instructions concise and scoped."
177    ),
178    rule!(
179        "AGL302",
180        "duplicate-instruction",
181        "Substantially duplicate instructions appear in multiple files.",
182        Severity::Warning,
183        RuleCategory::Context,
184        "Repeated instructions consume context and drift independently.",
185        "Repeat the same test requirement in AGENTS.md and CLAUDE.md.",
186        "Keep shared guidance in AGENTS.md and link to it."
187    ),
188    rule!(
189        "AGL303",
190        "verbose-block",
191        "A single instruction block is excessively verbose.",
192        Severity::Info,
193        RuleCategory::Context,
194        "Dense blocks are harder for people and agents to follow.",
195        "A paragraph longer than 1,200 characters",
196        "Split instructions into concise, actionable sections."
197    ),
198    rule!(
199        "AGL401",
200        "hardcoded-secret",
201        "A likely credential is embedded in agent configuration.",
202        Severity::Error,
203        RuleCategory::Security,
204        "Agent configuration is commonly committed and broadly read.",
205        "OPENAI_API_KEY=sk-proj-example-secret",
206        "OPENAI_API_KEY=${OPENAI_API_KEY}"
207    ),
208    rule!(
209        "AGL501",
210        "destructive-command",
211        "An instruction contains a destructive shell command.",
212        Severity::Warning,
213        RuleCategory::Commands,
214        "An agent may run the command without understanding unrecoverable effects.",
215        "git clean -fdx",
216        "Review untracked files, then remove explicit paths."
217    ),
218    rule!(
219        "AGL502",
220        "unknown-command",
221        "A shell command is misspelled or unavailable.",
222        Severity::Warning,
223        RuleCategory::Commands,
224        "An unavailable command makes the documented workflow fail.",
225        "nmp test",
226        "npm test"
227    ),
228    rule!(
229        "AGL503",
230        "missing-package-script",
231        "A package-manager command references a missing script.",
232        Severity::Error,
233        RuleCategory::Commands,
234        "The command cannot run against the repository package metadata.",
235        "npm run chek",
236        "npm run check"
237    ),
238    rule!(
239        "AGL601",
240        "package-manager-conflict",
241        "Instructions conflict with the repository package manager.",
242        Severity::Warning,
243        RuleCategory::Consistency,
244        "Mixing package managers changes lockfiles and dependency resolution.",
245        "Use npm in a pnpm repository.",
246        "Use pnpm in a pnpm repository."
247    ),
248    rule!(
249        "AGL602",
250        "contradictory-instruction",
251        "Two high-confidence instructions contradict each other.",
252        Severity::Warning,
253        RuleCategory::Consistency,
254        "Conflicting guidance makes agent behavior unpredictable.",
255        "Always use npm. Never use npm.",
256        "Always use pnpm."
257    ),
258    rule!(
259        "AGL603",
260        "tooling-mismatch",
261        "Instructions require tooling unsupported by repository metadata.",
262        Severity::Warning,
263        RuleCategory::Consistency,
264        "Stale commands distract agents and fail in clean environments.",
265        "Run pytest in a repository with no Python project.",
266        "Run cargo test in a Rust repository."
267    ),
268];
269
270pub fn definitions() -> &'static [RuleDefinition] {
271    RULES
272}
273
274pub fn definition(id: &str) -> Option<&'static RuleDefinition> {
275    RULES.iter().find(|rule| rule.id.eq_ignore_ascii_case(id))
276}
277
278pub fn default_severity(id: &str) -> Severity {
279    definition(id).map_or(Severity::Warning, |rule| rule.default_severity)
280}