hermead 0.1.0__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.
- hermead-0.1.0/.gitignore +27 -0
- hermead-0.1.0/LICENSE +21 -0
- hermead-0.1.0/PKG-INFO +213 -0
- hermead-0.1.0/README.md +197 -0
- hermead-0.1.0/hermead/__init__.py +29 -0
- hermead-0.1.0/hermead/config.py +121 -0
- hermead-0.1.0/hermead/detector.py +172 -0
- hermead-0.1.0/hermead/hooks.py +284 -0
- hermead-0.1.0/hermead/manifest.json +12 -0
- hermead-0.1.0/hermead/reporter.py +430 -0
- hermead-0.1.0/hermead/runners/__init__.py +158 -0
- hermead-0.1.0/hermead/runners/generic.py +205 -0
- hermead-0.1.0/hermead/runners/go.py +279 -0
- hermead-0.1.0/hermead/runners/javascript.py +349 -0
- hermead-0.1.0/hermead/runners/python.py +411 -0
- hermead-0.1.0/hermead/runners/rust.py +345 -0
- hermead-0.1.0/hermead/runners/shell.py +181 -0
- hermead-0.1.0/pyproject.toml +25 -0
- hermead-0.1.0/skills/hermead/SKILL.md +206 -0
- hermead-0.1.0/tests/__init__.py +1 -0
- hermead-0.1.0/tests/fixtures/intentional_issues/bad_code.js +16 -0
- hermead-0.1.0/tests/fixtures/intentional_issues/bad_code.py +42 -0
- hermead-0.1.0/tests/fixtures/intentional_issues/bad_script.sh +17 -0
- hermead-0.1.0/tests/fixtures/intentional_issues/pyproject.toml +19 -0
- hermead-0.1.0/tests/integration_test.py +310 -0
hermead-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
venv/
|
|
11
|
+
.venv/
|
|
12
|
+
|
|
13
|
+
# IDEs
|
|
14
|
+
.vscode/
|
|
15
|
+
.idea/
|
|
16
|
+
|
|
17
|
+
# OS
|
|
18
|
+
.DS_Store
|
|
19
|
+
Thumbs.db
|
|
20
|
+
|
|
21
|
+
# Hermes
|
|
22
|
+
.hermes/
|
|
23
|
+
|
|
24
|
+
# Test coverage
|
|
25
|
+
htmlcov/
|
|
26
|
+
.coverage
|
|
27
|
+
.coverage.*
|
hermead-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Tony Simons
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
hermead-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hermead
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hermes Agent plugin for project-level linting, type-checking, formatting, and security scanning
|
|
5
|
+
Author: Tony Simons
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Provides-Extra: dashboard
|
|
11
|
+
Requires-Dist: fastapi; extra == 'dashboard'
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-timeout; extra == 'dev'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# HermeAd
|
|
18
|
+
|
|
19
|
+
[](https://pypi.org/project/hermead/)
|
|
20
|
+
[](LICENSE)
|
|
21
|
+
[](https://pypi.org/project/hermead/)
|
|
22
|
+
|
|
23
|
+
**HermeAd** is a Hermes Agent plugin that automatically runs linters, type checkers, formatters, and security scanners on project files after every `write_file` or `patch` tool call.
|
|
24
|
+
|
|
25
|
+
No manual commands. No context switching. Files get checked the moment you write them.
|
|
26
|
+
|
|
27
|
+
## How It Works
|
|
28
|
+
|
|
29
|
+
HermeAd registers a single `post_tool_call` hook. After each file modification:
|
|
30
|
+
|
|
31
|
+
1. Detects the file type from extension or filename
|
|
32
|
+
2. Walks up the tree to find the project root (`.git`, `.hg`, or `.hermes` directory)
|
|
33
|
+
3. Loads config: built-in defaults → global `~/.hermes/hermead.yaml` → per-project `.hermes/hermead.yaml`
|
|
34
|
+
4. Auto-detects tooling from `pyproject.toml`, `package.json`, `go.mod`, `Cargo.toml`, `.shellcheckrc`
|
|
35
|
+
5. Routes to the right runner for each check category (lint, type check, format, security)
|
|
36
|
+
|
|
37
|
+
Results appear inline in the session. Issues surface at the moment of introduction, not after a CI run.
|
|
38
|
+
|
|
39
|
+
## Quick Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install hermead
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
From source:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/asimons81/hermead.git
|
|
49
|
+
cd hermead
|
|
50
|
+
pip install -e ".[dev]"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Verify it loaded:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
hermes plugin list
|
|
57
|
+
# → hermead should appear
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- **Zero-config for common projects.** Works out of the box with ruff + mypy for Python, eslint + prettier + tsc for JS/TS, golangci-lint + go vet + gofmt for Go, clippy + rustfmt for Rust, shellcheck for shell scripts.
|
|
63
|
+
- **Auto-detection.** Reads `pyproject.toml`, `package.json`, `go.mod`, `Cargo.toml`, and `.shellcheckrc` to pick the right tools. Auto-detected tools take priority over config.
|
|
64
|
+
- **Per-project config.** `.hermes/hermead.yaml` overrides global `~/.hermes/hermead.yaml`, which overrides built-in defaults.
|
|
65
|
+
- **Threshold system.** Set findings to `block`, `warn`, or `ignore` per severity level (lint warnings, type errors, security findings).
|
|
66
|
+
- **6 language runners.** Python, JavaScript/TypeScript, Go, Rust, Shell, and a Generic runner for config/script files (semgrep).
|
|
67
|
+
- **Graceful degredation.** Missing tools produce empty results. No crashes, no spurious errors. Use `which <tool>` to check availability.
|
|
68
|
+
- **Ignore patterns.** Skip `node_modules/`, `venv/`, build output dirs, and anything else you add.
|
|
69
|
+
- **Extra CLI args.** Pass custom flags per tool (e.g. `ruff: ["--line-length", "100"]`).
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
### Quick Start
|
|
74
|
+
|
|
75
|
+
Create `.hermes/hermead.yaml` at your project root:
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
python:
|
|
79
|
+
lint: ruff
|
|
80
|
+
type_check: mypy
|
|
81
|
+
formatter: ruff
|
|
82
|
+
security: semgrep
|
|
83
|
+
|
|
84
|
+
thresholds:
|
|
85
|
+
lint_warnings: warn
|
|
86
|
+
type_errors: block
|
|
87
|
+
security_high: block
|
|
88
|
+
|
|
89
|
+
ignore_paths:
|
|
90
|
+
- node_modules/**
|
|
91
|
+
- venv/**
|
|
92
|
+
- __pycache__/**
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Full Reference
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
# Per-language tool assignments (null = skip that check category)
|
|
99
|
+
python:
|
|
100
|
+
lint: ruff
|
|
101
|
+
type_check: mypy
|
|
102
|
+
formatter: ruff
|
|
103
|
+
security: semgrep
|
|
104
|
+
|
|
105
|
+
javascript:
|
|
106
|
+
lint: eslint
|
|
107
|
+
type_check: tsc
|
|
108
|
+
formatter: prettier
|
|
109
|
+
security: semgrep
|
|
110
|
+
|
|
111
|
+
go:
|
|
112
|
+
lint: golangci-lint
|
|
113
|
+
type_check: go vet
|
|
114
|
+
formatter: gofmt
|
|
115
|
+
security: gosec
|
|
116
|
+
|
|
117
|
+
rust:
|
|
118
|
+
lint: clippy
|
|
119
|
+
type_check: cargo check
|
|
120
|
+
formatter: rustfmt
|
|
121
|
+
security: cargo audit
|
|
122
|
+
|
|
123
|
+
shell:
|
|
124
|
+
lint: shellcheck
|
|
125
|
+
formatter: shfmt
|
|
126
|
+
|
|
127
|
+
generic:
|
|
128
|
+
lint: semgrep
|
|
129
|
+
security: semgrep
|
|
130
|
+
|
|
131
|
+
thresholds:
|
|
132
|
+
lint_warnings: warn # warn | block | ignore
|
|
133
|
+
type_errors: block
|
|
134
|
+
security_high: block
|
|
135
|
+
security_medium: warn
|
|
136
|
+
security_low: info
|
|
137
|
+
|
|
138
|
+
ignore_paths:
|
|
139
|
+
- node_modules/**
|
|
140
|
+
- venv/**
|
|
141
|
+
- .venv/**
|
|
142
|
+
- __pycache__/**
|
|
143
|
+
- .git/**
|
|
144
|
+
- dist/**
|
|
145
|
+
- build/**
|
|
146
|
+
- target/**
|
|
147
|
+
- .hermes/**
|
|
148
|
+
|
|
149
|
+
extra_args:
|
|
150
|
+
ruff: ["--line-length", "100"]
|
|
151
|
+
mypy: ["--strict"]
|
|
152
|
+
golangci-lint: ["--timeout", "5m"]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Supported Languages
|
|
156
|
+
|
|
157
|
+
| Language | Extensions | Lint | Type Check | Format | Security |
|
|
158
|
+
|----------|-----------|------|------------|--------|----------|
|
|
159
|
+
| Python | `.py` | ruff | mypy | ruff / black | bandit / semgrep |
|
|
160
|
+
| JavaScript / TypeScript | `.js`, `.ts`, `.jsx`, `.tsx`, `.mjs`, `.cjs` | eslint | tsc | prettier | semgrep |
|
|
161
|
+
| Go | `.go` | golangci-lint | go vet | gofmt | gosec |
|
|
162
|
+
| Rust | `.rs` | clippy | cargo check | rustfmt | cargo audit |
|
|
163
|
+
| Shell | `.sh`, `.bash`, `.zsh`, `.fish` | shellcheck | — | shfmt | — |
|
|
164
|
+
| Generic | Dockerfile, Makefile, `.gitignore`, etc. | semgrep | — | — | semgrep |
|
|
165
|
+
|
|
166
|
+
## Inline Reports
|
|
167
|
+
|
|
168
|
+
When HermeAd runs, findings appear in your session as structured results:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
╔══════════════════════════════════════════════════════════╗
|
|
172
|
+
║ HermeAd — lint results for src/main.py ║
|
|
173
|
+
╠══════════════════════════════════════════════════════════╣
|
|
174
|
+
║ tool: ruff severity: error line: 42 col: 5 ║
|
|
175
|
+
║ F841 — local variable 'x' is assigned to but never ║
|
|
176
|
+
║ used ║
|
|
177
|
+
╠══════════════════════════════════════════════════════════╣
|
|
178
|
+
║ 2 findings (1 error, 1 warning) — 0 blocked, 2 warned ║
|
|
179
|
+
╚══════════════════════════════════════════════════════════╝
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Each result shows the tool, severity, source location, message, and rule code. Empty output means the file passed or the tool wasn't available.
|
|
183
|
+
|
|
184
|
+
## Contributing
|
|
185
|
+
|
|
186
|
+
PRs welcome. HermeAd is a young project — the runner interface, result format, and hooks API are still finding their shape.
|
|
187
|
+
|
|
188
|
+
- Runners live in `hermead/runners/`. Each module self-registers via `register_runner(language, action, func)`.
|
|
189
|
+
- The hook dispatcher lives in `hermead/hooks.py`.
|
|
190
|
+
- Config loading lives in `hermead/config.py`.
|
|
191
|
+
- The registry lives in `hermead/runners/__init__.py`.
|
|
192
|
+
|
|
193
|
+
Before opening a PR, run the test suite:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
pip install -e ".[dev]"
|
|
197
|
+
pytest -v
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Plugin Architecture
|
|
201
|
+
|
|
202
|
+
HermeAd is a Hermes Agent plugin — no MCP server, no web server, no external API. It registers a single hook entry point in `pyproject.toml`:
|
|
203
|
+
|
|
204
|
+
```toml
|
|
205
|
+
[project.entry-points."hermes_agent.plugins"]
|
|
206
|
+
hermead = "hermead:register"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
For more on building Hermes plugins, see the [Hermes Agent Plugin Docs](https://hermes-agent.nousresearch.com/docs/plugins).
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT — see [LICENSE](LICENSE).
|
hermead-0.1.0/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# HermeAd
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/hermead/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://pypi.org/project/hermead/)
|
|
6
|
+
|
|
7
|
+
**HermeAd** is a Hermes Agent plugin that automatically runs linters, type checkers, formatters, and security scanners on project files after every `write_file` or `patch` tool call.
|
|
8
|
+
|
|
9
|
+
No manual commands. No context switching. Files get checked the moment you write them.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
HermeAd registers a single `post_tool_call` hook. After each file modification:
|
|
14
|
+
|
|
15
|
+
1. Detects the file type from extension or filename
|
|
16
|
+
2. Walks up the tree to find the project root (`.git`, `.hg`, or `.hermes` directory)
|
|
17
|
+
3. Loads config: built-in defaults → global `~/.hermes/hermead.yaml` → per-project `.hermes/hermead.yaml`
|
|
18
|
+
4. Auto-detects tooling from `pyproject.toml`, `package.json`, `go.mod`, `Cargo.toml`, `.shellcheckrc`
|
|
19
|
+
5. Routes to the right runner for each check category (lint, type check, format, security)
|
|
20
|
+
|
|
21
|
+
Results appear inline in the session. Issues surface at the moment of introduction, not after a CI run.
|
|
22
|
+
|
|
23
|
+
## Quick Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install hermead
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
From source:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/asimons81/hermead.git
|
|
33
|
+
cd hermead
|
|
34
|
+
pip install -e ".[dev]"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Verify it loaded:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
hermes plugin list
|
|
41
|
+
# → hermead should appear
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
- **Zero-config for common projects.** Works out of the box with ruff + mypy for Python, eslint + prettier + tsc for JS/TS, golangci-lint + go vet + gofmt for Go, clippy + rustfmt for Rust, shellcheck for shell scripts.
|
|
47
|
+
- **Auto-detection.** Reads `pyproject.toml`, `package.json`, `go.mod`, `Cargo.toml`, and `.shellcheckrc` to pick the right tools. Auto-detected tools take priority over config.
|
|
48
|
+
- **Per-project config.** `.hermes/hermead.yaml` overrides global `~/.hermes/hermead.yaml`, which overrides built-in defaults.
|
|
49
|
+
- **Threshold system.** Set findings to `block`, `warn`, or `ignore` per severity level (lint warnings, type errors, security findings).
|
|
50
|
+
- **6 language runners.** Python, JavaScript/TypeScript, Go, Rust, Shell, and a Generic runner for config/script files (semgrep).
|
|
51
|
+
- **Graceful degredation.** Missing tools produce empty results. No crashes, no spurious errors. Use `which <tool>` to check availability.
|
|
52
|
+
- **Ignore patterns.** Skip `node_modules/`, `venv/`, build output dirs, and anything else you add.
|
|
53
|
+
- **Extra CLI args.** Pass custom flags per tool (e.g. `ruff: ["--line-length", "100"]`).
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
### Quick Start
|
|
58
|
+
|
|
59
|
+
Create `.hermes/hermead.yaml` at your project root:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
python:
|
|
63
|
+
lint: ruff
|
|
64
|
+
type_check: mypy
|
|
65
|
+
formatter: ruff
|
|
66
|
+
security: semgrep
|
|
67
|
+
|
|
68
|
+
thresholds:
|
|
69
|
+
lint_warnings: warn
|
|
70
|
+
type_errors: block
|
|
71
|
+
security_high: block
|
|
72
|
+
|
|
73
|
+
ignore_paths:
|
|
74
|
+
- node_modules/**
|
|
75
|
+
- venv/**
|
|
76
|
+
- __pycache__/**
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Full Reference
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
# Per-language tool assignments (null = skip that check category)
|
|
83
|
+
python:
|
|
84
|
+
lint: ruff
|
|
85
|
+
type_check: mypy
|
|
86
|
+
formatter: ruff
|
|
87
|
+
security: semgrep
|
|
88
|
+
|
|
89
|
+
javascript:
|
|
90
|
+
lint: eslint
|
|
91
|
+
type_check: tsc
|
|
92
|
+
formatter: prettier
|
|
93
|
+
security: semgrep
|
|
94
|
+
|
|
95
|
+
go:
|
|
96
|
+
lint: golangci-lint
|
|
97
|
+
type_check: go vet
|
|
98
|
+
formatter: gofmt
|
|
99
|
+
security: gosec
|
|
100
|
+
|
|
101
|
+
rust:
|
|
102
|
+
lint: clippy
|
|
103
|
+
type_check: cargo check
|
|
104
|
+
formatter: rustfmt
|
|
105
|
+
security: cargo audit
|
|
106
|
+
|
|
107
|
+
shell:
|
|
108
|
+
lint: shellcheck
|
|
109
|
+
formatter: shfmt
|
|
110
|
+
|
|
111
|
+
generic:
|
|
112
|
+
lint: semgrep
|
|
113
|
+
security: semgrep
|
|
114
|
+
|
|
115
|
+
thresholds:
|
|
116
|
+
lint_warnings: warn # warn | block | ignore
|
|
117
|
+
type_errors: block
|
|
118
|
+
security_high: block
|
|
119
|
+
security_medium: warn
|
|
120
|
+
security_low: info
|
|
121
|
+
|
|
122
|
+
ignore_paths:
|
|
123
|
+
- node_modules/**
|
|
124
|
+
- venv/**
|
|
125
|
+
- .venv/**
|
|
126
|
+
- __pycache__/**
|
|
127
|
+
- .git/**
|
|
128
|
+
- dist/**
|
|
129
|
+
- build/**
|
|
130
|
+
- target/**
|
|
131
|
+
- .hermes/**
|
|
132
|
+
|
|
133
|
+
extra_args:
|
|
134
|
+
ruff: ["--line-length", "100"]
|
|
135
|
+
mypy: ["--strict"]
|
|
136
|
+
golangci-lint: ["--timeout", "5m"]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Supported Languages
|
|
140
|
+
|
|
141
|
+
| Language | Extensions | Lint | Type Check | Format | Security |
|
|
142
|
+
|----------|-----------|------|------------|--------|----------|
|
|
143
|
+
| Python | `.py` | ruff | mypy | ruff / black | bandit / semgrep |
|
|
144
|
+
| JavaScript / TypeScript | `.js`, `.ts`, `.jsx`, `.tsx`, `.mjs`, `.cjs` | eslint | tsc | prettier | semgrep |
|
|
145
|
+
| Go | `.go` | golangci-lint | go vet | gofmt | gosec |
|
|
146
|
+
| Rust | `.rs` | clippy | cargo check | rustfmt | cargo audit |
|
|
147
|
+
| Shell | `.sh`, `.bash`, `.zsh`, `.fish` | shellcheck | — | shfmt | — |
|
|
148
|
+
| Generic | Dockerfile, Makefile, `.gitignore`, etc. | semgrep | — | — | semgrep |
|
|
149
|
+
|
|
150
|
+
## Inline Reports
|
|
151
|
+
|
|
152
|
+
When HermeAd runs, findings appear in your session as structured results:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
╔══════════════════════════════════════════════════════════╗
|
|
156
|
+
║ HermeAd — lint results for src/main.py ║
|
|
157
|
+
╠══════════════════════════════════════════════════════════╣
|
|
158
|
+
║ tool: ruff severity: error line: 42 col: 5 ║
|
|
159
|
+
║ F841 — local variable 'x' is assigned to but never ║
|
|
160
|
+
║ used ║
|
|
161
|
+
╠══════════════════════════════════════════════════════════╣
|
|
162
|
+
║ 2 findings (1 error, 1 warning) — 0 blocked, 2 warned ║
|
|
163
|
+
╚══════════════════════════════════════════════════════════╝
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Each result shows the tool, severity, source location, message, and rule code. Empty output means the file passed or the tool wasn't available.
|
|
167
|
+
|
|
168
|
+
## Contributing
|
|
169
|
+
|
|
170
|
+
PRs welcome. HermeAd is a young project — the runner interface, result format, and hooks API are still finding their shape.
|
|
171
|
+
|
|
172
|
+
- Runners live in `hermead/runners/`. Each module self-registers via `register_runner(language, action, func)`.
|
|
173
|
+
- The hook dispatcher lives in `hermead/hooks.py`.
|
|
174
|
+
- Config loading lives in `hermead/config.py`.
|
|
175
|
+
- The registry lives in `hermead/runners/__init__.py`.
|
|
176
|
+
|
|
177
|
+
Before opening a PR, run the test suite:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pip install -e ".[dev]"
|
|
181
|
+
pytest -v
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Plugin Architecture
|
|
185
|
+
|
|
186
|
+
HermeAd is a Hermes Agent plugin — no MCP server, no web server, no external API. It registers a single hook entry point in `pyproject.toml`:
|
|
187
|
+
|
|
188
|
+
```toml
|
|
189
|
+
[project.entry-points."hermes_agent.plugins"]
|
|
190
|
+
hermead = "hermead:register"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
For more on building Hermes plugins, see the [Hermes Agent Plugin Docs](https://hermes-agent.nousresearch.com/docs/plugins).
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""HermeAd: Hermes Agent plugin for project-level linting, type-checking, formatting, and security scanning.
|
|
2
|
+
|
|
3
|
+
Runs entirely through Hermes hook system. No external API, no MCP server, no web server.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from hermead.hooks import post_tool_call
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register():
|
|
12
|
+
"""Register HermeAd hooks with the Hermes Agent plugin system.
|
|
13
|
+
|
|
14
|
+
Returns the hook registration dict expected by Hermes Agent.
|
|
15
|
+
"""
|
|
16
|
+
return {
|
|
17
|
+
"hooks": {
|
|
18
|
+
"post_tool_call": post_tool_call,
|
|
19
|
+
},
|
|
20
|
+
"metadata": {
|
|
21
|
+
"name": "hermead",
|
|
22
|
+
"version": __version__,
|
|
23
|
+
"description": (
|
|
24
|
+
"Post-tool-call hook that detects write_file/patch on project "
|
|
25
|
+
"files and auto-runs linting, type-checking, formatting, and "
|
|
26
|
+
"security checks."
|
|
27
|
+
),
|
|
28
|
+
},
|
|
29
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Configuration loader for HermeAd.
|
|
2
|
+
|
|
3
|
+
Loads .hermes/hermead.yaml with per-project first, then global fallback.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
DEFAULT_CONFIG: dict[str, Any] = {
|
|
15
|
+
"python": {
|
|
16
|
+
"lint": "ruff",
|
|
17
|
+
"type_check": "mypy",
|
|
18
|
+
"formatter": "ruff",
|
|
19
|
+
"security": "semgrep",
|
|
20
|
+
},
|
|
21
|
+
"javascript": {
|
|
22
|
+
"lint": "eslint",
|
|
23
|
+
"type_check": "tsc",
|
|
24
|
+
"formatter": "prettier",
|
|
25
|
+
"security": "semgrep",
|
|
26
|
+
},
|
|
27
|
+
"go": {
|
|
28
|
+
"lint": "golangci-lint",
|
|
29
|
+
"type_check": "go vet",
|
|
30
|
+
"formatter": "gofmt",
|
|
31
|
+
"security": "gosec",
|
|
32
|
+
},
|
|
33
|
+
"rust": {
|
|
34
|
+
"lint": "clippy",
|
|
35
|
+
"type_check": "rustc",
|
|
36
|
+
"formatter": "rustfmt",
|
|
37
|
+
"security": "cargo audit",
|
|
38
|
+
},
|
|
39
|
+
"shell": {
|
|
40
|
+
"lint": "shellcheck",
|
|
41
|
+
"type_check": None,
|
|
42
|
+
"formatter": "shfmt",
|
|
43
|
+
"security": None,
|
|
44
|
+
},
|
|
45
|
+
"generic": {
|
|
46
|
+
"lint": "semgrep",
|
|
47
|
+
"type_check": None,
|
|
48
|
+
"formatter": None,
|
|
49
|
+
"security": "semgrep",
|
|
50
|
+
},
|
|
51
|
+
"thresholds": {
|
|
52
|
+
"lint_warnings": "warn",
|
|
53
|
+
"type_errors": "block",
|
|
54
|
+
"security_high": "block",
|
|
55
|
+
"security_medium": "warn",
|
|
56
|
+
},
|
|
57
|
+
"ignore_paths": [
|
|
58
|
+
"node_modules/**",
|
|
59
|
+
"venv/**",
|
|
60
|
+
".venv/**",
|
|
61
|
+
"__pycache__/**",
|
|
62
|
+
".git/**",
|
|
63
|
+
"dist/**",
|
|
64
|
+
"build/**",
|
|
65
|
+
"target/**",
|
|
66
|
+
".hermes/**",
|
|
67
|
+
],
|
|
68
|
+
"extra_args": {},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def find_project_root(path: str | Path | None = None) -> Path | None:
|
|
73
|
+
"""Walk up from *path* (default: cwd) looking for a .git, .hg, or .hermes directory."""
|
|
74
|
+
start = Path(path or os.getcwd()).resolve()
|
|
75
|
+
for ancestor in [start] + list(start.parents):
|
|
76
|
+
if (ancestor / ".git").is_dir():
|
|
77
|
+
return ancestor
|
|
78
|
+
if (ancestor / ".hg").is_dir():
|
|
79
|
+
return ancestor
|
|
80
|
+
if (ancestor / ".hermes").is_dir():
|
|
81
|
+
return ancestor
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def load_hermead_config(path: str | Path | None = None) -> dict[str, Any]:
|
|
86
|
+
"""Load the effective HermeAd config, merging global → per-project overrides.
|
|
87
|
+
|
|
88
|
+
1. Start with DEFAULT_CONFIG.
|
|
89
|
+
2. Overlay ``~/.hermes/hermead.yaml`` if it exists (global).
|
|
90
|
+
3. Overlay ``<project-root>/.hermes/hermead.yaml`` if it exists (per-project).
|
|
91
|
+
"""
|
|
92
|
+
config: dict[str, Any] = _deep_merge({}, DEFAULT_CONFIG)
|
|
93
|
+
|
|
94
|
+
# Global fallback
|
|
95
|
+
global_path = Path.home() / ".hermes" / "hermead.yaml"
|
|
96
|
+
if global_path.is_file():
|
|
97
|
+
with open(global_path, encoding="utf-8") as fh:
|
|
98
|
+
global_cfg: dict[str, Any] = yaml.safe_load(fh) or {}
|
|
99
|
+
config = _deep_merge(config, global_cfg)
|
|
100
|
+
|
|
101
|
+
# Per-project override
|
|
102
|
+
project_root = find_project_root(path)
|
|
103
|
+
if project_root is not None:
|
|
104
|
+
local_path = project_root / ".hermes" / "hermead.yaml"
|
|
105
|
+
if local_path.is_file():
|
|
106
|
+
with open(local_path, encoding="utf-8") as fh:
|
|
107
|
+
local_cfg: dict[str, Any] = yaml.safe_load(fh) or {}
|
|
108
|
+
config = _deep_merge(config, local_cfg)
|
|
109
|
+
|
|
110
|
+
return config
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _deep_merge(base: dict, overlay: dict) -> dict:
|
|
114
|
+
"""Recursively merge *overlay* into *base* (keys in overlay win)."""
|
|
115
|
+
merged = dict(base)
|
|
116
|
+
for key, value in overlay.items():
|
|
117
|
+
if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
|
|
118
|
+
merged[key] = _deep_merge(merged[key], value)
|
|
119
|
+
else:
|
|
120
|
+
merged[key] = value
|
|
121
|
+
return merged
|