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.
- maat16_agentlint-0.1.1.dev3/.github/workflows/publish.yml +54 -0
- maat16_agentlint-0.1.1.dev3/.gitignore +6 -0
- maat16_agentlint-0.1.1.dev3/PKG-INFO +169 -0
- maat16_agentlint-0.1.1.dev3/README.md +156 -0
- maat16_agentlint-0.1.1.dev3/agentlint/__init__.py +13 -0
- maat16_agentlint-0.1.1.dev3/agentlint/__main__.py +3 -0
- maat16_agentlint-0.1.1.dev3/agentlint/cli.py +111 -0
- maat16_agentlint-0.1.1.dev3/agentlint/core.py +373 -0
- maat16_agentlint-0.1.1.dev3/agentlint/packs/__init__.py +4 -0
- maat16_agentlint-0.1.1.dev3/agentlint/packs/_shared.py +329 -0
- maat16_agentlint-0.1.1.dev3/agentlint/packs/agents.py +264 -0
- maat16_agentlint-0.1.1.dev3/agentlint/packs/claudemd.py +191 -0
- maat16_agentlint-0.1.1.dev3/agentlint/packs/data/claude-code-settings.schema.json +4260 -0
- maat16_agentlint-0.1.1.dev3/agentlint/packs/mcp.py +171 -0
- maat16_agentlint-0.1.1.dev3/agentlint/packs/settings.py +172 -0
- maat16_agentlint-0.1.1.dev3/agentlint/packs/skills.py +315 -0
- maat16_agentlint-0.1.1.dev3/agentlint/report.py +209 -0
- maat16_agentlint-0.1.1.dev3/agentlint/report_html.py +259 -0
- maat16_agentlint-0.1.1.dev3/docs/README.md +14 -0
- maat16_agentlint-0.1.1.dev3/docs/agents.md +26 -0
- maat16_agentlint-0.1.1.dev3/docs/claudemd.md +20 -0
- maat16_agentlint-0.1.1.dev3/docs/governance.md +13 -0
- maat16_agentlint-0.1.1.dev3/docs/mcp.md +20 -0
- maat16_agentlint-0.1.1.dev3/docs/settings.md +19 -0
- maat16_agentlint-0.1.1.dev3/docs/skills.md +31 -0
- maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/PKG-INFO +169 -0
- maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/SOURCES.txt +37 -0
- maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/dependency_links.txt +1 -0
- maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/entry_points.txt +2 -0
- maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/requires.txt +1 -0
- maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/scm_file_list.json +33 -0
- maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/scm_version.json +8 -0
- maat16_agentlint-0.1.1.dev3/maat16_agentlint.egg-info/top_level.txt +1 -0
- maat16_agentlint-0.1.1.dev3/projects/friday/RULES.md +130 -0
- maat16_agentlint-0.1.1.dev3/projects/friday/connectors.txt +153 -0
- maat16_agentlint-0.1.1.dev3/projects/friday/policy.toml +34 -0
- maat16_agentlint-0.1.1.dev3/projects/friday/rules.py +515 -0
- maat16_agentlint-0.1.1.dev3/pyproject.toml +40 -0
- 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,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,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())
|