maat16-agentlint 0.1.1.dev3__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.
Files changed (39) hide show
  1. maat16_agentlint-0.1.1.dev3/.github/workflows/publish.yml +54 -0
  2. maat16_agentlint-0.1.1.dev3/.gitignore +6 -0
  3. maat16_agentlint-0.1.1.dev3/PKG-INFO +169 -0
  4. maat16_agentlint-0.1.1.dev3/README.md +156 -0
  5. maat16_agentlint-0.1.1.dev3/agentlint/__init__.py +13 -0
  6. maat16_agentlint-0.1.1.dev3/agentlint/__main__.py +3 -0
  7. maat16_agentlint-0.1.1.dev3/agentlint/cli.py +111 -0
  8. maat16_agentlint-0.1.1.dev3/agentlint/core.py +373 -0
  9. maat16_agentlint-0.1.1.dev3/agentlint/packs/__init__.py +4 -0
  10. maat16_agentlint-0.1.1.dev3/agentlint/packs/_shared.py +329 -0
  11. maat16_agentlint-0.1.1.dev3/agentlint/packs/agents.py +264 -0
  12. maat16_agentlint-0.1.1.dev3/agentlint/packs/claudemd.py +191 -0
  13. maat16_agentlint-0.1.1.dev3/agentlint/packs/data/claude-code-settings.schema.json +4260 -0
  14. maat16_agentlint-0.1.1.dev3/agentlint/packs/mcp.py +171 -0
  15. maat16_agentlint-0.1.1.dev3/agentlint/packs/settings.py +172 -0
  16. maat16_agentlint-0.1.1.dev3/agentlint/packs/skills.py +315 -0
  17. maat16_agentlint-0.1.1.dev3/agentlint/report.py +209 -0
  18. maat16_agentlint-0.1.1.dev3/agentlint/report_html.py +259 -0
  19. maat16_agentlint-0.1.1.dev3/docs/README.md +14 -0
  20. maat16_agentlint-0.1.1.dev3/docs/agents.md +26 -0
  21. maat16_agentlint-0.1.1.dev3/docs/claudemd.md +20 -0
  22. maat16_agentlint-0.1.1.dev3/docs/governance.md +13 -0
  23. maat16_agentlint-0.1.1.dev3/docs/mcp.md +20 -0
  24. maat16_agentlint-0.1.1.dev3/docs/settings.md +19 -0
  25. maat16_agentlint-0.1.1.dev3/docs/skills.md +31 -0
  26. maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/PKG-INFO +169 -0
  27. maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/SOURCES.txt +37 -0
  28. maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/dependency_links.txt +1 -0
  29. maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/entry_points.txt +2 -0
  30. maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/requires.txt +1 -0
  31. maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/scm_file_list.json +33 -0
  32. maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/scm_version.json +8 -0
  33. maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/top_level.txt +1 -0
  34. maat16_agentlint-0.1.1.dev3/projects/friday/RULES.md +130 -0
  35. maat16_agentlint-0.1.1.dev3/projects/friday/connectors.txt +153 -0
  36. maat16_agentlint-0.1.1.dev3/projects/friday/policy.toml +34 -0
  37. maat16_agentlint-0.1.1.dev3/projects/friday/rules.py +515 -0
  38. maat16_agentlint-0.1.1.dev3/pyproject.toml +40 -0
  39. maat16_agentlint-0.1.1.dev3/setup.cfg +4 -0
