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.
- git_agent_ratchet-1.1.0/.gitignore +54 -0
- git_agent_ratchet-1.1.0/.pre-commit-hooks.yaml +41 -0
- git_agent_ratchet-1.1.0/AGENTS.md +495 -0
- git_agent_ratchet-1.1.0/LICENSE +21 -0
- git_agent_ratchet-1.1.0/PKG-INFO +521 -0
- git_agent_ratchet-1.1.0/README.md +493 -0
- git_agent_ratchet-1.1.0/docs/TODO.md +65 -0
- git_agent_ratchet-1.1.0/docs/spec.md +211 -0
- git_agent_ratchet-1.1.0/examples/downstream/README.md +70 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/__init__.py +5 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/__main__.py +3 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/_version.py +1 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/baseline.py +82 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/cli.py +53 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/hooks/__init__.py +1 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/hooks/anti_bypass.py +66 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/hooks/deny_agent_chatter.py +57 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/hooks/max_file_lines.py +128 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/hooks/no_duplicate_helpers.py +135 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/paths.py +18 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/py.typed +0 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/__init__.py +1 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/agent_chatter.py +79 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/anti_bypass.py +100 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/duplicate_helpers.py +89 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/extractors/__init__.py +40 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/extractors/csharp_ext.py +51 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/extractors/python_ext.py +49 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/extractors/typescript_ext.py +67 -0
- git_agent_ratchet-1.1.0/git_agent_ratchet/ratchets/max_file_lines.py +84 -0
- git_agent_ratchet-1.1.0/pyproject.toml +101 -0
- git_agent_ratchet-1.1.0/tests/__init__.py +1 -0
- git_agent_ratchet-1.1.0/tests/test_agent_chatter.py +120 -0
- git_agent_ratchet-1.1.0/tests/test_anti_bypass.py +105 -0
- git_agent_ratchet-1.1.0/tests/test_baseline.py +103 -0
- git_agent_ratchet-1.1.0/tests/test_cli.py +116 -0
- git_agent_ratchet-1.1.0/tests/test_duplicate_helpers.py +145 -0
- git_agent_ratchet-1.1.0/tests/test_extractor_csharp.py +199 -0
- git_agent_ratchet-1.1.0/tests/test_extractor_python.py +94 -0
- git_agent_ratchet-1.1.0/tests/test_extractor_typescript.py +180 -0
- git_agent_ratchet-1.1.0/tests/test_hooks_anti_bypass.py +77 -0
- git_agent_ratchet-1.1.0/tests/test_hooks_deny_agent_chatter.py +56 -0
- git_agent_ratchet-1.1.0/tests/test_hooks_max_file_lines.py +161 -0
- git_agent_ratchet-1.1.0/tests/test_hooks_no_duplicate_helpers.py +124 -0
- git_agent_ratchet-1.1.0/tests/test_max_file_lines.py +137 -0
- 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.
|