agentlint-cli 0.1.0

A fast, local-first linter for AI coding-agent configuration
Documentation
use crate::Severity;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuleCategory {
    Structure,
    References,
    Context,
    Security,
    Commands,
    Consistency,
}

impl std::fmt::Display for RuleCategory {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            formatter,
            "{}",
            match self {
                Self::Structure => "structure",
                Self::References => "references",
                Self::Context => "context",
                Self::Security => "security",
                Self::Commands => "commands",
                Self::Consistency => "consistency",
            }
        )
    }
}

#[derive(Debug, Clone, Copy)]
pub struct RuleDefinition {
    pub id: &'static str,
    pub name: &'static str,
    pub description: &'static str,
    pub default_severity: Severity,
    pub category: RuleCategory,
    pub why: &'static str,
    pub bad_example: &'static str,
    pub good_example: &'static str,
}

macro_rules! rule {
    ($id:literal, $name:literal, $description:literal, $severity:expr, $category:expr, $why:literal, $bad:literal, $good:literal) => {
        RuleDefinition {
            id: $id,
            name: $name,
            description: $description,
            default_severity: $severity,
            category: $category,
            why: $why,
            bad_example: $bad,
            good_example: $good,
        }
    };
}

pub static RULES: &[RuleDefinition] = &[
    rule!(
        "AGL001",
        "invalid-syntax",
        "Configuration syntax is malformed.",
        Severity::Error,
        RuleCategory::Structure,
        "Malformed configuration cannot be loaded reliably by an agent.",
        "{ \"mcpServers\":",
        "{ \"mcpServers\": {} }"
    ),
    rule!(
        "AGL002",
        "invalid-frontmatter",
        "Markdown frontmatter is malformed or uses unsupported fields.",
        Severity::Error,
        RuleCategory::Structure,
        "Broken metadata can silently disable scoped instructions and skills.",
        "---\ndescription: [\n---",
        "---\ndescription: Backend rules\n---"
    ),
    rule!(
        "AGL003",
        "duplicate-key",
        "A configuration object contains a duplicate key.",
        Severity::Error,
        RuleCategory::Structure,
        "Parsers disagree about which duplicate value wins.",
        "{ \"command\": \"a\", \"command\": \"b\" }",
        "{ \"command\": \"a\" }"
    ),
    rule!(
        "AGL101",
        "invalid-mcp",
        "An MCP configuration or server definition is invalid.",
        Severity::Error,
        RuleCategory::Structure,
        "Invalid MCP servers fail at agent startup or tool invocation.",
        "{ \"mcpServers\": { \"fs\": {} } }",
        "{ \"mcpServers\": { \"fs\": { \"command\": \"npx\", \"args\": [] } } }"
    ),
    rule!(
        "AGL102",
        "unknown-field",
        "A configuration contains an obviously unsupported field.",
        Severity::Warning,
        RuleCategory::Structure,
        "Misspelled fields are usually ignored without an error.",
        "commandd: npx",
        "command: npx"
    ),
    rule!(
        "AGL103",
        "invalid-url",
        "An MCP URL is malformed or uses an unsafe scheme.",
        Severity::Error,
        RuleCategory::Structure,
        "Malformed endpoints cannot connect; non-HTTP schemes are unexpected here.",
        "url: ht!tp://host",
        "url: https://mcp.example.com"
    ),
    rule!(
        "AGL104",
        "missing-executable",
        "A referenced executable is unavailable.",
        Severity::Error,
        RuleCategory::Structure,
        "The configured command will fail immediately.",
        "command: npxx",
        "command: npx"
    ),
    rule!(
        "AGL105",
        "undocumented-environment",
        "An MCP environment variable is not documented in an environment template.",
        Severity::Warning,
        RuleCategory::Structure,
        "Undocumented variables make MCP setup difficult to reproduce.",
        "env: { API_TOKEN: ${API_TOKEN} } with no .env.example",
        "Document API_TOKEN= in .env.example."
    ),
    rule!(
        "AGL201",
        "invalid-glob",
        "An instruction glob is malformed.",
        Severity::Error,
        RuleCategory::References,
        "A malformed glob can prevent instructions from applying.",
        "globs: '[src/**'",
        "globs: 'src/**'"
    ),
    rule!(
        "AGL202",
        "missing-path",
        "A repository path referenced by instructions does not exist.",
        Severity::Warning,
        RuleCategory::References,
        "Agents waste time or make wrong assumptions when guidance names stale paths.",
        "Read `src/removed/auth.ts` first.",
        "Read `src/auth.ts` first."
    ),
    rule!(
        "AGL203",
        "missing-script",
        "A referenced script file does not exist.",
        Severity::Error,
        RuleCategory::References,
        "Required validation or deployment commands cannot run.",
        "Run scripts/check.sh",
        "Run scripts/verify.sh"
    ),
    rule!(
        "AGL301",
        "large-context",
        "An instruction file consumes excessive context.",
        Severity::Warning,
        RuleCategory::Context,
        "Large instruction files reduce context available for useful work.",
        "An instruction file over 6,000 approximate tokens",
        "Keep durable instructions concise and scoped."
    ),
    rule!(
        "AGL302",
        "duplicate-instruction",
        "Substantially duplicate instructions appear in multiple files.",
        Severity::Warning,
        RuleCategory::Context,
        "Repeated instructions consume context and drift independently.",
        "Repeat the same test requirement in AGENTS.md and CLAUDE.md.",
        "Keep shared guidance in AGENTS.md and link to it."
    ),
    rule!(
        "AGL303",
        "verbose-block",
        "A single instruction block is excessively verbose.",
        Severity::Info,
        RuleCategory::Context,
        "Dense blocks are harder for people and agents to follow.",
        "A paragraph longer than 1,200 characters",
        "Split instructions into concise, actionable sections."
    ),
    rule!(
        "AGL401",
        "hardcoded-secret",
        "A likely credential is embedded in agent configuration.",
        Severity::Error,
        RuleCategory::Security,
        "Agent configuration is commonly committed and broadly read.",
        "OPENAI_API_KEY=sk-proj-example-secret",
        "OPENAI_API_KEY=${OPENAI_API_KEY}"
    ),
    rule!(
        "AGL501",
        "destructive-command",
        "An instruction contains a destructive shell command.",
        Severity::Warning,
        RuleCategory::Commands,
        "An agent may run the command without understanding unrecoverable effects.",
        "git clean -fdx",
        "Review untracked files, then remove explicit paths."
    ),
    rule!(
        "AGL502",
        "unknown-command",
        "A shell command is misspelled or unavailable.",
        Severity::Warning,
        RuleCategory::Commands,
        "An unavailable command makes the documented workflow fail.",
        "nmp test",
        "npm test"
    ),
    rule!(
        "AGL503",
        "missing-package-script",
        "A package-manager command references a missing script.",
        Severity::Error,
        RuleCategory::Commands,
        "The command cannot run against the repository package metadata.",
        "npm run chek",
        "npm run check"
    ),
    rule!(
        "AGL601",
        "package-manager-conflict",
        "Instructions conflict with the repository package manager.",
        Severity::Warning,
        RuleCategory::Consistency,
        "Mixing package managers changes lockfiles and dependency resolution.",
        "Use npm in a pnpm repository.",
        "Use pnpm in a pnpm repository."
    ),
    rule!(
        "AGL602",
        "contradictory-instruction",
        "Two high-confidence instructions contradict each other.",
        Severity::Warning,
        RuleCategory::Consistency,
        "Conflicting guidance makes agent behavior unpredictable.",
        "Always use npm. Never use npm.",
        "Always use pnpm."
    ),
    rule!(
        "AGL603",
        "tooling-mismatch",
        "Instructions require tooling unsupported by repository metadata.",
        Severity::Warning,
        RuleCategory::Consistency,
        "Stale commands distract agents and fail in clean environments.",
        "Run pytest in a repository with no Python project.",
        "Run cargo test in a Rust repository."
    ),
];

pub fn definitions() -> &'static [RuleDefinition] {
    RULES
}

pub fn definition(id: &str) -> Option<&'static RuleDefinition> {
    RULES.iter().find(|rule| rule.id.eq_ignore_ascii_case(id))
}

pub fn default_severity(id: &str) -> Severity {
    definition(id).map_or(Severity::Warning, |rule| rule.default_severity)
}