@@ -0,0 +1,54 @@
1
+ # Publish maat16-agentlint to PyPI.
2
+ #
3
+ # Every push to main publishes a DEV pre-release (0.1.1.devN from setuptools-scm) so
4
+ # consumers that install with `pip install --pre maat16-agentlint` always run main; a tag
5
+ # `vX.Y.Z` publishes that final version. No API token anywhere: PyPI trusts this
6
+ # workflow's OIDC identity (Trusted Publishing — the project on pypi.org lists
7
+ # owner maat16 / repository agentlint / workflow publish.yml / environment pypi).
8
+ #
9
+ # The publish job needs `id-token: write`, and NOTHING else in the job may run
10
+ # untrusted code, which is why build and publish are separate jobs.
11
+ name: publish
12
+
13
+ on:
14
+ push:
15
+ branches: [main]
16
+ tags: ["v*"]
17
+ workflow_dispatch:
18
+
19
+ permissions:
20
+ contents: read
21
+
22
+ jobs:
23
+ build:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v5
27
+ with:
28
+ fetch-depth: 0 # setuptools-scm needs the tags and the commit count
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: "3.12"
32
+ - run: python -m pip install --quiet build
33
+ - run: python -m build
34
+ - run: |
35
+ python -m pip install --quiet dist/*.whl
36
+ agentlint --list-rules | head -3
37
+ python -c 'import agentlint; print("maat16-agentlint", agentlint.__version__)'
38
+ - uses: actions/upload-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+
43
+ publish:
44
+ needs: build
45
+ runs-on: ubuntu-latest
46
+ environment: pypi
47
+ permissions:
48
+ id-token: write
49
+ steps:
50
+ - uses: actions/download-artifact@v4
51
+ with:
52
+ name: dist
53
+ path: dist/
54
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ reports/
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: maat16-agentlint
3
+ Version: 0.1.1.dev3
4
+ Summary: Design-rule governance for agent projects: skills, CLAUDE.md, MCP configuration and each project's own rules, with levels, exemptions and reports for CI.
5
+ Project-URL: Source, https://github.com/maat16/agentlint
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3 :: Only
8
+ Classifier: Environment :: Console
9
+ Classifier: Topic :: Software Development :: Quality Assurance
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: pyyaml>=6
13
+
14
+ # agentlint
15
+
16
+ Design-rule governance for agent projects. One tool, many projects: it discovers a
17
+ project's skills, subagents, CLAUDE.md and AGENTS.md files, MCP configuration and Claude
18
+ Code settings, checks each against its family of rules, adds the project's own rules, and
19
+ reports under a policy that says which rules apply, at what level, and what is exempt for
20
+ how long and why.
21
+
22
+ ```
23
+ python -m agentlint friday # text report, exit 1 on errors
24
+ python -m agentlint friday --strict # warnings fail too (CI)
25
+ python -m agentlint friday --format html --out reports/friday.html --open
26
+ python -m agentlint friday --format github # GitHub Actions annotations
27
+ python -m agentlint friday --format json # for other tools
28
+ python -m agentlint friday --list-rules # the register for that project
29
+ python -m agentlint --list-rules # the built-in register, no project needed
30
+ ```
31
+
32
+ Needs Python 3.11+ and pyyaml. `pip install maat16-agentlint` (PyPI; the bare name is taken and `agent-lint` too similar —
33
+ the import name and command are still `agentlint`) or `pip install -e .` from a checkout adds
34
+ the `agentlint` command. `pip install --pre maat16-agentlint` tracks `main`: every push there is
35
+ published as a dev pre-release by `.github/workflows/publish.yml`, and a `vX.Y.Z` tag
36
+ publishes a final version.
37
+
38
+ ## The rules, documented
39
+
40
+ Every rule is declared in code with its id, default level, summary, explanation and source,
41
+ and the documents are generated from that register so they cannot drift:
42
+
43
+ - **[docs/](docs/README.md)**: the common rules, one document per family.
44
+ - **`projects/<name>/RULES.md`**: everything that runs for one project, including its own
45
+ rules, with the levels its policy makes effective.
46
+
47
+ Regenerate after any rule or policy change:
48
+
49
+ ```
50
+ python -m agentlint --list-rules --format md --split --out docs
51
+ python -m agentlint friday --list-rules --format md --out projects/friday/RULES.md
52
+ ```
53
+
54
+ ## Families
55
+
56
+ Rules are `DR-nnn` design rules in packs, one per family, each with its number range:
57
+
58
+ | range | pack | what it covers | source |
59
+ |---|---|---|---|
60
+ | DR-000–009 | engine | the policy itself: expired and unused exemptions, crashed rules | agentlint |
61
+ | DR-010–099 | `skills` | SKILL.md: the Agent Skills specification (hard rules, then recommendations) and Claude Code's extensions | agentskills.io, code.claude.com/docs/en/skills |
62
+ | DR-100–199 | `claudemd` | CLAUDE.md, AGENTS.md, CLAUDE.local.md, `.claude/rules`: imports, depth, size, scope, credentials | code.claude.com/docs/en/memory |
63
+ | DR-200–299 | `mcp` | `.mcp.json` servers by transport type, expansions, approvals in settings, tool naming, credentials | code.claude.com/docs/en/mcp |
64
+ | DR-300–399 | `agents` | `.claude/agents/*.md` subagents: loading, names, tools, values, preloaded skills, hooks, budget | code.claude.com/docs/en/sub-agents |
65
+ | DR-400–499 | `settings` | `.claude/settings*.json`: strict JSON, permission rule syntax, modes that cannot apply, hooks, unknown keys | code.claude.com/docs/en/settings, permissions, hooks; the published schema |
66
+ | DR-500–899 | | reserved | |
67
+ | DR-900–999 | `projects/<name>/rules.py` | the project's own rules; one project pack per run | the project |
68
+
69
+ **Discovery is automatic.** A pack finds its own files anywhere under the project root,
70
+ skipping `.git`, `node_modules`, virtual environments and build trees. The report header
71
+ lists what was found. A policy may add or narrow the skill globs; it never has to list
72
+ files.
73
+
74
+ ## The model
75
+
76
+ **Levels** are `error` (fails the run), `warn` (prints; fails with `--strict`), `info`
77
+ (prints) and `off` (not run). A rule has a default; a policy may change it.
78
+
79
+ **Exemptions** name a rule, a path glob, a reason, an owner and an expiry date. A covered
80
+ finding is still reported, marked EXEMPT with its reason, so nothing disappears. An expired
81
+ exemption stops applying and is reported itself (DR-001). One that matches nothing is
82
+ reported (DR-002). A rule that crashes is reported (DR-003), never skipped in silence.
83
+
84
+ **Skips** are rules that could not run, for example a project table that would not import.
85
+ A skip is visible in every report and never counts as a pass.
86
+
87
+ **Exit codes**: 0 pass · 1 findings at a failing level · 2 the policy or a pack is broken
88
+ (nothing was validated).
89
+
90
+ ## A project
91
+
92
+ ```
93
+ projects/<name>/
94
+ policy.toml root, packs, level overrides, exemptions (skill globs only to add or narrow)
95
+ rules.py the project's DR-9xx rules (optional)
96
+ RULES.md the generated register for this project
97
+ ... data the rules need (friday keeps its connector catalogue here)
98
+ ```
99
+
100
+ ```toml
101
+ [project]
102
+ name = "friday"
103
+ root = "../../../friday" # relative to this file
104
+
105
+ [rules]
106
+ packs = ["skills", "claudemd", "mcp", "agents", "settings", "rules.py"]
107
+
108
+ [rules.levels]
109
+ DR-024 = "off" # error | warn | info | off
110
+
111
+ [[exemptions]]
112
+ rule = "DR-013"
113
+ path = "friday/assets/*/.claude/skills/*/SKILL.md"
114
+ reason = "frontmatter moves under metadata in the next asset pass"
115
+ by = "suhail"
116
+ until = 2026-10-09 # a bare date
117
+ ```
118
+
119
+ A policy is found by project name under `projects/`, or given as a path. A project with no
120
+ special rules needs only the `[project]` table; every built-in pack then runs.
121
+
122
+ ## Writing a rule
123
+
124
+ A rule is a generator that yields `(path, message)` pairs, paths relative to the project
125
+ root, or `ctx.skip(reason)` when it cannot run.
126
+
127
+ ```python
128
+ from agentlint.core import rule
129
+
130
+ @rule("DR-905", "error", "one-line summary", source="where the rule comes from")
131
+ def contracts(ctx):
132
+ """Why the rule exists, in a sentence or two. This is the register's explanation."""
133
+ for s in ctx.skills: # discovered skills: dir, rel, text, meta, body, error
134
+ if ...:
135
+ yield s.rel, "what is wrong"
136
+ ```
137
+
138
+ `ctx` carries `root`, `policy`, `skills`, `files(*globs)` for other project files, `rel(path)`,
139
+ `inventory` for the report header, and a `cache` dict a pack can use to parse something once.
140
+ `agentlint/packs/_shared.py` has the helpers the built-in packs share: frontmatter and JSON
141
+ file loaders, the secret detector, hook and MCP server shape checks, tool and permission
142
+ rule syntax, the bundled settings schema. A new family is a new module under
143
+ `agentlint/packs/` with its own range, named in a policy's `packs`.
144
+
145
+ ## Reports
146
+
147
+ - `text`: what was found, one line per open finding, skips, exemptions grouped by entry, a summary line.
148
+ - `json`: inventory, findings, exemptions with state and match counts, the full register.
149
+ - `github`: `::error` / `::warning` / `::notice` annotations for Actions, plus the summary.
150
+ - `html`: a self-contained page with the verdict, a filterable findings table, the
151
+ exemptions table and the rules register. Write it with `--out`, open it with `--open`;
152
+ the text report still prints so CI logs show the result.
153
+
154
+ ## CI
155
+
156
+ ```yaml
157
+ - run: pip install pyyaml -e ./agentlint -e ./friday
158
+ - run: python -m agentlint friday --strict --format github
159
+ - run: python -m agentlint friday --format html --out reports/friday.html
160
+ if: always()
161
+ - uses: actions/upload-artifact@v4
162
+ if: always()
163
+ with: { name: agentlint-friday, path: reports/friday.html }
164
+ ```
165
+
166
+ ## Projects
167
+
168
+ - `friday`: the desk. Its pack covers the desk layout, the connector scope, the code
169
+ tables that mirror the assets, identical copies of shared skills and packaging.
@@ -0,0 +1,156 @@
1
+ # agentlint
2
+
3
+ Design-rule governance for agent projects. One tool, many projects: it discovers a
4
+ project's skills, subagents, CLAUDE.md and AGENTS.md files, MCP configuration and Claude
5
+ Code settings, checks each against its family of rules, adds the project's own rules, and
6
+ reports under a policy that says which rules apply, at what level, and what is exempt for
7
+ how long and why.
8
+
9
+ ```
10
+ python -m agentlint friday # text report, exit 1 on errors
11
+ python -m agentlint friday --strict # warnings fail too (CI)
12
+ python -m agentlint friday --format html --out reports/friday.html --open
13
+ python -m agentlint friday --format github # GitHub Actions annotations
14
+ python -m agentlint friday --format json # for other tools
15
+ python -m agentlint friday --list-rules # the register for that project
16
+ python -m agentlint --list-rules # the built-in register, no project needed
17
+ ```
18
+
19
+ Needs Python 3.11+ and pyyaml. `pip install maat16-agentlint` (PyPI; the bare name is taken and `agent-lint` too similar —
20
+ the import name and command are still `agentlint`) or `pip install -e .` from a checkout adds
21
+ the `agentlint` command. `pip install --pre maat16-agentlint` tracks `main`: every push there is
22
+ published as a dev pre-release by `.github/workflows/publish.yml`, and a `vX.Y.Z` tag
23
+ publishes a final version.
24
+
25
+ ## The rules, documented
26
+
27
+ Every rule is declared in code with its id, default level, summary, explanation and source,
28
+ and the documents are generated from that register so they cannot drift:
29
+
30
+ - **[docs/](docs/README.md)**: the common rules, one document per family.
31
+ - **`projects/<name>/RULES.md`**: everything that runs for one project, including its own
32
+ rules, with the levels its policy makes effective.
33
+
34
+ Regenerate after any rule or policy change:
35
+
36
+ ```
37
+ python -m agentlint --list-rules --format md --split --out docs
38
+ python -m agentlint friday --list-rules --format md --out projects/friday/RULES.md
39
+ ```
40
+
41
+ ## Families
42
+
43
+ Rules are `DR-nnn` design rules in packs, one per family, each with its number range:
44
+
45
+ | range | pack | what it covers | source |
46
+ |---|---|---|---|
47
+ | DR-000–009 | engine | the policy itself: expired and unused exemptions, crashed rules | agentlint |
48
+ | DR-010–099 | `skills` | SKILL.md: the Agent Skills specification (hard rules, then recommendations) and Claude Code's extensions | agentskills.io, code.claude.com/docs/en/skills |
49
+ | DR-100–199 | `claudemd` | CLAUDE.md, AGENTS.md, CLAUDE.local.md, `.claude/rules`: imports, depth, size, scope, credentials | code.claude.com/docs/en/memory |
50
+ | DR-200–299 | `mcp` | `.mcp.json` servers by transport type, expansions, approvals in settings, tool naming, credentials | code.claude.com/docs/en/mcp |
51
+ | DR-300–399 | `agents` | `.claude/agents/*.md` subagents: loading, names, tools, values, preloaded skills, hooks, budget | code.claude.com/docs/en/sub-agents |
52
+ | DR-400–499 | `settings` | `.claude/settings*.json`: strict JSON, permission rule syntax, modes that cannot apply, hooks, unknown keys | code.claude.com/docs/en/settings, permissions, hooks; the published schema |
53
+ | DR-500–899 | | reserved | |
54
+ | DR-900–999 | `projects/<name>/rules.py` | the project's own rules; one project pack per run | the project |
55
+
56
+ **Discovery is automatic.** A pack finds its own files anywhere under the project root,
57
+ skipping `.git`, `node_modules`, virtual environments and build trees. The report header
58
+ lists what was found. A policy may add or narrow the skill globs; it never has to list
59
+ files.
60
+
61
+ ## The model
62
+
63
+ **Levels** are `error` (fails the run), `warn` (prints; fails with `--strict`), `info`
64
+ (prints) and `off` (not run). A rule has a default; a policy may change it.
65
+
66
+ **Exemptions** name a rule, a path glob, a reason, an owner and an expiry date. A covered
67
+ finding is still reported, marked EXEMPT with its reason, so nothing disappears. An expired
68
+ exemption stops applying and is reported itself (DR-001). One that matches nothing is
69
+ reported (DR-002). A rule that crashes is reported (DR-003), never skipped in silence.
70
+
71
+ **Skips** are rules that could not run, for example a project table that would not import.
72
+ A skip is visible in every report and never counts as a pass.
73
+
74
+ **Exit codes**: 0 pass · 1 findings at a failing level · 2 the policy or a pack is broken
75
+ (nothing was validated).
76
+
77
+ ## A project
78
+
79
+ ```
80
+ projects/<name>/
81
+ policy.toml root, packs, level overrides, exemptions (skill globs only to add or narrow)
82
+ rules.py the project's DR-9xx rules (optional)
83
+ RULES.md the generated register for this project
84
+ ... data the rules need (friday keeps its connector catalogue here)
85
+ ```
86
+
87
+ ```toml
88
+ [project]
89
+ name = "friday"
90
+ root = "../../../friday" # relative to this file
91
+
92
+ [rules]
93
+ packs = ["skills", "claudemd", "mcp", "agents", "settings", "rules.py"]
94
+
95
+ [rules.levels]
96
+ DR-024 = "off" # error | warn | info | off
97
+
98
+ [[exemptions]]
99
+ rule = "DR-013"
100
+ path = "friday/assets/*/.claude/skills/*/SKILL.md"
101
+ reason = "frontmatter moves under metadata in the next asset pass"
102
+ by = "suhail"
103
+ until = 2026-10-09 # a bare date
104
+ ```
105
+
106
+ A policy is found by project name under `projects/`, or given as a path. A project with no
107
+ special rules needs only the `[project]` table; every built-in pack then runs.
108
+
109
+ ## Writing a rule
110
+
111
+ A rule is a generator that yields `(path, message)` pairs, paths relative to the project
112
+ root, or `ctx.skip(reason)` when it cannot run.
113
+
114
+ ```python
115
+ from agentlint.core import rule
116
+
117
+ @rule("DR-905", "error", "one-line summary", source="where the rule comes from")
118
+ def contracts(ctx):
119
+ """Why the rule exists, in a sentence or two. This is the register's explanation."""
120
+ for s in ctx.skills: # discovered skills: dir, rel, text, meta, body, error
121
+ if ...:
122
+ yield s.rel, "what is wrong"
123
+ ```
124
+
125
+ `ctx` carries `root`, `policy`, `skills`, `files(*globs)` for other project files, `rel(path)`,
126
+ `inventory` for the report header, and a `cache` dict a pack can use to parse something once.
127
+ `agentlint/packs/_shared.py` has the helpers the built-in packs share: frontmatter and JSON
128
+ file loaders, the secret detector, hook and MCP server shape checks, tool and permission
129
+ rule syntax, the bundled settings schema. A new family is a new module under
130
+ `agentlint/packs/` with its own range, named in a policy's `packs`.
131
+
132
+ ## Reports
133
+
134
+ - `text`: what was found, one line per open finding, skips, exemptions grouped by entry, a summary line.
135
+ - `json`: inventory, findings, exemptions with state and match counts, the full register.
136
+ - `github`: `::error` / `::warning` / `::notice` annotations for Actions, plus the summary.
137
+ - `html`: a self-contained page with the verdict, a filterable findings table, the
138
+ exemptions table and the rules register. Write it with `--out`, open it with `--open`;
139
+ the text report still prints so CI logs show the result.
140
+
141
+ ## CI
142
+
143
+ ```yaml
144
+ - run: pip install pyyaml -e ./agentlint -e ./friday
145
+ - run: python -m agentlint friday --strict --format github
146
+ - run: python -m agentlint friday --format html --out reports/friday.html
147
+ if: always()
148
+ - uses: actions/upload-artifact@v4
149
+ if: always()
150
+ with: { name: agentlint-friday, path: reports/friday.html }
151
+ ```
152
+
153
+ ## Projects
154
+
155
+ - `friday`: the desk. Its pack covers the desk layout, the connector scope, the code
156
+ tables that mirror the assets, identical copies of shared skills and packaging.
@@ -0,0 +1,13 @@
1
+ """agentlint: design-rule governance for agent projects.
2
+
3
+ Rules are ``DR-nnn`` ids in packs (built-in: spec, claudemd, mcp; per project: rules.py).
4
+ A project's policy sets levels and exemptions; reports come as text, json, GitHub
5
+ annotations or html. See README.md.
6
+ """
7
+ from .core import Context, Finding, Policy, PolicyError, Skill, rule # noqa: F401
8
+
9
+ try:
10
+ from importlib.metadata import PackageNotFoundError, version as _version
11
+ __version__ = _version("maat16-agentlint") # set by setuptools-scm from git at build time
12
+ except PackageNotFoundError: # a bare checkout on PYTHONPATH, not installed
13
+ __version__ = "0+unknown"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,111 @@
1
+ """agentlint command line.
2
+
3
+ agentlint <policy.toml | project-name> [--strict] [--format text|json|github|html] [--out FILE] [--open]
4
+ agentlint <policy> --list-rules [--format text|json|md] [--out FILE]
5
+ agentlint --list-rules --format md --split --out docs # the built-in families, one file each
6
+
7
+ Exit codes: 0 pass · 1 findings at a failing level (errors, or warnings with --strict) ·
8
+ 2 the policy or a rule pack is broken (nothing was validated).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import sys
14
+ import webbrowser
15
+ from pathlib import Path
16
+
17
+ from .core import RANGES, REGISTRY, PolicyError, builtin_policy, family_of, load_packs, load_policy, run
18
+ from .report import (render_github, render_json, render_rules_index, render_rules_json,
19
+ render_rules_md, render_rules_text, render_text, summary)
20
+ from .report_html import render_html
21
+
22
+ PROJECTS = Path(__file__).resolve().parent.parent / "projects"
23
+ RENDER = {"text": render_text, "json": render_json, "github": render_github, "html": render_html}
24
+ RULES = {"text": render_rules_text, "json": render_rules_json, "md": render_rules_md}
25
+
26
+
27
+ def resolve_policy(arg: str) -> Path:
28
+ p = Path(arg)
29
+ if p.is_file():
30
+ return p.resolve()
31
+ if p.is_dir() and (p / "policy.toml").is_file():
32
+ return (p / "policy.toml").resolve()
33
+ cand = PROJECTS / arg / "policy.toml"
34
+ if cand.is_file():
35
+ return cand
36
+ raise PolicyError(f"no policy at {arg!r} and no project {arg!r} under {PROJECTS}")
37
+
38
+
39
+ def emit(text: str, out: Path | None) -> None:
40
+ if out is None:
41
+ print(text)
42
+ return
43
+ out.parent.mkdir(parents=True, exist_ok=True)
44
+ out.write_text(text, encoding="utf-8", newline="\n") # LF on every OS: generated files are committed
45
+ print(f"agentlint: wrote {out}")
46
+
47
+
48
+ def write_split(policy, out: Path) -> None:
49
+ """One Markdown document per family plus an index, into a directory."""
50
+ out.mkdir(parents=True, exist_ok=True)
51
+ families = sorted({family_of(r.number) for r in REGISTRY.values()},
52
+ key=lambda f: next(i for i, (_r, n, _d) in enumerate(RANGES) if n == f))
53
+ for fam in families:
54
+ (out / f"{fam}.md").write_text(render_rules_md(policy, fam), encoding="utf-8", newline="\n")
55
+ (out / "README.md").write_text(render_rules_index(policy), encoding="utf-8", newline="\n")
56
+ print(f"agentlint: wrote {out}/README.md and {', '.join(f + '.md' for f in families)}")
57
+
58
+
59
+ def main(argv: list[str] | None = None) -> int:
60
+ for stream in (sys.stdout, sys.stderr):
61
+ if hasattr(stream, "reconfigure"):
62
+ stream.reconfigure(encoding="utf-8", errors="replace")
63
+ ap = argparse.ArgumentParser(prog="agentlint", description=(
64
+ "Design-rule governance for agent projects: skills, subagents, CLAUDE.md, MCP and settings, "
65
+ "plus each project's own rules, with levels, exemptions and reports for CI."))
66
+ ap.add_argument("policy", nargs="?", help="a policy.toml (or its directory), or a project name under "
67
+ "projects/; omit it with --list-rules to see the built-in packs")
68
+ ap.add_argument("--root", type=Path, help="override the project root the policy names")
69
+ ap.add_argument("--strict", action="store_true", help="warnings fail too (the CI setting)")
70
+ ap.add_argument("--format", choices=(*RENDER, "md"), default="text",
71
+ help="report format (default text); md is for --list-rules only")
72
+ ap.add_argument("--out", type=Path, help="write the report to this file; the text report still prints")
73
+ ap.add_argument("--open", action="store_true", help="open the file written with --out in the default browser")
74
+ ap.add_argument("--list-rules", action="store_true", help="print the rules register and exit")
75
+ ap.add_argument("--split", action="store_true",
76
+ help="with --list-rules --format md --out DIR: one document per family plus README.md")
77
+ args = ap.parse_args(argv)
78
+
79
+ try:
80
+ if args.policy is None:
81
+ if not args.list_rules:
82
+ raise PolicyError("name a policy or a project, or use --list-rules for the built-in register")
83
+ policy = builtin_policy(Path.cwd())
84
+ else:
85
+ policy = load_policy(resolve_policy(args.policy), args.root)
86
+ load_packs(policy)
87
+ if args.list_rules:
88
+ if args.split:
89
+ if args.format != "md" or args.out is None:
90
+ raise PolicyError("--split needs --format md and --out DIR")
91
+ write_split(policy, args.out)
92
+ return 0
93
+ emit(RULES.get(args.format, render_rules_text)(policy), args.out)
94
+ return 0
95
+ if args.format == "md":
96
+ raise PolicyError("--format md is for --list-rules; reports come as text, json, github or html")
97
+ findings, ctx = run(policy)
98
+ except PolicyError as e:
99
+ print(f"agentlint: {e}", file=sys.stderr)
100
+ return 2
101
+
102
+ emit(RENDER[args.format](policy, ctx, findings, args.strict), args.out)
103
+ if args.out is not None:
104
+ print(render_text(policy, ctx, findings, args.strict))
105
+ if args.open:
106
+ webbrowser.open(args.out.resolve().as_uri())
107
+ return 1 if summary(findings, args.strict)["failed"] else 0
108
+
109
+
110
+ if __name__ == "__main__": # pragma: no cover
111
+ raise SystemExit(main())