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.
Files changed (79) hide show
  1. henxels-0.3.0/LICENSE +21 -0
  2. henxels-0.3.0/PKG-INFO +259 -0
  3. henxels-0.3.0/README.md +228 -0
  4. henxels-0.3.0/henxels/__init__.py +17 -0
  5. henxels-0.3.0/henxels/__main__.py +8 -0
  6. henxels-0.3.0/henxels/bless.py +68 -0
  7. henxels-0.3.0/henxels/casing.py +30 -0
  8. henxels-0.3.0/henxels/catalogue.py +128 -0
  9. henxels-0.3.0/henxels/cli.py +366 -0
  10. henxels-0.3.0/henxels/commands.py +30 -0
  11. henxels-0.3.0/henxels/contract.py +152 -0
  12. henxels-0.3.0/henxels/diffinfo.py +53 -0
  13. henxels-0.3.0/henxels/digest.py +85 -0
  14. henxels-0.3.0/henxels/doctor.py +58 -0
  15. henxels-0.3.0/henxels/engine/__init__.py +1 -0
  16. henxels-0.3.0/henxels/engine/discover.py +74 -0
  17. henxels-0.3.0/henxels/engine/gitinfo.py +95 -0
  18. henxels-0.3.0/henxels/engine/report.py +79 -0
  19. henxels-0.3.0/henxels/explain.py +53 -0
  20. henxels-0.3.0/henxels/filesize.py +98 -0
  21. henxels-0.3.0/henxels/findings.py +32 -0
  22. henxels-0.3.0/henxels/guard.py +80 -0
  23. henxels-0.3.0/henxels/hookrun.py +90 -0
  24. henxels-0.3.0/henxels/hooks.py +82 -0
  25. henxels-0.3.0/henxels/invocation.py +26 -0
  26. henxels-0.3.0/henxels/locations.py +96 -0
  27. henxels-0.3.0/henxels/runner.py +102 -0
  28. henxels-0.3.0/henxels/scaffold.py +130 -0
  29. henxels-0.3.0/henxels/schema/__init__.py +9 -0
  30. henxels-0.3.0/henxels/schema/henxels.schema.json +131 -0
  31. henxels-0.3.0/henxels/settings.py +61 -0
  32. henxels-0.3.0/henxels/similarity.py +112 -0
  33. henxels-0.3.0/henxels/statements/__init__.py +25 -0
  34. henxels-0.3.0/henxels/statements/builtins/__init__.py +33 -0
  35. henxels-0.3.0/henxels/statements/builtins/_helpers.py +44 -0
  36. henxels-0.3.0/henxels/statements/builtins/commands.py +18 -0
  37. henxels-0.3.0/henxels/statements/builtins/content.py +167 -0
  38. henxels-0.3.0/henxels/statements/builtins/history.py +52 -0
  39. henxels-0.3.0/henxels/statements/builtins/links.py +100 -0
  40. henxels-0.3.0/henxels/statements/builtins/meta.py +50 -0
  41. henxels-0.3.0/henxels/statements/builtins/naming.py +35 -0
  42. henxels-0.3.0/henxels/statements/builtins/security.py +39 -0
  43. henxels-0.3.0/henxels/statements/builtins/size.py +17 -0
  44. henxels-0.3.0/henxels/statements/builtins/structure.py +92 -0
  45. henxels-0.3.0/henxels/statements/registry.py +95 -0
  46. henxels-0.3.0/henxels/statements/scope.py +85 -0
  47. henxels-0.3.0/henxels/util/__init__.py +1 -0
  48. henxels-0.3.0/henxels/util/glob.py +55 -0
  49. henxels-0.3.0/henxels.egg-info/PKG-INFO +259 -0
  50. henxels-0.3.0/henxels.egg-info/SOURCES.txt +77 -0
  51. henxels-0.3.0/henxels.egg-info/dependency_links.txt +1 -0
  52. henxels-0.3.0/henxels.egg-info/entry_points.txt +2 -0
  53. henxels-0.3.0/henxels.egg-info/requires.txt +9 -0
  54. henxels-0.3.0/henxels.egg-info/top_level.txt +1 -0
  55. henxels-0.3.0/pyproject.toml +72 -0
  56. henxels-0.3.0/setup.cfg +4 -0
  57. henxels-0.3.0/tests/test_bless.py +35 -0
  58. henxels-0.3.0/tests/test_catalogue.py +52 -0
  59. henxels-0.3.0/tests/test_cli.py +84 -0
  60. henxels-0.3.0/tests/test_contract.py +102 -0
  61. henxels-0.3.0/tests/test_digest.py +38 -0
  62. henxels-0.3.0/tests/test_discover.py +39 -0
  63. henxels-0.3.0/tests/test_explain.py +41 -0
  64. henxels-0.3.0/tests/test_filesize.py +56 -0
  65. henxels-0.3.0/tests/test_glob.py +41 -0
  66. henxels-0.3.0/tests/test_guards.py +67 -0
  67. henxels-0.3.0/tests/test_history.py +96 -0
  68. henxels-0.3.0/tests/test_hooks.py +44 -0
  69. henxels-0.3.0/tests/test_hooks_integration.py +64 -0
  70. henxels-0.3.0/tests/test_init_doctor.py +51 -0
  71. henxels-0.3.0/tests/test_invocation.py +20 -0
  72. henxels-0.3.0/tests/test_locations.py +55 -0
  73. henxels-0.3.0/tests/test_npm_wrapper.py +36 -0
  74. henxels-0.3.0/tests/test_report.py +66 -0
  75. henxels-0.3.0/tests/test_runner_v2.py +114 -0
  76. henxels-0.3.0/tests/test_schema.py +33 -0
  77. henxels-0.3.0/tests/test_settings.py +25 -0
  78. henxels-0.3.0/tests/test_similarity.py +46 -0
  79. 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.
@@ -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,8 @@
1
+ """Enable ``python -m henxels`` (used as a hook fallback when the script isn't on PATH)."""
2
+
3
+ import sys
4
+
5
+ from henxels.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -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))