henxels 0.3.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.
- henxels-0.3.0/LICENSE +21 -0
- henxels-0.3.0/PKG-INFO +259 -0
- henxels-0.3.0/README.md +228 -0
- henxels-0.3.0/henxels/__init__.py +17 -0
- henxels-0.3.0/henxels/__main__.py +8 -0
- henxels-0.3.0/henxels/bless.py +68 -0
- henxels-0.3.0/henxels/casing.py +30 -0
- henxels-0.3.0/henxels/catalogue.py +128 -0
- henxels-0.3.0/henxels/cli.py +366 -0
- henxels-0.3.0/henxels/commands.py +30 -0
- henxels-0.3.0/henxels/contract.py +152 -0
- henxels-0.3.0/henxels/diffinfo.py +53 -0
- henxels-0.3.0/henxels/digest.py +85 -0
- henxels-0.3.0/henxels/doctor.py +58 -0
- henxels-0.3.0/henxels/engine/__init__.py +1 -0
- henxels-0.3.0/henxels/engine/discover.py +74 -0
- henxels-0.3.0/henxels/engine/gitinfo.py +95 -0
- henxels-0.3.0/henxels/engine/report.py +79 -0
- henxels-0.3.0/henxels/explain.py +53 -0
- henxels-0.3.0/henxels/filesize.py +98 -0
- henxels-0.3.0/henxels/findings.py +32 -0
- henxels-0.3.0/henxels/guard.py +80 -0
- henxels-0.3.0/henxels/hookrun.py +90 -0
- henxels-0.3.0/henxels/hooks.py +82 -0
- henxels-0.3.0/henxels/invocation.py +26 -0
- henxels-0.3.0/henxels/locations.py +96 -0
- henxels-0.3.0/henxels/runner.py +102 -0
- henxels-0.3.0/henxels/scaffold.py +130 -0
- henxels-0.3.0/henxels/schema/__init__.py +9 -0
- henxels-0.3.0/henxels/schema/henxels.schema.json +131 -0
- henxels-0.3.0/henxels/settings.py +61 -0
- henxels-0.3.0/henxels/similarity.py +112 -0
- henxels-0.3.0/henxels/statements/__init__.py +25 -0
- henxels-0.3.0/henxels/statements/builtins/__init__.py +33 -0
- henxels-0.3.0/henxels/statements/builtins/_helpers.py +44 -0
- henxels-0.3.0/henxels/statements/builtins/commands.py +18 -0
- henxels-0.3.0/henxels/statements/builtins/content.py +167 -0
- henxels-0.3.0/henxels/statements/builtins/history.py +52 -0
- henxels-0.3.0/henxels/statements/builtins/links.py +100 -0
- henxels-0.3.0/henxels/statements/builtins/meta.py +50 -0
- henxels-0.3.0/henxels/statements/builtins/naming.py +35 -0
- henxels-0.3.0/henxels/statements/builtins/security.py +39 -0
- henxels-0.3.0/henxels/statements/builtins/size.py +17 -0
- henxels-0.3.0/henxels/statements/builtins/structure.py +92 -0
- henxels-0.3.0/henxels/statements/registry.py +95 -0
- henxels-0.3.0/henxels/statements/scope.py +85 -0
- henxels-0.3.0/henxels/util/__init__.py +1 -0
- henxels-0.3.0/henxels/util/glob.py +55 -0
- henxels-0.3.0/henxels.egg-info/PKG-INFO +259 -0
- henxels-0.3.0/henxels.egg-info/SOURCES.txt +77 -0
- henxels-0.3.0/henxels.egg-info/dependency_links.txt +1 -0
- henxels-0.3.0/henxels.egg-info/entry_points.txt +2 -0
- henxels-0.3.0/henxels.egg-info/requires.txt +9 -0
- henxels-0.3.0/henxels.egg-info/top_level.txt +1 -0
- henxels-0.3.0/pyproject.toml +72 -0
- henxels-0.3.0/setup.cfg +4 -0
- henxels-0.3.0/tests/test_bless.py +35 -0
- henxels-0.3.0/tests/test_catalogue.py +52 -0
- henxels-0.3.0/tests/test_cli.py +84 -0
- henxels-0.3.0/tests/test_contract.py +102 -0
- henxels-0.3.0/tests/test_digest.py +38 -0
- henxels-0.3.0/tests/test_discover.py +39 -0
- henxels-0.3.0/tests/test_explain.py +41 -0
- henxels-0.3.0/tests/test_filesize.py +56 -0
- henxels-0.3.0/tests/test_glob.py +41 -0
- henxels-0.3.0/tests/test_guards.py +67 -0
- henxels-0.3.0/tests/test_history.py +96 -0
- henxels-0.3.0/tests/test_hooks.py +44 -0
- henxels-0.3.0/tests/test_hooks_integration.py +64 -0
- henxels-0.3.0/tests/test_init_doctor.py +51 -0
- henxels-0.3.0/tests/test_invocation.py +20 -0
- henxels-0.3.0/tests/test_locations.py +55 -0
- henxels-0.3.0/tests/test_npm_wrapper.py +36 -0
- henxels-0.3.0/tests/test_report.py +66 -0
- henxels-0.3.0/tests/test_runner_v2.py +114 -0
- henxels-0.3.0/tests/test_schema.py +33 -0
- henxels-0.3.0/tests/test_settings.py +25 -0
- henxels-0.3.0/tests/test_similarity.py +46 -0
- henxels-0.3.0/tests/test_statements.py +334 -0
henxels-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benquemax
|
|
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.
|
henxels-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: henxels
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A repo-level harness for coding agents — file-level structure constraints that keep every agent true to your repo
|
|
5
|
+
Author: benquemax
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/benquemax/henxels
|
|
8
|
+
Project-URL: Repository, https://github.com/benquemax/henxels
|
|
9
|
+
Project-URL: Issues, https://github.com/benquemax/henxels/issues
|
|
10
|
+
Keywords: harness,agent-harness,coding-agent,structure,contract,agents,ai,lint,guardrails,pre-commit,validation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: PyYAML>=6.0
|
|
24
|
+
Provides-Extra: markdown
|
|
25
|
+
Requires-Dist: pymarkdownlnt>=0.9; extra == "markdown"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
29
|
+
Requires-Dist: pymarkdownlnt>=0.9; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
<!-- markdownlint-disable -->
|
|
33
|
+
```
|
|
34
|
+
╭───────────────╮
|
|
35
|
+
│ ╷ ╷ ╷ ╷ │ h e n x e l s
|
|
36
|
+
│ ╵‖ ╵ ╵ ‖╵ │ suspenders for your repo
|
|
37
|
+
│ ‖ ‖ │ keep your ADHD agent in henxels
|
|
38
|
+
╰───────────────╯
|
|
39
|
+
```
|
|
40
|
+
<!-- markdownlint-enable -->
|
|
41
|
+
|
|
42
|
+
# henxels
|
|
43
|
+
|
|
44
|
+
**A repo-level harness for coding agents** — file-level rules that steer agents (and
|
|
45
|
+
humans) to keep a repository true to a contract. Each rule is a *henxel* (from Finnish
|
|
46
|
+
_henkselit_, "suspenders").
|
|
47
|
+
|
|
48
|
+
Most agent harnesses wrap the *agent*. henxels is a harness that lives in the *repo*: a
|
|
49
|
+
structural contract that holds **every** agent — Claude Code, OpenCode, Aider, Hermes,
|
|
50
|
+
Pi, or a human — to the same shape, no matter which one made the change.
|
|
51
|
+
|
|
52
|
+
henxels is for repos where small, eager, easily-distracted coding agents keep writing
|
|
53
|
+
the right thing in the wrong place. It puts the expected structure **in front of the
|
|
54
|
+
agent** (in `AGENTS.md` and on demand) and makes breaking it **impossible by accident**:
|
|
55
|
+
to disobey a henxel you must change the contract — a conscious, reviewable act.
|
|
56
|
+
|
|
57
|
+
Under the hood henxels is **a framework + a growing community library of checks**:
|
|
58
|
+
the contract just lists which checks apply where; the checks are functions, and you can
|
|
59
|
+
add your own in three lines.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## The contract reads like a whiteboard
|
|
64
|
+
|
|
65
|
+
`henxels.yaml` is a list of rules. Each **henxel** is a sentence (which doubles as the
|
|
66
|
+
failure message) plus the **statements** that must all pass. Logic lives inside the
|
|
67
|
+
statements, so the YAML stays a dumb, readable list.
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
settings: # behaviours, not tests
|
|
71
|
+
ask_me_before_staging: true
|
|
72
|
+
confirm_before_push: true
|
|
73
|
+
confirm_before_deleting: { over_lines: 5 }
|
|
74
|
+
|
|
75
|
+
henxels:
|
|
76
|
+
- henxel: "Docs are kebab-case markdown, each with a title and summary"
|
|
77
|
+
in: ./docs # ./docs = this level; ./docs/* = recursive
|
|
78
|
+
allowed_filetypes: .md # a scalar; lists are OR: [.md, .txt]
|
|
79
|
+
filename_casing: kebab-case
|
|
80
|
+
required_frontmatter: [title, summary] # a list here is AND: both required
|
|
81
|
+
|
|
82
|
+
- henxel: "Project config lives only in pyproject.toml"
|
|
83
|
+
forbidden_files: [setup.py, setup.cfg] # no `in:` = whole repo
|
|
84
|
+
|
|
85
|
+
- henxel: "The test suite passes before every commit"
|
|
86
|
+
run_before_commit: "uv run pytest -q"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Browse every statement you can use with **`henxels catalogue`**.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Principles
|
|
94
|
+
|
|
95
|
+
1. **The contract is the single source of structural truth.** If it isn't in
|
|
96
|
+
`henxels.yaml`, it isn't a rule.
|
|
97
|
+
2. **Read it like a document.** A human — or a small model — understands the repo's
|
|
98
|
+
shape without reading a validator.
|
|
99
|
+
3. **A henxel is a test that must pass.** Logic hides in the statement functions; the
|
|
100
|
+
YAML just lists them. Failure returns an *instruction*, not a bare boolean.
|
|
101
|
+
4. **Steer before you stop.** Every henxel says, in words, what to do instead.
|
|
102
|
+
5. **Disobey responsibly.** The only escape hatch is editing the contract.
|
|
103
|
+
6. **Awareness beats blocking** — especially for duplication.
|
|
104
|
+
7. **Beautiful for humans, silent for machines.** Fancy in a terminal, plain in a pipe.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Quick start
|
|
109
|
+
|
|
110
|
+
### Install
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
uv tool install henxels # puts `henxels` on your PATH (recommended)
|
|
114
|
+
# alternatives:
|
|
115
|
+
# pipx install henxels # same idea, via pipx
|
|
116
|
+
# uvx henxels <cmd> # zero-install, run on demand
|
|
117
|
+
# npm i -g henxels # node-friendly launcher (shims to Python)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
> If henxels lives only inside a project venv, invoke it as `uv run henxels …`. The git
|
|
121
|
+
> hooks resolve it either way, and henxels' own messages always print the form that
|
|
122
|
+
> works in your shell.
|
|
123
|
+
|
|
124
|
+
### Give this to your agent
|
|
125
|
+
|
|
126
|
+
> Install henxels (`uv tool install henxels`, or `pipx install henxels`, or
|
|
127
|
+
> `npm i -D henxels` — it shims to Python; install a prerequisite if one's missing).
|
|
128
|
+
> Run `henxels init`, then tailor `henxels.yaml` to this repo's folders (run
|
|
129
|
+
> `henxels catalogue` to see the statements), and run `henxels sync` and
|
|
130
|
+
> `henxels check --all`.
|
|
131
|
+
|
|
132
|
+
### Or yourself
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
henxels init # scaffold contract + git hooks + AGENTS.md digest
|
|
136
|
+
henxels catalogue # browse the statements you can use
|
|
137
|
+
henxels explain docs/x.md # what governs this path?
|
|
138
|
+
henxels check --all # run the contract
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Custom checks in three lines
|
|
144
|
+
|
|
145
|
+
Need a check that doesn't exist? Write a statement. Drop it in `henxels_checks.py` at
|
|
146
|
+
the repo root (auto-loaded — no config) and use it like any built-in.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from henxels import statement
|
|
150
|
+
|
|
151
|
+
@statement("max_lines", help="source files stay under a line budget")
|
|
152
|
+
def max_lines(param, file, scope): # asks for `file` → per-file, no loop
|
|
153
|
+
if scope.line_count(file) > param:
|
|
154
|
+
return f"split it — keep under {param} lines" # fail = the instruction itself
|
|
155
|
+
```
|
|
156
|
+
```yaml
|
|
157
|
+
- henxel: "No source file exceeds 500 lines"
|
|
158
|
+
in: ./src/*
|
|
159
|
+
max_lines: 500
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Arguments are injected by name (`param`, `scope`, `file`, `root`, `settings`) — take
|
|
163
|
+
only what you need. Return `None`/`True` to pass, or a string instruction to fail.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Example: keeping an LLM wiki from scattering
|
|
168
|
+
|
|
169
|
+
An **LLM wiki** — a markdown knowledge base an agent reads and writes (the pattern
|
|
170
|
+
popularized by Andrej Karpathy) — is a perfect henxels use case. The idea is great, but
|
|
171
|
+
small models drift hard: they write against the conventions, and knowledge that belongs
|
|
172
|
+
in **one** page ends up **scattered across several near-duplicate files**. henxels gives
|
|
173
|
+
the wiki a structure the agent has to follow, and warns it the moment it's about to
|
|
174
|
+
fragment a topic.
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
settings:
|
|
178
|
+
confirm_before_deleting: { over_lines: 10 } # don't lose knowledge to a diff slip
|
|
179
|
+
warn_about_similar_files: # the anti-scatter henxel: nudges the
|
|
180
|
+
above: 0.82 # agent to UPDATE a page, not clone it
|
|
181
|
+
ignore: ["**/index.md"]
|
|
182
|
+
|
|
183
|
+
henxels:
|
|
184
|
+
- henxel: "Every wiki page is kebab-case markdown with findable metadata"
|
|
185
|
+
in: ./pages/*
|
|
186
|
+
filename_casing: kebab-case
|
|
187
|
+
allowed_filetypes: [.md]
|
|
188
|
+
required_frontmatter: [title, tags, updated]
|
|
189
|
+
- henxel: "Pages are clean markdown with no dead links"
|
|
190
|
+
in: ./pages/*
|
|
191
|
+
markdown_lint: true
|
|
192
|
+
links_resolve: true # a custom check (see "Custom checks" above)
|
|
193
|
+
- henxel: "The wiki has a single index"
|
|
194
|
+
in: ./pages
|
|
195
|
+
required_files: index.md
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Because the contract is mirrored into `AGENTS.md`, the agent reads *"one page per topic,
|
|
199
|
+
kebab-case, with these fields"* **before** it writes — and `warn_about_similar_files`
|
|
200
|
+
catches it when it's about to create the fifth slightly-different page about the same
|
|
201
|
+
thing. Strong guidance is exactly what small LLMs need to stay tidy.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Ouroboric by design
|
|
206
|
+
|
|
207
|
+
henxels eats its own tail, and it's the better for it. Its own repo is governed by its
|
|
208
|
+
own `henxels.yaml`, so every feature is dogfooded on the tool before it ships.
|
|
209
|
+
`well_formed_statements` is a check that checks the checks. `markdown_links_absolute`
|
|
210
|
+
guards the README that documents henxels. The pre-commit hook runs `henxels check` —
|
|
211
|
+
henxels gating henxels. The contract even mirrors itself into `AGENTS.md` to steer the
|
|
212
|
+
agent that edits the contract.
|
|
213
|
+
|
|
214
|
+
The tail-eating *is* the test.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Guards & bless
|
|
219
|
+
|
|
220
|
+
`settings` can guard hard-to-undo actions. They don't forbid — they make you mean it:
|
|
221
|
+
|
|
222
|
+
```text
|
|
223
|
+
$ git push
|
|
224
|
+
✗ Push is guarded — a push is hard to take back
|
|
225
|
+
→ henxels bless push (then push again)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`henxels bless push` mints a one-time token bound to the exact commit. The delete guard
|
|
229
|
+
covers deleted files **and** net-removed lines (diff-edit mistakes lose rows), released
|
|
230
|
+
by `henxels bless delete`.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Contributing — the agentic era
|
|
235
|
+
|
|
236
|
+
henxels thrives on contributions. **We'd rather get a ready-to-merge PR than an issue.**
|
|
237
|
+
If you (or your agent) write a check that's *reusable* — general, not tied to your repo —
|
|
238
|
+
contribute it upstream: `henxels contribute`. Quality gates (ruff + the test suite) run
|
|
239
|
+
in pre-commit and CI, so a green local run means your PR is merge-ready. See
|
|
240
|
+
[`CONTRIBUTING.md`](https://github.com/benquemax/henxels/blob/main/CONTRIBUTING.md).
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Commands
|
|
245
|
+
|
|
246
|
+
| Command | Purpose |
|
|
247
|
+
|---------|---------|
|
|
248
|
+
| `henxels init` | scaffold contract + hooks + digest |
|
|
249
|
+
| `henxels check [--all\|--staged] […]` | run the contract |
|
|
250
|
+
| `henxels explain <path>` | what governs this location |
|
|
251
|
+
| `henxels catalogue` | browse the statements you can use |
|
|
252
|
+
| `henxels create-new-statement <name>` | scaffold a custom statement |
|
|
253
|
+
| `henxels contribute [name]` | how to upstream a reusable statement |
|
|
254
|
+
| `henxels bless <push\|delete>` | consciously override a guard |
|
|
255
|
+
| `henxels sync` / `henxels doctor` | refresh the digest / check the setup |
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
MIT.
|
henxels-0.3.0/README.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
<!-- markdownlint-disable -->
|
|
2
|
+
```
|
|
3
|
+
╭───────────────╮
|
|
4
|
+
│ ╷ ╷ ╷ ╷ │ h e n x e l s
|
|
5
|
+
│ ╵‖ ╵ ╵ ‖╵ │ suspenders for your repo
|
|
6
|
+
│ ‖ ‖ │ keep your ADHD agent in henxels
|
|
7
|
+
╰───────────────╯
|
|
8
|
+
```
|
|
9
|
+
<!-- markdownlint-enable -->
|
|
10
|
+
|
|
11
|
+
# henxels
|
|
12
|
+
|
|
13
|
+
**A repo-level harness for coding agents** — file-level rules that steer agents (and
|
|
14
|
+
humans) to keep a repository true to a contract. Each rule is a *henxel* (from Finnish
|
|
15
|
+
_henkselit_, "suspenders").
|
|
16
|
+
|
|
17
|
+
Most agent harnesses wrap the *agent*. henxels is a harness that lives in the *repo*: a
|
|
18
|
+
structural contract that holds **every** agent — Claude Code, OpenCode, Aider, Hermes,
|
|
19
|
+
Pi, or a human — to the same shape, no matter which one made the change.
|
|
20
|
+
|
|
21
|
+
henxels is for repos where small, eager, easily-distracted coding agents keep writing
|
|
22
|
+
the right thing in the wrong place. It puts the expected structure **in front of the
|
|
23
|
+
agent** (in `AGENTS.md` and on demand) and makes breaking it **impossible by accident**:
|
|
24
|
+
to disobey a henxel you must change the contract — a conscious, reviewable act.
|
|
25
|
+
|
|
26
|
+
Under the hood henxels is **a framework + a growing community library of checks**:
|
|
27
|
+
the contract just lists which checks apply where; the checks are functions, and you can
|
|
28
|
+
add your own in three lines.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## The contract reads like a whiteboard
|
|
33
|
+
|
|
34
|
+
`henxels.yaml` is a list of rules. Each **henxel** is a sentence (which doubles as the
|
|
35
|
+
failure message) plus the **statements** that must all pass. Logic lives inside the
|
|
36
|
+
statements, so the YAML stays a dumb, readable list.
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
settings: # behaviours, not tests
|
|
40
|
+
ask_me_before_staging: true
|
|
41
|
+
confirm_before_push: true
|
|
42
|
+
confirm_before_deleting: { over_lines: 5 }
|
|
43
|
+
|
|
44
|
+
henxels:
|
|
45
|
+
- henxel: "Docs are kebab-case markdown, each with a title and summary"
|
|
46
|
+
in: ./docs # ./docs = this level; ./docs/* = recursive
|
|
47
|
+
allowed_filetypes: .md # a scalar; lists are OR: [.md, .txt]
|
|
48
|
+
filename_casing: kebab-case
|
|
49
|
+
required_frontmatter: [title, summary] # a list here is AND: both required
|
|
50
|
+
|
|
51
|
+
- henxel: "Project config lives only in pyproject.toml"
|
|
52
|
+
forbidden_files: [setup.py, setup.cfg] # no `in:` = whole repo
|
|
53
|
+
|
|
54
|
+
- henxel: "The test suite passes before every commit"
|
|
55
|
+
run_before_commit: "uv run pytest -q"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Browse every statement you can use with **`henxels catalogue`**.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Principles
|
|
63
|
+
|
|
64
|
+
1. **The contract is the single source of structural truth.** If it isn't in
|
|
65
|
+
`henxels.yaml`, it isn't a rule.
|
|
66
|
+
2. **Read it like a document.** A human — or a small model — understands the repo's
|
|
67
|
+
shape without reading a validator.
|
|
68
|
+
3. **A henxel is a test that must pass.** Logic hides in the statement functions; the
|
|
69
|
+
YAML just lists them. Failure returns an *instruction*, not a bare boolean.
|
|
70
|
+
4. **Steer before you stop.** Every henxel says, in words, what to do instead.
|
|
71
|
+
5. **Disobey responsibly.** The only escape hatch is editing the contract.
|
|
72
|
+
6. **Awareness beats blocking** — especially for duplication.
|
|
73
|
+
7. **Beautiful for humans, silent for machines.** Fancy in a terminal, plain in a pipe.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Quick start
|
|
78
|
+
|
|
79
|
+
### Install
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv tool install henxels # puts `henxels` on your PATH (recommended)
|
|
83
|
+
# alternatives:
|
|
84
|
+
# pipx install henxels # same idea, via pipx
|
|
85
|
+
# uvx henxels <cmd> # zero-install, run on demand
|
|
86
|
+
# npm i -g henxels # node-friendly launcher (shims to Python)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
> If henxels lives only inside a project venv, invoke it as `uv run henxels …`. The git
|
|
90
|
+
> hooks resolve it either way, and henxels' own messages always print the form that
|
|
91
|
+
> works in your shell.
|
|
92
|
+
|
|
93
|
+
### Give this to your agent
|
|
94
|
+
|
|
95
|
+
> Install henxels (`uv tool install henxels`, or `pipx install henxels`, or
|
|
96
|
+
> `npm i -D henxels` — it shims to Python; install a prerequisite if one's missing).
|
|
97
|
+
> Run `henxels init`, then tailor `henxels.yaml` to this repo's folders (run
|
|
98
|
+
> `henxels catalogue` to see the statements), and run `henxels sync` and
|
|
99
|
+
> `henxels check --all`.
|
|
100
|
+
|
|
101
|
+
### Or yourself
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
henxels init # scaffold contract + git hooks + AGENTS.md digest
|
|
105
|
+
henxels catalogue # browse the statements you can use
|
|
106
|
+
henxels explain docs/x.md # what governs this path?
|
|
107
|
+
henxels check --all # run the contract
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Custom checks in three lines
|
|
113
|
+
|
|
114
|
+
Need a check that doesn't exist? Write a statement. Drop it in `henxels_checks.py` at
|
|
115
|
+
the repo root (auto-loaded — no config) and use it like any built-in.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from henxels import statement
|
|
119
|
+
|
|
120
|
+
@statement("max_lines", help="source files stay under a line budget")
|
|
121
|
+
def max_lines(param, file, scope): # asks for `file` → per-file, no loop
|
|
122
|
+
if scope.line_count(file) > param:
|
|
123
|
+
return f"split it — keep under {param} lines" # fail = the instruction itself
|
|
124
|
+
```
|
|
125
|
+
```yaml
|
|
126
|
+
- henxel: "No source file exceeds 500 lines"
|
|
127
|
+
in: ./src/*
|
|
128
|
+
max_lines: 500
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Arguments are injected by name (`param`, `scope`, `file`, `root`, `settings`) — take
|
|
132
|
+
only what you need. Return `None`/`True` to pass, or a string instruction to fail.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Example: keeping an LLM wiki from scattering
|
|
137
|
+
|
|
138
|
+
An **LLM wiki** — a markdown knowledge base an agent reads and writes (the pattern
|
|
139
|
+
popularized by Andrej Karpathy) — is a perfect henxels use case. The idea is great, but
|
|
140
|
+
small models drift hard: they write against the conventions, and knowledge that belongs
|
|
141
|
+
in **one** page ends up **scattered across several near-duplicate files**. henxels gives
|
|
142
|
+
the wiki a structure the agent has to follow, and warns it the moment it's about to
|
|
143
|
+
fragment a topic.
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
settings:
|
|
147
|
+
confirm_before_deleting: { over_lines: 10 } # don't lose knowledge to a diff slip
|
|
148
|
+
warn_about_similar_files: # the anti-scatter henxel: nudges the
|
|
149
|
+
above: 0.82 # agent to UPDATE a page, not clone it
|
|
150
|
+
ignore: ["**/index.md"]
|
|
151
|
+
|
|
152
|
+
henxels:
|
|
153
|
+
- henxel: "Every wiki page is kebab-case markdown with findable metadata"
|
|
154
|
+
in: ./pages/*
|
|
155
|
+
filename_casing: kebab-case
|
|
156
|
+
allowed_filetypes: [.md]
|
|
157
|
+
required_frontmatter: [title, tags, updated]
|
|
158
|
+
- henxel: "Pages are clean markdown with no dead links"
|
|
159
|
+
in: ./pages/*
|
|
160
|
+
markdown_lint: true
|
|
161
|
+
links_resolve: true # a custom check (see "Custom checks" above)
|
|
162
|
+
- henxel: "The wiki has a single index"
|
|
163
|
+
in: ./pages
|
|
164
|
+
required_files: index.md
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Because the contract is mirrored into `AGENTS.md`, the agent reads *"one page per topic,
|
|
168
|
+
kebab-case, with these fields"* **before** it writes — and `warn_about_similar_files`
|
|
169
|
+
catches it when it's about to create the fifth slightly-different page about the same
|
|
170
|
+
thing. Strong guidance is exactly what small LLMs need to stay tidy.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Ouroboric by design
|
|
175
|
+
|
|
176
|
+
henxels eats its own tail, and it's the better for it. Its own repo is governed by its
|
|
177
|
+
own `henxels.yaml`, so every feature is dogfooded on the tool before it ships.
|
|
178
|
+
`well_formed_statements` is a check that checks the checks. `markdown_links_absolute`
|
|
179
|
+
guards the README that documents henxels. The pre-commit hook runs `henxels check` —
|
|
180
|
+
henxels gating henxels. The contract even mirrors itself into `AGENTS.md` to steer the
|
|
181
|
+
agent that edits the contract.
|
|
182
|
+
|
|
183
|
+
The tail-eating *is* the test.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Guards & bless
|
|
188
|
+
|
|
189
|
+
`settings` can guard hard-to-undo actions. They don't forbid — they make you mean it:
|
|
190
|
+
|
|
191
|
+
```text
|
|
192
|
+
$ git push
|
|
193
|
+
✗ Push is guarded — a push is hard to take back
|
|
194
|
+
→ henxels bless push (then push again)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`henxels bless push` mints a one-time token bound to the exact commit. The delete guard
|
|
198
|
+
covers deleted files **and** net-removed lines (diff-edit mistakes lose rows), released
|
|
199
|
+
by `henxels bless delete`.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Contributing — the agentic era
|
|
204
|
+
|
|
205
|
+
henxels thrives on contributions. **We'd rather get a ready-to-merge PR than an issue.**
|
|
206
|
+
If you (or your agent) write a check that's *reusable* — general, not tied to your repo —
|
|
207
|
+
contribute it upstream: `henxels contribute`. Quality gates (ruff + the test suite) run
|
|
208
|
+
in pre-commit and CI, so a green local run means your PR is merge-ready. See
|
|
209
|
+
[`CONTRIBUTING.md`](https://github.com/benquemax/henxels/blob/main/CONTRIBUTING.md).
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Commands
|
|
214
|
+
|
|
215
|
+
| Command | Purpose |
|
|
216
|
+
|---------|---------|
|
|
217
|
+
| `henxels init` | scaffold contract + hooks + digest |
|
|
218
|
+
| `henxels check [--all\|--staged] […]` | run the contract |
|
|
219
|
+
| `henxels explain <path>` | what governs this location |
|
|
220
|
+
| `henxels catalogue` | browse the statements you can use |
|
|
221
|
+
| `henxels create-new-statement <name>` | scaffold a custom statement |
|
|
222
|
+
| `henxels contribute [name]` | how to upstream a reusable statement |
|
|
223
|
+
| `henxels bless <push\|delete>` | consciously override a guard |
|
|
224
|
+
| `henxels sync` / `henxels doctor` | refresh the digest / check the setup |
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""henxels — external structure that gives shape to your project.
|
|
2
|
+
|
|
3
|
+
Custom checks register with the ``@statement`` decorator:
|
|
4
|
+
|
|
5
|
+
from henxels import statement
|
|
6
|
+
|
|
7
|
+
@statement("max_lines")
|
|
8
|
+
def max_lines(limit, scope):
|
|
9
|
+
return [f"{f} is too long" for f in scope.files
|
|
10
|
+
if scope.line_count(f) > limit]
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from henxels.statements import Scope, as_list, statement
|
|
14
|
+
|
|
15
|
+
__version__ = "0.2.0"
|
|
16
|
+
|
|
17
|
+
__all__ = ["statement", "Scope", "as_list", "__version__"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Conscious-override tokens — the elegant escape hatch for guards.
|
|
2
|
+
|
|
3
|
+
A guard blocks a destructive reflex (push, deleting lines/files). To proceed you run
|
|
4
|
+
a deliberate, non-standard verb — ``henxels bless push`` — which mints a one-time
|
|
5
|
+
token. The git hook then *consumes* it. A reflexive retry can't reuse the token
|
|
6
|
+
because it's bound to a fingerprint (the exact SHA being pushed, or the exact set of
|
|
7
|
+
deletions) and expires quickly.
|
|
8
|
+
|
|
9
|
+
Tokens live under ``.git/henxels/`` so they're repo-local and never committed.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
DEFAULT_TTL = 600 # seconds — a bless is a *recent* conscious act, not a standing grant
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _store_dir(root: Path | str) -> Path:
|
|
22
|
+
return Path(root) / ".git" / "henxels"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _token_path(root: Path | str, action: str) -> Path:
|
|
26
|
+
return _store_dir(root) / f"bless-{action}.json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def bless(root: Path | str, action: str, fingerprint: str, ttl: int = DEFAULT_TTL, now: float | None = None) -> Path:
|
|
30
|
+
"""Mint a one-time token for ``action`` bound to ``fingerprint``."""
|
|
31
|
+
now = time.time() if now is None else now
|
|
32
|
+
store = _store_dir(root)
|
|
33
|
+
store.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
path = _token_path(root, action)
|
|
35
|
+
path.write_text(
|
|
36
|
+
json.dumps({"fingerprint": fingerprint, "expires_at": now + ttl}),
|
|
37
|
+
encoding="utf-8",
|
|
38
|
+
)
|
|
39
|
+
return path
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_blessed(root: Path | str, action: str, fingerprint: str, now: float | None = None) -> bool:
|
|
43
|
+
"""True if a matching, unexpired token exists (without consuming it)."""
|
|
44
|
+
now = time.time() if now is None else now
|
|
45
|
+
path = _token_path(root, action)
|
|
46
|
+
if not path.is_file():
|
|
47
|
+
return False
|
|
48
|
+
try:
|
|
49
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
50
|
+
except (ValueError, OSError):
|
|
51
|
+
return False
|
|
52
|
+
if data.get("fingerprint") != fingerprint:
|
|
53
|
+
return False
|
|
54
|
+
if float(data.get("expires_at", 0)) < now:
|
|
55
|
+
return False
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def consume(root: Path | str, action: str, fingerprint: str, now: float | None = None) -> bool:
|
|
60
|
+
"""Verify a matching token and delete it. Returns True if it was valid."""
|
|
61
|
+
ok = is_blessed(root, action, fingerprint, now=now)
|
|
62
|
+
# Always clear the token file: a used token is spent, a stale/mismatched one is junk.
|
|
63
|
+
_token_path(root, action).unlink(missing_ok=True)
|
|
64
|
+
return ok
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def clear(root: Path | str, action: str) -> None:
|
|
68
|
+
_token_path(root, action).unlink(missing_ok=True)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Naming conventions — the closed set used by the `casing` statement and Scope."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
# Keys are the values accepted by the `casing:` statement (and the schema enum).
|
|
8
|
+
# snake_case / SCREAMING_SNAKE_CASE permit leading underscores — `_private.py` and
|
|
9
|
+
# `_helpers.py` are idiomatic Python, not naming violations. (Dunder files like
|
|
10
|
+
# __init__.py are exempted separately, in is_dunder.)
|
|
11
|
+
NAMING_CONVENTIONS: dict[str, str] = {
|
|
12
|
+
"snake_case": r"^_*[a-z0-9]+(_[a-z0-9]+)*$",
|
|
13
|
+
"kebab-case": r"^[a-z0-9]+(-[a-z0-9]+)*$",
|
|
14
|
+
"camelCase": r"^[a-z][a-zA-Z0-9]*$",
|
|
15
|
+
"PascalCase": r"^[A-Z][a-zA-Z0-9]*$",
|
|
16
|
+
"SCREAMING_SNAKE_CASE": r"^_*[A-Z0-9]+(_[A-Z0-9]+)*$",
|
|
17
|
+
"any": r".*",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_dunder(base: str) -> bool:
|
|
22
|
+
"""Dunder files (__init__, __main__) are language-mandated, not style choices."""
|
|
23
|
+
return len(base) > 4 and base.startswith("__") and base.endswith("__")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def matches(base: str, convention: str) -> bool:
|
|
27
|
+
pattern = NAMING_CONVENTIONS.get(convention)
|
|
28
|
+
if pattern is None:
|
|
29
|
+
return True
|
|
30
|
+
return not base or is_dunder(base) or bool(re.match(pattern, base))
|