git-agent-ratchet 1.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.
Files changed (46) hide show
  1. git_agent_ratchet-1.1.0/.gitignore +54 -0
  2. git_agent_ratchet-1.1.0/.pre-commit-hooks.yaml +41 -0
  3. git_agent_ratchet-1.1.0/AGENTS.md +495 -0
  4. git_agent_ratchet-1.1.0/LICENSE +21 -0
  5. git_agent_ratchet-1.1.0/PKG-INFO +521 -0
  6. git_agent_ratchet-1.1.0/README.md +493 -0
  7. git_agent_ratchet-1.1.0/docs/TODO.md +65 -0
  8. git_agent_ratchet-1.1.0/docs/spec.md +211 -0
  9. git_agent_ratchet-1.1.0/examples/downstream/README.md +70 -0
  10. git_agent_ratchet-1.1.0/git_agent_ratchet/__init__.py +5 -0
  11. git_agent_ratchet-1.1.0/git_agent_ratchet/__main__.py +3 -0
  12. git_agent_ratchet-1.1.0/git_agent_ratchet/_version.py +1 -0
  13. git_agent_ratchet-1.1.0/git_agent_ratchet/baseline.py +82 -0
  14. git_agent_ratchet-1.1.0/git_agent_ratchet/cli.py +53 -0
  15. git_agent_ratchet-1.1.0/git_agent_ratchet/hooks/__init__.py +1 -0
  16. git_agent_ratchet-1.1.0/git_agent_ratchet/hooks/anti_bypass.py +66 -0
  17. git_agent_ratchet-1.1.0/git_agent_ratchet/hooks/deny_agent_chatter.py +57 -0
  18. git_agent_ratchet-1.1.0/git_agent_ratchet/hooks/max_file_lines.py +128 -0
  19. git_agent_ratchet-1.1.0/git_agent_ratchet/hooks/no_duplicate_helpers.py +135 -0
  20. git_agent_ratchet-1.1.0/git_agent_ratchet/paths.py +18 -0
  21. git_agent_ratchet-1.1.0/git_agent_ratchet/py.typed +0 -0
  22. git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/__init__.py +1 -0
  23. git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/agent_chatter.py +79 -0
  24. git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/anti_bypass.py +100 -0
  25. git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/duplicate_helpers.py +89 -0
  26. git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/extractors/__init__.py +40 -0
  27. git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/extractors/csharp_ext.py +51 -0
  28. git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/extractors/python_ext.py +49 -0
  29. git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/extractors/typescript_ext.py +67 -0
  30. git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/max_file_lines.py +84 -0
  31. git_agent_ratchet-1.1.0/pyproject.toml +101 -0
  32. git_agent_ratchet-1.1.0/tests/__init__.py +1 -0
  33. git_agent_ratchet-1.1.0/tests/test_agent_chatter.py +120 -0
  34. git_agent_ratchet-1.1.0/tests/test_anti_bypass.py +105 -0
  35. git_agent_ratchet-1.1.0/tests/test_baseline.py +103 -0
  36. git_agent_ratchet-1.1.0/tests/test_cli.py +116 -0
  37. git_agent_ratchet-1.1.0/tests/test_duplicate_helpers.py +145 -0
  38. git_agent_ratchet-1.1.0/tests/test_extractor_csharp.py +199 -0
  39. git_agent_ratchet-1.1.0/tests/test_extractor_python.py +94 -0
  40. git_agent_ratchet-1.1.0/tests/test_extractor_typescript.py +180 -0
  41. git_agent_ratchet-1.1.0/tests/test_hooks_anti_bypass.py +77 -0
  42. git_agent_ratchet-1.1.0/tests/test_hooks_deny_agent_chatter.py +56 -0
  43. git_agent_ratchet-1.1.0/tests/test_hooks_max_file_lines.py +161 -0
  44. git_agent_ratchet-1.1.0/tests/test_hooks_no_duplicate_helpers.py +124 -0
  45. git_agent_ratchet-1.1.0/tests/test_max_file_lines.py +137 -0
  46. git_agent_ratchet-1.1.0/tests/test_paths.py +28 -0
