Files
dev-workflows/skills/hermaguard/tools/agentsmd-linter/agentsmd-lint.py
T
evandrodevbr c17192c9fe feat: vendorizar as 10 skills do pilar de seguranca em skills/
Copia local (nao fork, nao submodule) de safedeps, sca-audit,
secrets-scan, code-review-security, api-security-review,
web-security-review, cve-triage, patch-prioritization,
dependency-scanning e hermaguard -- commit exato de origem e licenca
de cada uma em skills/NOTICE.md. Elimina o passo de clone externo
pro pilar de seguranca do wf-security-review: git clone deste repo
ja traz tudo funcional.

Corrige tambem o comando de instalacao pro Claude Code nos dois
READMEs e no docs/SKILLS.md: Claude Code exige um diretorio por
skill (skills/<nome>/SKILL.md), nao aceita os .md soltos que o
comando antigo copiava -- confirmado testando a instalacao real.
2026-08-24 00:32:20 -03:00

260 lines
10 KiB
Python

#!/usr/bin/env python3
"""
AGENTS.md Linter — Scores instruction files based on evidence from research.
A regex keyword/structure heuristic inspired by (not derived from) the
Instructions-as-Code criteria for what makes instruction files effective for
AI coding agents. A high score is necessary, not sufficient: it rewards
presence of the right signals, it does not verify they are correct.
Criteria (from research):
1. Specificity — concrete rules, not vague guidance
2. Project conventions — naming, file structure, patterns
3. Test guidance — how to run tests, what coverage means
4. Component navigation — which files do what, dependency map
5. Stack details — versions, tools, configuration
6. Anti-patterns — what NOT to do, common mistakes
7. Scope boundaries — what the agent should and shouldn't touch
Usage:
python3 agentsmd-lint.py /path/to/CLAUDE.md
python3 agentsmd-lint.py --dir /path/to/project (finds CLAUDE.md/AGENTS.md)
"""
import argparse
import os
import re
import sys
from datetime import datetime
VERSION = "1.0.0"
def find_instruction_files(directory: str) -> list:
"""Find CLAUDE.md, AGENTS.md, or .cursorrules files."""
candidates = ["CLAUDE.md", "AGENTS.md", ".cursorrules", ".claude/CLAUDE.md", ".github/AGENTS.md"]
found = []
for c in candidates:
path = os.path.join(directory, c)
if os.path.isfile(path):
found.append(path)
return found
def score_file(filepath: str) -> dict:
"""Score an instruction file and return detailed results."""
if not os.path.isfile(filepath):
return {"error": f"File not found: {filepath}", "score": 0}
with open(filepath, encoding="utf-8", errors="replace") as f:
content = f.read()
lines = content.split("\n")
line_count = len(lines)
word_count = len(content.split())
# ── Criterion 1: Specificity (0-20) ──
# Concrete rules use: "do X", "don't Y", "always", "never", "must", "must not"
specificity_patterns = [
(r'\b(?:do|don\'?t|always|never|must|must\s+not|should\s+not|ensure)\b', 3),
(r'```', 3), # code examples
(r'\b(?:e\.g\.|for example|specifically)\b', 2),
(r'\b(?:error|warning|fail|break)\b', 2),
]
specificity_score = 0
for pattern, weight in specificity_patterns:
matches = len(re.findall(pattern, content, re.IGNORECASE))
specificity_score += min(matches * weight, 10)
# Cap at 20
specificity_score = min(specificity_score, 20)
# ── Criterion 2: Project Conventions (0-15) ──
convention_keywords = [
r'\b(?:naming|convention|pattern|structure|organisation|organization|layout)\b',
r'\b(?:file\s+name|directory|folder|path|module)\b',
r'\b(?:import|require|dependency|package)\b',
]
convention_score = 0
for pattern in convention_keywords:
if re.search(pattern, content, re.IGNORECASE):
convention_score += 5
convention_score = min(convention_score, 15)
# ── Criterion 3: Test Guidance (0-15) ──
test_keywords = [
r'\b(?:test|tests|testing|coverage|spec)\b',
r'\b(?:pytest|jest|mocha|vitest|unittest|rspec)\b',
r'\b(?:run\s+tests?|test\s+command|how\s+to\s+test)\b',
]
test_score = 0
for pattern in test_keywords:
if re.search(pattern, content, re.IGNORECASE):
test_score += 5
test_score = min(test_score, 15)
# ── Criterion 4: Component Navigation (0-15) ──
nav_keywords = [
r'\b(?:src/|lib/|app/|components?/|pages/|routes?/)\b',
r'\b(?:entry\s+point|main|index|app\b.*\bentry)\b',
r'\b(?:architecture|dependency|depends\s+on|calls?|uses?)\b',
]
nav_score = 0
for pattern in nav_keywords:
if re.search(pattern, content, re.IGNORECASE):
nav_score += 5
# Bonus for explicit file tree
if re.search(r'├|└|│', content):
nav_score += 5
nav_score = min(nav_score, 15)
# ── Criterion 5: Stack Details (0-10) ──
stack_keywords = [
r'\b(?:version|v\d+\.\d+|@\d+\.\d+)\b',
r'\b(?:python|node|rust|go|typescript|javascript)\s+\d+\.\d+\b',
r'\b(?:config|\.env|settings|setup)\b',
]
stack_score = 0
for pattern in stack_keywords:
if re.search(pattern, content, re.IGNORECASE):
stack_score += 3
stack_score = min(stack_score, 10)
# ── Criterion 6: Anti-patterns (0-10) ──
antipattern_keywords = [
r'\b(?:don\'?t|avoid|never|do\s+not|should\s+not|must\s+not|wrong)\b',
r'\b(?:common\s+mistake|pitfall|gotcha|beware|warning)\b',
r'\b(?:bad|incorrect|broken|anti.?pattern)\b',
]
antipattern_score = 0
for pattern in antipattern_keywords:
matches = len(re.findall(pattern, content, re.IGNORECASE))
antipattern_score += min(matches * 2, 6)
antipattern_score = min(antipattern_score, 10)
# ── Criterion 7: Scope Boundaries (0-10) ──
scope_keywords = [
r'\b(?:scope|boundary|limit|constraint|out\s+of\s+scope|in\s+scope)\b',
r'\b(?:don\'?t\s+touch|leave\s+alone|hands?\s+off|do\s+not\s+modify)\b',
r'\b(?:only|restrict|confine|within)\b',
]
scope_score = 0
for pattern in scope_keywords:
if re.search(pattern, content, re.IGNORECASE):
scope_score += 3
scope_score = min(scope_score, 10)
# ── Bonus: Structure (0-5) ──
# Well-structured files use headings, bullet lists, and sections
has_headings = bool(re.search(r'^#{1,3}\s', content, re.MULTILINE))
has_lists = bool(re.search(r'^\s*[-*+]\s', content, re.MULTILINE))
has_sections = len(re.findall(r'^#{1,3}\s', content, re.MULTILINE)) >= 3
structure_bonus = (2 if has_headings else 0) + (1 if has_lists else 0) + (2 if has_sections else 0)
total = specificity_score + convention_score + test_score + nav_score + stack_score + antipattern_score + scope_score + structure_bonus
total = min(total, 100)
# ── Recommendations ──
recommendations = []
if specificity_score < 10:
recommendations.append("Add concrete rules: use 'do X', 'don't Y', 'always', 'never'. Include code examples (``` blocks).")
if convention_score < 5:
recommendations.append("Document naming conventions, file structure, and import patterns.")
if test_score < 5:
recommendations.append("Add test guidance: how to run tests, what framework, coverage expectations.")
if nav_score < 5:
recommendations.append("Map key directories (src/, lib/, routes/) and explain what lives where.")
if stack_score < 3:
recommendations.append("Specify language versions and key dependencies with versions.")
if antipattern_score < 5:
recommendations.append("List common mistakes to avoid — 'never do X because Y'.")
if scope_score < 3:
recommendations.append("Define what the agent should and shouldn't modify.")
# Grade
grade = "S" if total >= 90 else ("A" if total >= 75 else ("B" if total >= 60 else ("C" if total >= 40 else "D")))
return {
"file": filepath,
"score": total,
"grade": grade,
"max_score": 100,
"stats": {
"lines": line_count,
"words": word_count,
},
"breakdown": {
"specificity": specificity_score,
"conventions": convention_score,
"test_guidance": test_score,
"component_navigation": nav_score,
"stack_details": stack_score,
"antipatterns": antipattern_score,
"scope_boundaries": scope_score,
"structure_bonus": structure_bonus,
},
"recommendations": recommendations,
"linted_at": datetime.now().isoformat(),
}
def main():
parser = argparse.ArgumentParser(description="AGENTS.md Linter — score instruction files for AI agent effectiveness")
parser.add_argument("path", nargs="?", help="Path to instruction file")
parser.add_argument("--dir", help="Directory to search for instruction files")
args = parser.parse_args()
if args.dir:
files = find_instruction_files(args.dir)
if not files:
print(f"No instruction files found in {args.dir}")
print(f"Looked for: CLAUDE.md, AGENTS.md, .cursorrules")
sys.exit(1)
print(f"Found {len(files)} instruction file(s) in {args.dir}\n")
for f in files:
result = score_file(f)
print(f" {os.path.basename(f)}: {result['score']}/100 ({result['grade']})")
return
elif args.path:
result = score_file(args.path)
else:
parser.print_help()
sys.exit(1)
if "error" in result:
print(f"Error: {result['error']}")
sys.exit(1)
print(f"\n{'─'*60}")
print(f" File: {result['file']}")
print(f" Score: {result['score']}/{result['max_score']} ({result['grade']})")
print(f" Stats: {result['stats']['lines']} lines, {result['stats']['words']} words")
print(f"{'─'*60}")
# Each criterion has its own max; normalise the bar to that max so a full
# 10/10 reads as full, not half (the old bar assumed a fixed /20 scale).
criterion_max = {
"specificity": 20, "conventions": 15, "test_guidance": 15,
"component_navigation": 15, "stack_details": 10, "antipatterns": 10,
"scope_boundaries": 10, "structure_bonus": 5,
}
print(f"\n Breakdown:")
for criterion, score in result["breakdown"].items():
cmax = criterion_max.get(criterion, 10) or 1
filled = round(score / cmax * 10)
bar = "█" * filled + "░" * (10 - filled)
print(f" {criterion.replace('_', ' ').title():25s} {score:3d}/{cmax:<3d} {bar}")
if result["recommendations"]:
print(f"\n Recommendations:")
for i, rec in enumerate(result["recommendations"], 1):
print(f" {i}. {rec}")
print(f"\n Grade scale: S (90+) A (75-89) B (60-74) C (40-59) D (<40)")
print(f" Method: regex keyword/structure heuristics, inspired by (not derived from)")
print(f" the Instructions-as-Code criteria. A high score is necessary, not sufficient.\n")
if __name__ == "__main__":
main()