@@ -0,0 +1,54 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ .Python
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ wheels/
20
+ share/python-wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+ MANIFEST
25
+
26
+ # Virtual environments
27
+ .venv/
28
+ venv/
29
+ ENV/
30
+ env/
31
+
32
+ # Test / coverage
33
+ .pytest_cache/
34
+ .coverage
35
+ .coverage.*
36
+ htmlcov/
37
+ coverage.xml
38
+ *.cover
39
+ .hypothesis/
40
+
41
+ # Tooling caches
42
+ .ruff_cache/
43
+ .mypy_cache/
44
+ .tox/
45
+
46
+ # IDE
47
+ .idea/
48
+ .vscode/
49
+ *.swp
50
+ *.swo
51
+
52
+ # OS
53
+ .DS_Store
54
+ Thumbs.db
@@ -0,0 +1,41 @@
1
+ - id: ratchet-no-duplicate-helpers
2
+ name: git-agent-ratchet | no duplicate private helpers
3
+ description: >-
4
+ Fail when the count of duplicate private helper functions across the target
5
+ directory exceeds the recorded baseline. Auto-shrinks the baseline on cleanup.
6
+ entry: ratchet-no-duplicate-helpers
7
+ language: python
8
+ pass_filenames: false
9
+ always_run: true
10
+ stages: [pre-commit, manual]
11
+
12
+ - id: ratchet-deny-agent-chatter
13
+ name: git-agent-ratchet | deny agent chatter
14
+ description: >-
15
+ Block conversational agent preamble and postscript artifacts from being
16
+ committed to text files.
17
+ entry: ratchet-deny-agent-chatter
18
+ language: python
19
+ types: [text]
20
+ stages: [pre-commit, manual]
21
+
22
+ - id: ratchet-anti-bypass
23
+ name: git-agent-ratchet | anti-bypass guard
24
+ description: >-
25
+ Block automated processes from mutating protected ratchet configuration files
26
+ unless HUMAN_RATCHET_BYPASS_KEY is set in the environment.
27
+ entry: ratchet-anti-bypass
28
+ language: python
29
+ pass_filenames: true
30
+ stages: [pre-commit, manual]
31
+
32
+ - id: ratchet-max-file-lines
33
+ name: git-agent-ratchet | max file lines
34
+ description: >-
35
+ Fail when the total line overage across over-sized source files exceeds the
36
+ recorded baseline. Auto-shrinks the baseline when files contract.
37
+ entry: ratchet-max-file-lines
38
+ language: python
39
+ pass_filenames: false
40
+ always_run: true
41
+ stages: [pre-commit, manual]
@@ -0,0 +1,495 @@
1
+ # AGENTS.md -- git-agent-ratchet Codebase Guide for AI Agents
2
+
3
+ ---
4
+
5
+ ### Core coding philosophy
6
+
7
+ > "The code you write makes you a programmer.
8
+ > The code you delete makes you a good one.
9
+ > The code you don't have to write makes you a great one."
10
+ > -- Mario Fusco
11
+
12
+ Lines of code are a cost, not an asset. The best contribution is often a
13
+ smaller diff than you arrived expecting to write -- a delete, a consolidation,
14
+ or a one-line addition to an existing helper instead of a new sibling. This
15
+ repo enforces that bias mechanically; see "Mechanical enforcement" below.
16
+
17
+ #### Before-you-write checklist (NON-NEGOTIABLE)
18
+
19
+ Run these four checks before writing any helper, utility, or "small" function:
20
+
21
+ 1. **Grep `git_agent_ratchet/` for the verb.** `grep -rn "def <verb>" git_agent_ratchet/`
22
+ -- if anything already does this, use it. If a near-miss exists, extend it
23
+ -- do not fork.
24
+ 2. **Grep the whole repo for the function name you're about to type.**
25
+ `grep -rn "def _your_name_here" .` -- if it exists outside `tests/`, that's
26
+ the existing implementation. Import it or extract both to a shared module
27
+ under `git_agent_ratchet/`.
28
+ 3. **Check the red-flag prefixes.** If your function starts with `_run_*`,
29
+ `_safe_*`, `_resolve_*`, `_load_*_or_default`, `_no_prompt_*`, `_retry_*`,
30
+ `_copy_*`, `_walk_*`, `_atomic_*`, `_canonical_*` -- stop. These are the
31
+ exact patterns that get forked. Check `git_agent_ratchet/` again, harder.
32
+ 4. **Run Ratchet A locally.**
33
+ `uv run git-agent-ratchet no-duplicate-helpers --dir git_agent_ratchet --baseline config/ratchets/duplicates.json`.
34
+ It exits non-zero with a per-name report if your change introduces or grows
35
+ a duplicate. The pre-commit hook wired in `.pre-commit-config.yaml` runs
36
+ the same check on every commit; do not rely on CI for it.
37
+
38
+ If you find yourself about to write the same helper twice in one session, the
39
+ second occurrence is the signal to extract immediately. Do not "do it once
40
+ more and clean up later". Later does not arrive.
41
+
42
+ #### Mechanical enforcement (this repo dogfoods itself)
43
+
44
+ This codebase ships the very ratchets that guard it. Every prose rule below
45
+ that ends in "NON-NEGOTIABLE" must map to a mechanical gate in
46
+ [.pre-commit-config.yaml](.pre-commit-config.yaml). If a rule cannot be
47
+ mechanically enforced, it goes in the "Known soft rules" section of
48
+ [DEVELOPERS.md](DEVELOPERS.md) so the gap is public.
49
+
50
+ | Rule | Programmatic gate | Source |
51
+ | --- | --- | --- |
52
+ | No duplicate private helpers | `ratchet-no-duplicate-helpers` | [git_agent_ratchet/hooks/no_duplicate_helpers.py](git_agent_ratchet/hooks/no_duplicate_helpers.py) |
53
+ | No agent chatter in any file | `ratchet-deny-agent-chatter` | [git_agent_ratchet/hooks/deny_agent_chatter.py](git_agent_ratchet/hooks/deny_agent_chatter.py) |
54
+ | No agent self-bypass of the ratchets | `ratchet-anti-bypass` | [git_agent_ratchet/hooks/anti_bypass.py](git_agent_ratchet/hooks/anti_bypass.py) |
55
+ | Per-file line count <= 350 (sum of overages) | `ratchet-max-file-lines` | [git_agent_ratchet/hooks/max_file_lines.py](git_agent_ratchet/hooks/max_file_lines.py) |
56
+ | Lint + format clean | `ruff` (check + format --check) | `pyproject.toml [tool.ruff]` |
57
+ | Trailing whitespace / EOF / line endings / merge markers / large files | `pre-commit-hooks` | `.pre-commit-config.yaml` |
58
+
59
+ - The baseline registry lives at [config/ratchets/duplicates.json](config/ratchets/duplicates.json).
60
+ It is **allowed to shrink** (cleanups) but never grow. Each cleanup gets its
61
+ own commit (`cleanup: extract _name -> git_agent_ratchet/...`) and the
62
+ ratchet rewrites the registry with the smaller count, staging the diff.
63
+ - Do not add an ignore-list to the scanner. The fix is always extraction.
64
+ - Do not edit `config/ratchets/duplicates.json` by hand to make a commit pass.
65
+ Ratchet C will block it; that is the point.
66
+
67
+ ---
68
+
69
+ ## What is this repo?
70
+
71
+ `git-agent-ratchet` is a small Python package that ships three pre-commit hooks
72
+ designed to keep LLM coding agents on rails. The premise (full version in
73
+ [docs/spec.md](docs/spec.md)): prose instructions in `AGENTS.md` or `CLAUDE.md`
74
+ experience silent rule erosion over long multi-turn context windows; agents
75
+ follow the path of least technical resistance. The fix is to convert each
76
+ prose rule into a deterministic gate at commit time, so the *cost profile* of
77
+ breaking the rule changes -- the rule itself does not need to be re-asserted
78
+ every turn.
79
+
80
+ The three hooks:
81
+
82
+ 1. **Ratchet A -- `no-duplicate-helpers`.** AST scan for private/semi-private
83
+ top-level functions that appear in two or more files. Count is tracked in a
84
+ JSON baseline; the count may shrink or stay flat, never grow.
85
+ 2. **Ratchet B -- `deny-agent-chatter`.** Regex scan for conversational
86
+ preamble/postscript leaking into source files ("Sure, I can help with...", <!-- ratchet-allow: agent_chatter -->
87
+ "As an AI, ...", "I have successfully updated...", "Now let me check the <!-- ratchet-allow: agent_chatter -->
88
+ docs..."). Any match is a hard block.
89
+ 3. **Ratchet C -- `anti-bypass`.** Blocks mutations to the ratchet config
90
+ files themselves unless `HUMAN_RATCHET_BYPASS_KEY` is set in the
91
+ environment. Detects common automated-agent env signatures
92
+ (`CURSOR_AGENT`, `CLAUDECODE`, `AIDER`, `COPILOT_AGENT`, ...).
93
+
94
+ The full design contract is [docs/spec.md](docs/spec.md). The CLI surface is
95
+ in [git_agent_ratchet/cli.py](git_agent_ratchet/cli.py).
96
+
97
+ ---
98
+
99
+ ## Vibe Coding Rules (Mandatory for all AI agents)
100
+
101
+ ### Prime directive (READ FIRST, OVERRIDES EVERYTHING BELOW)
102
+ **Do the right thing, not the expedient thing.** When a clean design and a
103
+ quick hack both reach green tests, pick the clean design. When fixing one test
104
+ the right way would require updating fifteen other tests, update the fifteen
105
+ tests -- do not add a back-compat shim, a transitional bridge, a "for now"
106
+ indirection, or a `_current_*()` helper that hides the legacy pattern. Those
107
+ shortcuts calcify. They get committed with TODO comments that never get
108
+ resolved, and the next agent inherits two ways to do the same thing forever.
109
+
110
+ Concrete tells that you are about to take the expedient path:
111
+ - "I'll add a fallback so legacy callers keep working" -- no, migrate the
112
+ legacy callers.
113
+ - "Tests monkeypatch the old constant; I'll make the new code read both" --
114
+ no, update the tests.
115
+ - "This is a bridge until the wider refactor lands" -- the bridge becomes
116
+ permanent. Land the refactor now or do not introduce the new abstraction yet.
117
+ - "Touching 15 files for one design change is too much" -- if the design is
118
+ right, touching 15 files is what it costs. Pay it.
119
+ - A `# TODO: remove once X migrates` comment in a commit that does not also
120
+ do X.
121
+
122
+ If the right thing is genuinely too large for one commit, **stop and say so**
123
+ -- do not ship the expedient half. Either reduce scope (pick a smaller
124
+ right-shaped change) or split into a sequence of right-shaped commits, each
125
+ individually principled.
126
+
127
+ ### File discipline
128
+ - **Max 350 lines per file.** Split aggressively. Enforced by Ratchet D
129
+ (`ratchet-max-file-lines`); the baseline lives at
130
+ `config/ratchets/file_lines.json` and is allowed to shrink, never grow.
131
+ - **Close irrelevant files.** Only keep the current file, its test, and
132
+ related module visible.
133
+
134
+ ### Test-driven workflow (NON-NEGOTIABLE)
135
+ - **Tests are the only way we ship.** Every hook, ratchet, and CLI surface
136
+ needs tests.
137
+ - **Run tests after every generation.** `uv run pytest -q`. Even minor edits
138
+ can introduce side effects -- regex changes in
139
+ [git_agent_ratchet/ratchets/agent_chatter.py](git_agent_ratchet/ratchets/agent_chatter.py)
140
+ are the canonical footgun.
141
+ - **Never skip or disable a test to make CI pass.** Fix the code, not the
142
+ test.
143
+ - **Bug fixes require a regression test. No exceptions.**
144
+ - Every fixed bug gets a test in `tests/test_<module>_regressions.py` (one
145
+ file per module).
146
+ - The test name and docstring must describe the bug in plain English: what
147
+ went wrong, what the impact was, what the fix is.
148
+ - The test must FAIL against the un-fixed code and PASS against the fix.
149
+ Confirm both directions before committing. **Do not** use `git stash` to
150
+ verify this (see Multi-agent collaboration); restore the buggy line
151
+ manually, run the test, re-apply the fix.
152
+ - Do not delete regression tests during refactors. They pin subtle
153
+ behaviour that is not obvious from the API surface.
154
+ - **Always report bugs and failures, even ones you do not fix in this run.**
155
+ If you notice a bug, a flaky test, an unexpected failure, suspicious
156
+ behaviour, or a latent footgun while doing other work, add an entry to the
157
+ `## Known bugs` section of [docs/TODO.md](docs/TODO.md) before you finish
158
+ the turn. **We never silently drop bugs.** Each entry must describe: (1)
159
+ what you observed, (2) where (file + symbol or test name), (3) impact /
160
+ blast radius if known, (4) whether it was fixed in this run or left for
161
+ later.
162
+
163
+ ### Pre-commit hooks (NON-NEGOTIABLE)
164
+ - **Pre-commit runs on every commit.** Never use `--no-verify`.
165
+ - **Hooks enforce:** the three ratchets above, plus trailing whitespace,
166
+ end-of-file, YAML/JSON validity, ruff lint+format.
167
+ - **Install with `make setup`.** If you bootstrapped with `uv sync` directly,
168
+ run `uv run pre-commit install` once -- otherwise the hooks are silently
169
+ bypassed and lint debt accumulates.
170
+
171
+ ### Git discipline
172
+ - **One commit = one meaningful unit of work.** Scoped, validated, tested.
173
+ - **Review every diff.** Do not blindly accept generated code.
174
+ - **Never edit the ratchet baseline by hand to pass a commit.** Ratchet C
175
+ will block it. The correct response to a tripped ratchet is to remove the
176
+ duplication or chatter, not to grow the registry.
177
+
178
+ ### Doc discipline (NON-NEGOTIABLE)
179
+ Every shipped feature must update the three user-facing surfaces in the same
180
+ commit (or follow-up commit before the next feature lands). Doc drift here is
181
+ treated like a failing test.
182
+ - **[docs/TODO.md](docs/TODO.md)** -- record what shipped, or open a new
183
+ entry. Mark prior `[ ]` items `[x]` with the commit / artefact reference.
184
+ - **[README.md](README.md)** -- if the change adds, removes, or renames a
185
+ hook, CLI subcommand, or config knob a user passes in
186
+ `.pre-commit-config.yaml`, update the usage section in the same commit.
187
+ - **[AGENTS.md](AGENTS.md)** (this file) and **[docs/spec.md](docs/spec.md)**
188
+ -- if you change a ratchet's gate behaviour, update the "Mechanical
189
+ enforcement" table here and the corresponding section in the spec. The
190
+ spec is the contract; this file is the agent-facing index.
191
+
192
+ If a refactor is purely internal (file split, helper extraction, test
193
+ reshuffle), only TODO.md needs to acknowledge it.
194
+
195
+ ### Multi-agent collaboration (READ THIS FIRST)
196
+ This repo runs **multiple AI agents concurrently in the same worktree by
197
+ design**. The operator does not always fork to a separate clone; they trade
198
+ isolation for velocity. You will frequently find another agent has edited
199
+ files between your `read_file` and your `commit`. Plan accordingly.
200
+
201
+ **Rules for every agent (NON-NEGOTIABLE):**
202
+ - **Never `git stash`.** Stash interacts catastrophically with concurrent
203
+ worktree edits, pre-commit's own auto-stashing, and Windows file locks. We
204
+ have repeatedly lost staged work and accumulated phantom stash entries when
205
+ agents tried to isolate their changes via stash. Instead: **commit your
206
+ work directly**, even if it's a WIP commit -- a WIP commit can always be
207
+ amended (`git commit --amend`) or squashed later, and it survives every
208
+ failure mode that destroys a stash. If you must verify a regression test
209
+ FAILS against unfixed code, restore the buggy line manually in the editor;
210
+ do not `git stash` the fix.
211
+ - **Always `git status --short` before staging or committing.** If you see
212
+ files you did not touch in the staged set, the previous agent left them
213
+ staged for their next commit -- unstage them with `git restore --staged
214
+ <file>` before you `git commit`.
215
+ - **Stage explicitly with named paths.** Never `git add -A` or `git add .`.
216
+ Always `git add <specific-files-you-touched>`.
217
+ - **Verify the staged set immediately before every commit.** `git diff
218
+ --cached --name-only` MUST list ONLY files you authored this turn -- no
219
+ more, no less. If it doesn't match exactly, `git restore --staged
220
+ <unexpected-file>` before you `git commit`.
221
+ - **Stage as late as possible.** Run your edits, run your tests, then `git
222
+ add` + `git diff --cached --name-only` + `git commit` as a tight three-step
223
+ block.
224
+ - **Watch the pre-commit auto-stash hijack window.** Pre-commit stashes
225
+ unstaged files before running hooks, then restores them. If a sibling runs
226
+ `git add` DURING that window, the restore + their stage lets THEIR files
227
+ end up committed with YOUR message. Before every `git commit`, verify
228
+ `git status --short` shows ALL files staged-only (capital M in the left
229
+ column, blank in the right). If anything has a right-column M, re-add
230
+ explicitly first.
231
+ - **Do not run mass refactors (`ruff --fix .`, sweeping renames, format-the-
232
+ world commits) while another agent is active.** Schedule them for a quiet
233
+ window.
234
+ - **Read commits that landed during your turn.** `git log --oneline -5` at
235
+ the start of any non-trivial action. The other agent may have already
236
+ fixed the bug you were about to fix.
237
+
238
+ ### Code reuse (NON-NEGOTIABLE)
239
+ - **Always check existing modules before writing a new helper.** Baseline
240
+ load/save belongs in [git_agent_ratchet/baseline.py](git_agent_ratchet/baseline.py).
241
+ Anything that walks Python source belongs near
242
+ [git_agent_ratchet/ratchets/duplicate_helpers.py](git_agent_ratchet/ratchets/duplicate_helpers.py).
243
+ Regex scanners belong near
244
+ [git_agent_ratchet/ratchets/agent_chatter.py](git_agent_ratchet/ratchets/agent_chatter.py).
245
+ - **If you find yourself writing a `_load_baseline` / `_normalise_path` /
246
+ `_iter_py_files` / `_signature_for` helper, stop.** Check the modules
247
+ above. If a helper with that purpose already exists, use it. If a
248
+ near-miss exists, extend it rather than forking a new one.
249
+ - **When you spot a duplicate during unrelated work, file it.** Add a
250
+ `Known duplicates` entry to [docs/TODO.md](docs/TODO.md). Don't silently
251
+ leave the duplication for the next agent -- Ratchet A will catch it but
252
+ TODO.md captures *why* the duplication appeared so the consolidation
253
+ doesn't just push it back down on the next iteration.
254
+
255
+ ### Code design discipline (NON-NEGOTIABLE)
256
+ Pythonic, testable code by default.
257
+
258
+ - **Prefer objects over module-mutable globals when state has identity.**
259
+ The `Baseline` dataclass in [git_agent_ratchet/baseline.py](git_agent_ratchet/baseline.py)
260
+ is the worked example -- it owns a path and a dict, and tests instantiate
261
+ their own with `tmp_path`. No `monkeypatch.setattr(module, "BASELINE_PATH",
262
+ tmp)` ever.
263
+ - **Dependency injection over monkeypatch.** Functions that read env take it
264
+ as a parameter with a default (`def detect_agent_signal(env: dict[str,
265
+ str] | None = None)`). If you find yourself writing
266
+ `monkeypatch.setattr(some_module, "SOME_CONSTANT", x)`, the production
267
+ code has a design bug -- fix the seam, not the test.
268
+ - **`@dataclass(frozen=True)` for settings and value objects.** `DuplicateHelper`,
269
+ `ChatterMatch`, `BypassDecision` are the canonical examples -- frozen by
270
+ default, mutation is a code smell.
271
+ - **Context managers for resource lifecycles.** If anything starts holding a
272
+ file lock or temp dir, wrap it with `__enter__` / `__exit__` or
273
+ `@contextlib.contextmanager`.
274
+ - **Class only when state + behaviour bind.** `Baseline` is a class because
275
+ load / save / get / set share a path + dict. A scanner that takes a root
276
+ and returns a list stays a free function.
277
+ - **Anti-patterns to refuse:**
278
+ - Module-level `_CACHE = {}` plus `def get(...)` plus `def clear_cache()`.
279
+ That's a class without the class -- write the class.
280
+ - Two helpers that differ only by which directory they walk. That's one
281
+ function with a parameter.
282
+ - "Helper" that takes the same first three arguments at every call site.
283
+ Those are constructor params.
284
+ - Test that monkeypatches a production module attr. Production code has a
285
+ missing seam -- fix the seam.
286
+
287
+ ### Package management (NON-NEGOTIABLE)
288
+ - **NEVER use `pip install`.** Always use `uv add` (or `uv add --dev` for dev
289
+ deps).
290
+ - **`uv sync`** to install from lockfile. **`uv run`** to execute commands.
291
+ - **No `src/` layout.** Package lives at `git_agent_ratchet/` at the repo root.
292
+ Build backend is hatchling (see `pyproject.toml`).
293
+
294
+ ### Logging standard (NON-NEGOTIABLE)
295
+ - **Use Python's `logging` module.** Every module that does non-trivial work
296
+ gets `logger = logging.getLogger(__name__)`. Never use `print()` for
297
+ diagnostic output. Hook scripts write user-facing failure messages to
298
+ `sys.stderr` via `print` -- that is the *one* allowed use, because
299
+ pre-commit captures and displays stderr directly to the developer.
300
+ - **Log every ratchet decision the user might need to debug:** which
301
+ baseline was loaded, what the current metric was, what the recorded metric
302
+ was, whether the registry was rewritten. The user is debugging a failed
303
+ commit; volume is fine.
304
+
305
+ ### Safety and secrets
306
+ - **Never log `HUMAN_RATCHET_BYPASS_KEY` or any token.** Ratchet C reads it
307
+ but must not echo it. Tests assert on this; do not break them.
308
+ - **`.env` files for local configuration.** `.gitignore` + `.env.example` =
309
+ security + transparency. This repo does not currently use `.env`; if you
310
+ add one, follow the pattern.
311
+
312
+ ### Agent narration policy (NON-NEGOTIABLE)
313
+ GitHub Copilot CLI, Cursor, Claude Code, and Aider all occasionally leak
314
+ agent narration ("Now let me check the docs directory:", "Sure, I can help <!-- ratchet-allow: agent_chatter -->
315
+ with...") into stdout despite `-s/--silent`. That narration must never reach
316
+ a committed file -- it reads as a chat transcript and destroys trust in the
317
+ codebase.
318
+
319
+ This repo's defence is **Ratchet B itself**
320
+ ([git_agent_ratchet/ratchets/agent_chatter.py](git_agent_ratchet/ratchets/agent_chatter.py)).
321
+ The regex signatures live in `CHATTER_SIGNATURES`. If a new narration
322
+ pattern slips through (a CLI version bump introduces new phrasing):
323
+
324
+ 1. Add the pattern to `CHATTER_SIGNATURES`.
325
+ 2. Add a regression test in `tests/test_agent_chatter_regressions.py` that
326
+ matches the new phrasing.
327
+ 3. Commit the regex change and the test in the same commit. **Do not** ship
328
+ the test without the regex -- the regression suite goes red and the next
329
+ agent is blocked.
330
+
331
+ If existing files in the repo are infected, fix them by hand in a separate
332
+ commit (`cleanup: scrub leaked narration from <file>`).
333
+
334
+ ---
335
+
336
+ ## Tech stack
337
+
338
+ | Layer | Choice |
339
+ |---|---|
340
+ | **Language** | Python 3.10+ |
341
+ | **Build backend** | hatchling |
342
+ | **Packaging** | uv (NEVER pip) |
343
+ | **Pre-commit framework** | pre-commit (the upstream Python tool) |
344
+ | **Testing** | pytest + pytest-cov |
345
+ | **Lint + format** | ruff (E, W, F, I, B, UP) |
346
+ | **Distribution** | published as a pre-commit-compatible repo via `.pre-commit-hooks.yaml` |
347
+
348
+ ---
349
+
350
+ ## Project structure
351
+
352
+ ```
353
+ git-agent-ratchet/
354
+ |-- AGENTS.md # This file -- agent grounding
355
+ |-- README.md # Front door
356
+ |-- LICENSE
357
+ |-- pyproject.toml # hatchling build, ruff config, pytest config
358
+ |-- .pre-commit-hooks.yaml # Hook manifest consumed by other repos
359
+ |-- .pre-commit-config.yaml # This repo's own pre-commit config (dogfood)
360
+ |-- .gitignore
361
+ |-- docs/
362
+ | |-- spec.md # Full design spec v1.0.0 (the contract)
363
+ | `-- TODO.md # Master progress tracker + Known bugs
364
+ |-- git_agent_ratchet/ # Flat package (no src/ layout)
365
+ | |-- __init__.py
366
+ | |-- __main__.py # `python -m git_agent_ratchet`
367
+ | |-- _version.py
368
+ | |-- py.typed
369
+ | |-- cli.py # Unified `git-agent-ratchet <subcommand>` dispatcher
370
+ | |-- baseline.py # JSON registry load / save / mutate
371
+ | |-- hooks/ # Pre-commit entry points (one per ratchet)
372
+ | | |-- no_duplicate_helpers.py # Ratchet A entry
373
+ | | |-- deny_agent_chatter.py # Ratchet B entry
374
+ | | `-- anti_bypass.py # Ratchet C entry
375
+ | `-- ratchets/ # Pure scanners (no I/O of their own)
376
+ | |-- duplicate_helpers.py # AST scan
377
+ | |-- agent_chatter.py # Regex scan
378
+ | `-- anti_bypass.py # Env + path inspection
379
+ |-- config/
380
+ | `-- ratchets/
381
+ | `-- duplicates.json # Ratchet A baseline registry
382
+ `-- tests/
383
+ |-- test_baseline.py
384
+ |-- test_duplicate_helpers.py
385
+ |-- test_agent_chatter.py
386
+ |-- test_anti_bypass.py
387
+ |-- test_hooks_no_duplicate_helpers.py
388
+ |-- test_hooks_deny_agent_chatter.py
389
+ |-- test_hooks_anti_bypass.py
390
+ `-- test_cli.py
391
+ ```
392
+
393
+ ---
394
+
395
+ ## Key concepts
396
+
397
+ ### Baseline registry
398
+ A versioned JSON file per ratchet, default `config/ratchets/duplicates.json`.
399
+ Shape and schema in section 2.2 of [docs/spec.md](docs/spec.md). The
400
+ invariant: for any ratchet `R`, the metric value `C_{t+1} <= C_t` across
401
+ commits. The registry is allowed to shrink (the hook rewrites it and stages
402
+ the diff into the current commit); it is structurally barred from growing
403
+ without a human bypass.
404
+
405
+ ### Hook lifecycle inside another repo
406
+ A consumer adds this repo to their `.pre-commit-config.yaml`:
407
+
408
+ ```yaml
409
+ repos:
410
+ - repo: https://github.com/monk-eee/git-agent-ratchet
411
+ rev: v1.0.0
412
+ hooks:
413
+ - id: ratchet-no-duplicate-helpers
414
+ args: [--baseline=config/ratchets/duplicates.json, --dir=src/]
415
+ - id: ratchet-deny-agent-chatter
416
+ files: \.(py|md|txt|go|js|ts|rs)$
417
+ - id: ratchet-anti-bypass
418
+ args: [--enforce-files=AGENTS.md,.pre-commit-config.yaml,config/ratchets/duplicates.json]
419
+ ```
420
+
421
+ Pre-commit installs this package into an isolated venv, then dispatches the
422
+ matching console script per hook id. Filenames are passed positionally;
423
+ flags configure paths and policy.
424
+
425
+ ### Anti-bypass policy
426
+ Ratchet C reads `HUMAN_RATCHET_BYPASS_KEY` from the environment. The key is
427
+ present iff a human operator has explicitly opted in for the current shell
428
+ session. Agents must not set it. If you (an agent) find yourself wanting to
429
+ set `HUMAN_RATCHET_BYPASS_KEY` to make a commit pass, you are the failure
430
+ mode the ratchet exists to catch -- stop and surface the blocker to the
431
+ operator instead.
432
+
433
+ ---
434
+
435
+ ## Agent workflow
436
+
437
+ When an agent is asked to extend or change this package:
438
+
439
+ 1. **Read [docs/spec.md](docs/spec.md)** first. It is the contract; this
440
+ file is the agent-facing index. If the change contradicts the spec, the
441
+ spec must be updated in the same commit.
442
+ 2. **Run the test suite before you start.** `uv run pytest -q`. If it is
443
+ already red, fix that first or surface it -- do not stack a new change on
444
+ a broken baseline.
445
+ 3. **Pick the smallest module that owns the change.**
446
+ - Changing a regex signature -> `git_agent_ratchet/ratchets/agent_chatter.py`
447
+ plus a regression test.
448
+ - Changing a hook's CLI surface -> `git_agent_ratchet/hooks/<name>.py` plus
449
+ the matching `tests/test_hooks_<name>.py`.
450
+ - Changing the registry shape -> `git_agent_ratchet/baseline.py` plus
451
+ `tests/test_baseline.py`, and bump `SCHEMA_URL` if the on-disk shape
452
+ changes.
453
+ 4. **Run regression tests for any module you touch** and add a new test in
454
+ `tests/test_<module>_regressions.py` for any bug fixed.
455
+ 5. **Run all three ratchets against this repo before committing.** `make
456
+ ratchet` runs them in sequence. This is the dogfood check -- if our own
457
+ hooks fail on our own code, the change is wrong.
458
+
459
+ ---
460
+
461
+ ## Commands
462
+
463
+ ```bash
464
+ # Setup
465
+ make setup # uv sync + pre-commit install
466
+
467
+ # Dev
468
+ make test # uv run pytest -q
469
+ make test-cov # uv run pytest --cov=git_agent_ratchet --cov-report=term-missing
470
+ make lint # ruff check + ruff format --check
471
+ make format # ruff check --fix + ruff format
472
+ make ratchet # run all three hooks against this repo
473
+
474
+ # Direct hook invocation (debugging)
475
+ uv run git-agent-ratchet no-duplicate-helpers --dir git_agent_ratchet --baseline config/ratchets/duplicates.json
476
+ uv run git-agent-ratchet deny-agent-chatter <file>...
477
+ uv run git-agent-ratchet anti-bypass --enforce-files AGENTS.md,.pre-commit-config.yaml,config/ratchets/duplicates.json <file>...
478
+
479
+ # Seed an empty baseline (first time only)
480
+ uv run git-agent-ratchet no-duplicate-helpers --dir git_agent_ratchet --baseline config/ratchets/duplicates.json
481
+ ```
482
+
483
+ ---
484
+
485
+ ## Conventions
486
+
487
+ - **File naming:** snake_case for Python modules, kebab-case for hook ids.
488
+ - **No emojis** in generated content, comments, commit messages, or docs.
489
+ - **JSON for machine data, Markdown for docs** -- the baseline registry is
490
+ JSON because it is rewritten programmatically; the spec and this file are
491
+ Markdown because humans and agents read them.
492
+ - **Hooks are idempotent.** Running a ratchet twice on a clean tree produces
493
+ the same exit code and the same registry. Tests assert this.
494
+ - **Every hook prints what it did to stderr.** The user is debugging a
495
+ failed commit; "exited 1 with no output" is a bug, not a feature.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Monkee Magic & Git Ratchet Core
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.