housebroken-cli 0.5.0__tar.gz → 0.6.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 (30) hide show
  1. housebroken_cli-0.6.0/PKG-INFO +154 -0
  2. housebroken_cli-0.6.0/README.md +142 -0
  3. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/bin/housebroken +2 -0
  4. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/pyproject.toml +1 -1
  5. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/scripts/branch-check.sh +52 -34
  6. housebroken_cli-0.6.0/scripts/check-ai-policy.sh +139 -0
  7. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/scripts/claim-check.sh +6 -13
  8. housebroken_cli-0.6.0/scripts/comment-check.sh +142 -0
  9. housebroken_cli-0.6.0/scripts/file-pr.sh +178 -0
  10. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/scripts/inbox.sh +1 -1
  11. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/scripts/review.sh +32 -29
  12. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/skills/housebroken/SKILL.md +43 -28
  13. housebroken_cli-0.6.0/src/housebroken/__init__.py +1 -0
  14. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/src/housebroken/cli.py +3 -4
  15. housebroken_cli-0.5.0/PKG-INFO +0 -185
  16. housebroken_cli-0.5.0/README.md +0 -173
  17. housebroken_cli-0.5.0/scripts/check-ai-policy.sh +0 -109
  18. housebroken_cli-0.5.0/scripts/file-pr.sh +0 -145
  19. housebroken_cli-0.5.0/src/housebroken/__init__.py +0 -1
  20. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/.gitignore +0 -0
  21. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/LICENSE +0 -0
  22. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/hatch_build.py +0 -0
  23. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/scripts/comment-census.sh +0 -0
  24. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/scripts/diff-defaults.sh +0 -0
  25. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/scripts/distinct-outside.sh +0 -0
  26. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/scripts/fork-hygiene.sh +0 -0
  27. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/scripts/notes.sh +0 -0
  28. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/scripts/pr-sweep.sh +0 -0
  29. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/scripts/prior-art.sh +0 -0
  30. {housebroken_cli-0.5.0 → housebroken_cli-0.6.0}/scripts/verify-filed-pr.sh +0 -0
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.5
2
+ Name: housebroken-cli
3
+ Version: 0.6.0
4
+ Summary: Agent-written pull requests that do not make a mess in someone else's house.
5
+ Project-URL: Repository, https://github.com/lenamonj/housebroken
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+
13
+ <p align="center">
14
+ <img src="https://raw.githubusercontent.com/lenamonj/housebroken/main/assets/door.jpg" width="560" alt="A white ceramic robot dog sits at an open, lit door, a scroll in its mouth, its paws stopped at a glowing amber line on the threshold">
15
+ </p>
16
+
17
+ <h1 align="center">housebroken</h1>
18
+
19
+ <p align="center"><em>Agent-written pull requests that do not make a mess in someone else's house.</em></p>
20
+
21
+ <p align="center">
22
+ <img src="https://img.shields.io/github/stars/lenamonj/housebroken?style=flat-square&color=111111&label=stars" alt="Stars">
23
+ <img src="https://img.shields.io/github/v/release/lenamonj/housebroken?style=flat-square&color=111111&label=release" alt="Release">
24
+ <img src="https://img.shields.io/pypi/v/housebroken-cli?style=flat-square&color=111111&label=pypi" alt="PyPI">
25
+ <img src="https://img.shields.io/badge/works%20with-Claude%20Code-111111?style=flat-square" alt="Works with Claude Code">
26
+ <img src="https://img.shields.io/badge/shellcheck-clean-111111?style=flat-square" alt="shellcheck clean">
27
+ <img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT license">
28
+ </p>
29
+
30
+ A maintainer runs her project on evenings and weekends. She opens GitHub to pull requests from accounts she has never seen, each rewriting a function she wrote years ago, each carrying comments in code that had none, a six-hundred-word body and a footer naming the tool that wrote it. She closes them without reading them, and she is right to.
31
+
32
+ housebroken is the set of rules that get a pull request past her, written as scripts that refuse to file until every rule is met. The rules are principles, not opinions, and the scripts are the enforcement: a gate that only suggests is followed when remembered.
33
+
34
+ ## What passes the door
35
+
36
+ ```
37
+ one finding, one pull request
38
+ the smallest diff in the project's own style, no comment added to their code
39
+ one test that fails on their main and passes with the patch
40
+ a body under 120 words, in their template, no footer, no trailer
41
+ their policy read first: no filing where AI is banned, disclosure where it is asked
42
+ prior art read and quoted before the branch existed
43
+ their CI target green on a fresh clone of their main
44
+ attacked by a second model before it left, evidence behind every finding
45
+ the CLA known before filing, the SECURITY.md route taken when it applies
46
+ ```
47
+
48
+ ## The rules
49
+
50
+ <img align="right" width="230" src="https://raw.githubusercontent.com/lenamonj/housebroken/main/assets/treatise.jpg" alt="An obsidian book with chrome corners titled The Housebroken Agent, a treatise on manners for machines calling at the homes of maintainers">
51
+
52
+ A pull request passes through the door in order, and stops at the first rule it fails. Each rule is a script where a script can hold it. The maintainers' own verdicts that shaped them are kept separately, in [docs/lessons.md](https://github.com/lenamonj/housebroken/blob/main/docs/lessons.md).
53
+
54
+ **1. Read the house rules before knocking.**
55
+ `check-ai-policy.sh` reads every contributing guide, code of conduct, template and agent instruction file on the repository's branch and in the organization's `.github` repository, matched by name without regard to case, and prints each sentence that names AI with its reading: BAN, DISCLOSE, MENTION or CLEAN. A project that bans AI-written contributions never gets one. A project that asks for disclosure gets one truthful sentence in the body. `distinct-outside.sh` counts outside contributors merged in the last 120 days; a project that has merged none is closed to outsiders whatever its README says.
56
+
57
+ **2. Is it already on the table?**
58
+ `prior-art.sh` lists every issue and pull request, open, closed and merged, that touches the file or symbol, with the type taken from the API and the closing ruling quoted for every closed item. An open pull request that fixes the same thing, or a closed issue that ruled the behaviour intended, ends the work. A ruling is never argued with.
59
+
60
+ **3. Is it a fix or an opinion?**
61
+ A change that rejects an input the project tolerated, changes a default, or narrows what the project publishes is the maintainer's call. It is sent with the call stated in the body: what breaks for existing users, and an offer to narrow or close it. A finding whose only evidence is a measurement is a hold, not a pull request.
62
+
63
+ **4. Prove it red first, and prove the proof.**
64
+ On a fresh clone of the upstream default branch, the new test fails; with the patch, it passes. Every arm of the experiment prints evidence that its precondition held, files are swapped by content and checked by hash, and two arms that agree exactly have not tested the variable.
65
+
66
+ **5. Run their CI, not yours.**
67
+ The project's own test target on the fresh clone, and every gate its pull request workflow runs: lint, format, mutation, API check. A gate not run is a red check the maintainer sees first.
68
+
69
+ **6. Match the house style.**
70
+ `comment-check.sh` refuses any comment the branch adds to code, test files included; an existing comment may be reworded, a new file may open with the header its neighbours carry, and a maintainer's explicit request for documentation is the one exception. `comment-census.sh` measures density. The body is under 120 words, in the project's template, and says what was wrong, what changed and how it was verified, every sentence reproduced on the fresh clone. No tool footer, no session link, no co-author trailer, no typographic dash. `claim-check.sh` lists every counted or absolute claim in the body so each is re-derived against the exact revision it names.
71
+
72
+ **7. Know the paperwork.**
73
+ Before the first pull request to an organization: CLA, DCO, signed-commit requirement, disclosure trailer, and the identity clause. Author, committer and sign-off carry the contributor's real name.
74
+
75
+ **8. Security goes through the side door.**
76
+ A memory-safety, remote-abort, injection or path-escape finding goes by the project's SECURITY.md route, privately, and never becomes a public pull request until the project answers.
77
+
78
+ **9. File through the gate.**
79
+ `branch-check.sh` refuses a dirty working tree, a file whose mode disagrees with its siblings, a file the repository's own `.gitignore` excludes, a working artifact, a commit carrying a tool trailer, a session link or a tool identity, a sign-off naming a username, a branch not on top of its base, and any comment added to code; it prints every absolute claim in added prose for falsification, and stamps the head it passed.
80
+ `review.sh` names the adversarial reviewer: a different model from the one that wrote the change, one tier down, never below Sonnet. The reviewer gets a fresh context, hard read-only limits and an attack list that starts with every claim the prose makes; every finding carries evidence it produced itself, and its report is bound to the head and the text it read. A fix is a new head and gets a new review.
81
+ `file-pr.sh` is the only way a pull request is opened. It refuses without a prior-art printout under a day old, a policy printout under a day old that does not read BAN, a body that discloses when the policy asks, a branch stamp for the exact head, and a POST AS IS review of that head and that body.
82
+
83
+ **10. Watch it land.**
84
+ `verify-filed-pr.sh` re-derives from GitHub that the head is the intended commit, the diff is exactly the intended files, and CI settled. `inbox.sh` prints every event on your threads since the last acknowledged cursor, bodies included, with a census of failing, blocked and conflicting checks. Every maintainer comment gets a same-day answer in the contributor's voice, checked by `claim-check.sh` and, when it concedes, disagrees or rides on a code change, by the adversarial review. Replies to one repository are spaced ten minutes apart. When the maintainer is right, concede in one sentence. A silent maintainer is never nudged.
85
+
86
+ **11. Clean up.**
87
+ `fork-hygiene.sh` deletes the fork branch of every merged or closed pull request and lists forks with no pull request left. No planning file, agent directory, journal or build output ever enters a diff.
88
+
89
+ **12. Three per repository, one finding each.**
90
+ A repository gets at most three open pull requests, each one finding, each meeting every rule above on its own, filed at least ten minutes apart.
91
+
92
+ **13. Write it down before you leave.**
93
+ A house rule learned about a repository goes into `notes.sh` the same session. A maintainer's verdict goes into docs/lessons.md the same day, quoted verbatim with the rule it became. A gate that was wrong, missing or done by hand becomes an issue before the run ends.
94
+
95
+ ## Install
96
+
97
+ You need `bash`, `gh` (signed in) and `jq`. Then, from PyPI; `pipx install housebroken-cli` and `uv tool install housebroken-cli` work the same way:
98
+
99
+ ```
100
+ pip install housebroken-cli
101
+ housebroken install-skill
102
+ ```
103
+
104
+ The first line puts the `housebroken` command on your path. The second puts the skill where Claude Code loads it, so the agent runs the door itself before it opens anything upstream. To run the main branch between releases, install from the repository instead:
105
+
106
+ ```
107
+ uv tool install git+https://github.com/lenamonj/housebroken
108
+ ```
109
+
110
+ `housebroken help` prints the door in order. Every gate is also a plain bash script under `scripts/`, runnable on its own.
111
+
112
+ ## What is in the repository
113
+
114
+ | script | gate |
115
+ |---|---|
116
+ | `scripts/check-ai-policy.sh` | the AI-contribution policy, read where it lives, with its reading: BAN, DISCLOSE, MENTION, CLEAN |
117
+ | `scripts/distinct-outside.sh` | outside contributors merged in 120 days |
118
+ | `scripts/prior-art.sh` | issues and pull requests on the touched files, type stated, rulings quoted |
119
+ | `scripts/comment-check.sh` | refuses a comment added to code nobody asked for |
120
+ | `scripts/comment-census.sh` | added code versus added comments, per branch |
121
+ | `scripts/diff-defaults.sh` | default arguments and one-line wrappers added to keep call sites untouched |
122
+ | `scripts/claim-check.sh` | every falsifiable claim in a body or a reply, listed to re-derive |
123
+ | `scripts/branch-check.sh` | the branch's mechanical facts: clean tree, modes, artifacts, trailers, sign-offs, comments, absolute claims |
124
+ | `scripts/review.sh` | the adversarial review: a different model attacks the change, and the filing is held to exactly what it read |
125
+ | `scripts/file-pr.sh` | the only way a pull request gets filed: five gates in front of `gh pr create` |
126
+ | `scripts/verify-filed-pr.sh` | the filed pull request is what was meant, and CI settled |
127
+ | `scripts/pr-sweep.sh` | every open pull request where the ball is in your court |
128
+ | `scripts/inbox.sh` | what GitHub has to tell you since you last looked, bodies included, with a census of every open pull request |
129
+ | `scripts/notes.sh` | what this repository's maintainers have asked for before |
130
+ | `scripts/fork-hygiene.sh` | branches deleted after merge or close, orphan forks listed |
131
+
132
+ The scripts are bash and need `gh` and `jq`. Each script's header says what it assumes.
133
+
134
+ ## Not in scope
135
+
136
+ housebroken does not find bugs and does not write patches. Any agent that produces a change can use it. It governs what leaves your machine and how it behaves once it arrives.
137
+
138
+ ## FAQ
139
+
140
+ **Is this only for AI-written pull requests?**
141
+ No. Every rule here predates agents. Agents made it cheap to break all of them at once, on a hundred repositories, before breakfast.
142
+
143
+ **Does it make maintainers like agent pull requests?**
144
+ No. It makes the pull request indistinguishable from a careful human's, and answers truthfully when they ask.
145
+
146
+ **What if the maintainer closes it anyway?**
147
+ Then the maintainer is the judge and the ruling stands. Read why, write it down, and if it is a class of mistake, make it a gate.
148
+
149
+ **Why "housebroken"?**
150
+ Because the alternative is what maintainers are calling it.
151
+
152
+ ## License
153
+
154
+ MIT.
@@ -0,0 +1,142 @@
1
+ <p align="center">
2
+ <img src="assets/door.jpg" width="560" alt="A white ceramic robot dog sits at an open, lit door, a scroll in its mouth, its paws stopped at a glowing amber line on the threshold">
3
+ </p>
4
+
5
+ <h1 align="center">housebroken</h1>
6
+
7
+ <p align="center"><em>Agent-written pull requests that do not make a mess in someone else's house.</em></p>
8
+
9
+ <p align="center">
10
+ <img src="https://img.shields.io/github/stars/lenamonj/housebroken?style=flat-square&color=111111&label=stars" alt="Stars">
11
+ <img src="https://img.shields.io/github/v/release/lenamonj/housebroken?style=flat-square&color=111111&label=release" alt="Release">
12
+ <img src="https://img.shields.io/pypi/v/housebroken-cli?style=flat-square&color=111111&label=pypi" alt="PyPI">
13
+ <img src="https://img.shields.io/badge/works%20with-Claude%20Code-111111?style=flat-square" alt="Works with Claude Code">
14
+ <img src="https://img.shields.io/badge/shellcheck-clean-111111?style=flat-square" alt="shellcheck clean">
15
+ <img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT license">
16
+ </p>
17
+
18
+ A maintainer runs her project on evenings and weekends. She opens GitHub to pull requests from accounts she has never seen, each rewriting a function she wrote years ago, each carrying comments in code that had none, a six-hundred-word body and a footer naming the tool that wrote it. She closes them without reading them, and she is right to.
19
+
20
+ housebroken is the set of rules that get a pull request past her, written as scripts that refuse to file until every rule is met. The rules are principles, not opinions, and the scripts are the enforcement: a gate that only suggests is followed when remembered.
21
+
22
+ ## What passes the door
23
+
24
+ ```
25
+ one finding, one pull request
26
+ the smallest diff in the project's own style, no comment added to their code
27
+ one test that fails on their main and passes with the patch
28
+ a body under 120 words, in their template, no footer, no trailer
29
+ their policy read first: no filing where AI is banned, disclosure where it is asked
30
+ prior art read and quoted before the branch existed
31
+ their CI target green on a fresh clone of their main
32
+ attacked by a second model before it left, evidence behind every finding
33
+ the CLA known before filing, the SECURITY.md route taken when it applies
34
+ ```
35
+
36
+ ## The rules
37
+
38
+ <img align="right" width="230" src="assets/treatise.jpg" alt="An obsidian book with chrome corners titled The Housebroken Agent, a treatise on manners for machines calling at the homes of maintainers">
39
+
40
+ A pull request passes through the door in order, and stops at the first rule it fails. Each rule is a script where a script can hold it. The maintainers' own verdicts that shaped them are kept separately, in [docs/lessons.md](docs/lessons.md).
41
+
42
+ **1. Read the house rules before knocking.**
43
+ `check-ai-policy.sh` reads every contributing guide, code of conduct, template and agent instruction file on the repository's branch and in the organization's `.github` repository, matched by name without regard to case, and prints each sentence that names AI with its reading: BAN, DISCLOSE, MENTION or CLEAN. A project that bans AI-written contributions never gets one. A project that asks for disclosure gets one truthful sentence in the body. `distinct-outside.sh` counts outside contributors merged in the last 120 days; a project that has merged none is closed to outsiders whatever its README says.
44
+
45
+ **2. Is it already on the table?**
46
+ `prior-art.sh` lists every issue and pull request, open, closed and merged, that touches the file or symbol, with the type taken from the API and the closing ruling quoted for every closed item. An open pull request that fixes the same thing, or a closed issue that ruled the behaviour intended, ends the work. A ruling is never argued with.
47
+
48
+ **3. Is it a fix or an opinion?**
49
+ A change that rejects an input the project tolerated, changes a default, or narrows what the project publishes is the maintainer's call. It is sent with the call stated in the body: what breaks for existing users, and an offer to narrow or close it. A finding whose only evidence is a measurement is a hold, not a pull request.
50
+
51
+ **4. Prove it red first, and prove the proof.**
52
+ On a fresh clone of the upstream default branch, the new test fails; with the patch, it passes. Every arm of the experiment prints evidence that its precondition held, files are swapped by content and checked by hash, and two arms that agree exactly have not tested the variable.
53
+
54
+ **5. Run their CI, not yours.**
55
+ The project's own test target on the fresh clone, and every gate its pull request workflow runs: lint, format, mutation, API check. A gate not run is a red check the maintainer sees first.
56
+
57
+ **6. Match the house style.**
58
+ `comment-check.sh` refuses any comment the branch adds to code, test files included; an existing comment may be reworded, a new file may open with the header its neighbours carry, and a maintainer's explicit request for documentation is the one exception. `comment-census.sh` measures density. The body is under 120 words, in the project's template, and says what was wrong, what changed and how it was verified, every sentence reproduced on the fresh clone. No tool footer, no session link, no co-author trailer, no typographic dash. `claim-check.sh` lists every counted or absolute claim in the body so each is re-derived against the exact revision it names.
59
+
60
+ **7. Know the paperwork.**
61
+ Before the first pull request to an organization: CLA, DCO, signed-commit requirement, disclosure trailer, and the identity clause. Author, committer and sign-off carry the contributor's real name.
62
+
63
+ **8. Security goes through the side door.**
64
+ A memory-safety, remote-abort, injection or path-escape finding goes by the project's SECURITY.md route, privately, and never becomes a public pull request until the project answers.
65
+
66
+ **9. File through the gate.**
67
+ `branch-check.sh` refuses a dirty working tree, a file whose mode disagrees with its siblings, a file the repository's own `.gitignore` excludes, a working artifact, a commit carrying a tool trailer, a session link or a tool identity, a sign-off naming a username, a branch not on top of its base, and any comment added to code; it prints every absolute claim in added prose for falsification, and stamps the head it passed.
68
+ `review.sh` names the adversarial reviewer: a different model from the one that wrote the change, one tier down, never below Sonnet. The reviewer gets a fresh context, hard read-only limits and an attack list that starts with every claim the prose makes; every finding carries evidence it produced itself, and its report is bound to the head and the text it read. A fix is a new head and gets a new review.
69
+ `file-pr.sh` is the only way a pull request is opened. It refuses without a prior-art printout under a day old, a policy printout under a day old that does not read BAN, a body that discloses when the policy asks, a branch stamp for the exact head, and a POST AS IS review of that head and that body.
70
+
71
+ **10. Watch it land.**
72
+ `verify-filed-pr.sh` re-derives from GitHub that the head is the intended commit, the diff is exactly the intended files, and CI settled. `inbox.sh` prints every event on your threads since the last acknowledged cursor, bodies included, with a census of failing, blocked and conflicting checks. Every maintainer comment gets a same-day answer in the contributor's voice, checked by `claim-check.sh` and, when it concedes, disagrees or rides on a code change, by the adversarial review. Replies to one repository are spaced ten minutes apart. When the maintainer is right, concede in one sentence. A silent maintainer is never nudged.
73
+
74
+ **11. Clean up.**
75
+ `fork-hygiene.sh` deletes the fork branch of every merged or closed pull request and lists forks with no pull request left. No planning file, agent directory, journal or build output ever enters a diff.
76
+
77
+ **12. Three per repository, one finding each.**
78
+ A repository gets at most three open pull requests, each one finding, each meeting every rule above on its own, filed at least ten minutes apart.
79
+
80
+ **13. Write it down before you leave.**
81
+ A house rule learned about a repository goes into `notes.sh` the same session. A maintainer's verdict goes into docs/lessons.md the same day, quoted verbatim with the rule it became. A gate that was wrong, missing or done by hand becomes an issue before the run ends.
82
+
83
+ ## Install
84
+
85
+ You need `bash`, `gh` (signed in) and `jq`. Then, from PyPI; `pipx install housebroken-cli` and `uv tool install housebroken-cli` work the same way:
86
+
87
+ ```
88
+ pip install housebroken-cli
89
+ housebroken install-skill
90
+ ```
91
+
92
+ The first line puts the `housebroken` command on your path. The second puts the skill where Claude Code loads it, so the agent runs the door itself before it opens anything upstream. To run the main branch between releases, install from the repository instead:
93
+
94
+ ```
95
+ uv tool install git+https://github.com/lenamonj/housebroken
96
+ ```
97
+
98
+ `housebroken help` prints the door in order. Every gate is also a plain bash script under `scripts/`, runnable on its own.
99
+
100
+ ## What is in the repository
101
+
102
+ | script | gate |
103
+ |---|---|
104
+ | `scripts/check-ai-policy.sh` | the AI-contribution policy, read where it lives, with its reading: BAN, DISCLOSE, MENTION, CLEAN |
105
+ | `scripts/distinct-outside.sh` | outside contributors merged in 120 days |
106
+ | `scripts/prior-art.sh` | issues and pull requests on the touched files, type stated, rulings quoted |
107
+ | `scripts/comment-check.sh` | refuses a comment added to code nobody asked for |
108
+ | `scripts/comment-census.sh` | added code versus added comments, per branch |
109
+ | `scripts/diff-defaults.sh` | default arguments and one-line wrappers added to keep call sites untouched |
110
+ | `scripts/claim-check.sh` | every falsifiable claim in a body or a reply, listed to re-derive |
111
+ | `scripts/branch-check.sh` | the branch's mechanical facts: clean tree, modes, artifacts, trailers, sign-offs, comments, absolute claims |
112
+ | `scripts/review.sh` | the adversarial review: a different model attacks the change, and the filing is held to exactly what it read |
113
+ | `scripts/file-pr.sh` | the only way a pull request gets filed: five gates in front of `gh pr create` |
114
+ | `scripts/verify-filed-pr.sh` | the filed pull request is what was meant, and CI settled |
115
+ | `scripts/pr-sweep.sh` | every open pull request where the ball is in your court |
116
+ | `scripts/inbox.sh` | what GitHub has to tell you since you last looked, bodies included, with a census of every open pull request |
117
+ | `scripts/notes.sh` | what this repository's maintainers have asked for before |
118
+ | `scripts/fork-hygiene.sh` | branches deleted after merge or close, orphan forks listed |
119
+
120
+ The scripts are bash and need `gh` and `jq`. Each script's header says what it assumes.
121
+
122
+ ## Not in scope
123
+
124
+ housebroken does not find bugs and does not write patches. Any agent that produces a change can use it. It governs what leaves your machine and how it behaves once it arrives.
125
+
126
+ ## FAQ
127
+
128
+ **Is this only for AI-written pull requests?**
129
+ No. Every rule here predates agents. Agents made it cheap to break all of them at once, on a hundred repositories, before breakfast.
130
+
131
+ **Does it make maintainers like agent pull requests?**
132
+ No. It makes the pull request indistinguishable from a careful human's, and answers truthfully when they ask.
133
+
134
+ **What if the maintainer closes it anyway?**
135
+ Then the maintainer is the judge and the ruling stands. Read why, write it down, and if it is a class of mistake, make it a gate.
136
+
137
+ **Why "housebroken"?**
138
+ Because the alternative is what maintainers are calling it.
139
+
140
+ ## License
141
+
142
+ MIT.
@@ -48,6 +48,7 @@ usage: housebroken <subcommand> [args...]
48
48
  1 outside count outside contributors merged in the last 120 days
49
49
  2 prior-art issues and pull requests on the touched files, rulings quoted
50
50
  6 census added code lines against added comment lines, per branch
51
+ 6 comments refuse a comment added to code nobody asked for
51
52
  6 claims every falsifiable claim in outbound text, listed to re-derive
52
53
  9 branch the mechanical facts about the branch: tree, modes, artifacts, claims
53
54
  9 review a different model attacks the change before it is filed
@@ -92,6 +93,7 @@ case "$cmd" in
92
93
  outside) script=distinct-outside.sh ;;
93
94
  prior-art) script=prior-art.sh ;;
94
95
  census) script=comment-census.sh ;;
96
+ comments) script=comment-check.sh ;;
95
97
  claims) script=claim-check.sh ;;
96
98
  branch) script=branch-check.sh ;;
97
99
  review) script=review.sh ;;
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "housebroken-cli"
7
- version = "0.5.0"
7
+ version = "0.6.0"
8
8
  description = "Agent-written pull requests that do not make a mess in someone else's house."
9
9
  dynamic = ["readme"]
10
10
  license = "MIT"
@@ -1,40 +1,37 @@
1
1
  #!/bin/bash
2
2
  # branch-check.sh - the mechanical facts about the branch you are about to file.
3
3
  #
4
- # Three defects on PaloAltoNetworks/docusaurus-openapi-docs #1624 were facts
5
- # about the branch rather than about the patch, and git could have printed all
6
- # three before anything left the machine.
4
+ # What you tested is what you file, and the branch carries nothing the project
5
+ # did not ask for. Refused: a dirty working tree; a file whose mode disagrees
6
+ # with its own siblings; a file the branch adds that the repository's own
7
+ # .gitignore excludes; a working artifact; a commit carrying a tool trailer, a
8
+ # session link or a tool identity; a sign-off naming a username; a branch that
9
+ # is not on top of its base; and a comment added to code, test files included,
10
+ # unless a maintainer asked for it (comment-check.sh). Absolute claims in added
11
+ # prose are printed for you to falsify one by one; some of them are true.
7
12
  #
8
- # A reviewer had just shown that a sentence in the added documentation
9
- # overclaimed. The wording was corrected in the working tree, the branch was
10
- # rebuilt with `git reset --soft <base>`, and the commits still carried the old
11
- # sentence: a soft reset leaves the index at the previous commit, so the
12
- # corrected file was never staged. The tree was dirty and nobody looked. A
13
- # dirty tree is a refusal here, because what you tested is not what you file.
14
- #
15
- # The new test file was committed 100755 where every other test in that
16
- # repository is 100644, which GitHub renders in the diff. A mode that disagrees
17
- # with the file's own siblings is a refusal.
18
- #
19
- # The overclaiming sentence was an absolute: "removes only the files the plugin
20
- # generated", which the code falsifies for two file names. Absolutes in added
21
- # prose are printed for you to check one by one; they are not refused, because
22
- # some of them are true.
13
+ # A branch that passes gets a stamp at $HOUSEBROKEN_HOME/branch-checks/<head>,
14
+ # which file-pr.sh requires for the head it files.
23
15
  #
24
16
  # Usage:
25
- # bash branch-check.sh [CLONE] [--base REF]
17
+ # bash branch-check.sh [CLONE] [--base REF] [--asked URL]
26
18
  # bash branch-check.sh --help
27
19
  #
28
20
  # CLONE defaults to the current directory. REF defaults to the first of
29
21
  # upstream/main, upstream/master, origin/main, origin/master that resolves.
22
+ # --asked URL is the maintainer's request for documentation, which turns the
23
+ # comment refusal into a check.
30
24
  #
31
25
  # Exits 0 when nothing blocks, 1 on a refusal, 2 on a usage error.
32
26
  set -u
33
27
 
34
28
  usage() { awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0"; }
35
29
 
30
+ here=$(cd "$(dirname "$0")" && pwd)
31
+ home="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
36
32
  clone="."
37
33
  base=""
34
+ asked=""
38
35
  while [ "$#" -gt 0 ]; do
39
36
  case "$1" in
40
37
  --help|-h) usage; exit 0 ;;
@@ -43,6 +40,13 @@ while [ "$#" -gt 0 ]; do
43
40
  base="${1:-}"
44
41
  [ -n "$base" ] || { echo "REFUSED: --base needs a ref" >&2; exit 2; }
45
42
  ;;
43
+ --base=*) base="${1#--base=}" ;;
44
+ --asked)
45
+ shift
46
+ asked="${1:-}"
47
+ [ -n "$asked" ] || { echo "REFUSED: --asked needs the URL of the maintainer's ask" >&2; exit 2; }
48
+ ;;
49
+ --asked=*) asked="${1#--asked=}" ;;
46
50
  -*) echo "REFUSED: unknown option $1" >&2; exit 2 ;;
47
51
  *) clone="$1" ;;
48
52
  esac
@@ -124,27 +128,37 @@ $files
124
128
  EOF
125
129
 
126
130
  # A tool trailer is the one thing in a commit that cannot be argued as style.
127
- # Thirteen open pull requests across ten organisations were found carrying
128
- # "Co-Authored-By: Claude ..." and four of them a live claude.ai session URL,
129
- # on NVIDIA, Microsoft, Apple, Uber, Tesla, Apache and IBM repositories. No
130
- # project asked for it; a harness adds it by default and nobody read the commit
131
- # before pushing. The body is checked for this at step 9; the commits were not.
132
- trailers=$(git log --format='%H %s%n%b' "$base..HEAD" 2>/dev/null \
133
- | grep -inE 'Co-Authored-By: *Claude|Claude-Session:|https://claude\.ai/code|Generated with .*Claude')
131
+ # Trailers are matched at the start of a line and a tool by its commit
132
+ # identity, so a subject that only names a trailer is not refused.
133
+ trailers=$(git log --format='%h author %an <%ae>, committer %cn <%ce>%n%b' "$base..HEAD" 2>/dev/null \
134
+ | grep -iE '^co-authored-by:.*(claude|anthropic|copilot|cursor|aider|codex|gemini)|^claude-session:|claude\.ai/code/session_|generated with .*claude|noreply@anthropic\.com|cursoragent@cursor\.com|noreply@aider\.chat')
134
135
  if [ -n "$trailers" ]; then
135
- block "a commit carries a tool trailer or a session link, which no project asked for"
136
+ block "a commit carries a tool trailer or a session link, or a tool identity, which no project asked for"
136
137
  printf '%s\n' "$trailers" | sed 's/^/ /'
137
138
  fi
138
139
 
139
- # DCO wants a human being, and several projects say so in as many words.
140
- if git log --format='%b' "$base..HEAD" 2>/dev/null | grep -qE '^Signed-off-by:'; then
141
- usernames=$(git log --format='%b' "$base..HEAD" 2>/dev/null | grep -E '^Signed-off-by: [a-z0-9_-]+ <')
142
- if [ -n "$usernames" ]; then
143
- block "a Signed-off-by names a username; the DCO asks for a real name"
144
- printf '%s\n' "$usernames" | sed 's/^/ /'
145
- fi
140
+ # DCO wants a human being. A one-word name is taken for a username whatever
141
+ # its case; a real one-word name is refused as well, which is the price of
142
+ # catching them.
143
+ usernames=$(git log --format='%b' "$base..HEAD" 2>/dev/null | grep -E '^Signed-off-by: [^ <]+ <')
144
+ if [ -n "$usernames" ]; then
145
+ block "a Signed-off-by names a username; the DCO asks for a real name"
146
+ printf '%s\n' "$usernames" | sed 's/^/ /'
146
147
  fi
147
148
 
149
+ # A comment added to somebody else's code is refused unless its maintainer
150
+ # asked for it; a reworded comment and a new file's opening header pass.
151
+ comment_args=(--base "$base")
152
+ [ -n "$asked" ] && comment_args+=(--asked "$asked")
153
+ comments=$(bash "$here/comment-check.sh" . "${comment_args[@]}" 2>&1); comment_rc=$?
154
+ case "$comment_rc" in
155
+ 0) ;;
156
+ 1) block "the branch adds comments to code that nobody asked for"
157
+ printf '%s\n' "$comments" | grep -E '^(REFUSED|CHECK): ' | sed 's/^REFUSED: / /' ;;
158
+ *) block "comment-check.sh could not run: $comments" ;;
159
+ esac
160
+ printf '%s\n' "$comments" | grep -E '^CHECK: ' | sed 's/^/ /'
161
+
148
162
  # Absolutes in added prose are the claims a maintainer can falsify in one grep.
149
163
  claims=$(git diff -U0 "$base..HEAD" -- '*.md' '*.mdx' '*.rst' '*.txt' 2>/dev/null \
150
164
  | grep '^+' | grep -v '^+++' | sed 's/^+//' \
@@ -165,5 +179,9 @@ if [ "$blocked" -ne 0 ]; then
165
179
  echo "branch-check: refused. Fix the causes above; a refusal is a result."
166
180
  exit 1
167
181
  fi
182
+ # The stamp file-pr.sh reads: this head passed, against this base, at this time.
183
+ head=$(git rev-parse HEAD)
184
+ mkdir -p "$home/branch-checks"
185
+ printf 'head: %s\nbase: %s\nchecked: %s\n' "$head" "$(git rev-parse "$base")" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$home/branch-checks/$head"
168
186
  echo
169
187
  echo "branch-check: nothing mechanical blocks this branch."
@@ -0,0 +1,139 @@
1
+ #!/bin/bash
2
+ # check-ai-policy.sh - read a repository's stance on AI-assisted contributions
3
+ # where it actually lives, and say what it requires.
4
+ #
5
+ # The rule: a project that bans AI-written contributions never gets one; a
6
+ # project that asks for disclosure gets a truthful sentence in the body; a
7
+ # project that says nothing gets nothing. The policy is read from the
8
+ # repository itself, never from a description of it: every file on the branch
9
+ # whose name is a contributing guide, a code of conduct, a pull request or
10
+ # issue template, a README or an agent instruction file, matched without
11
+ # regard to case, plus the same files in the organization's .github
12
+ # repository.
13
+ #
14
+ # Usage:
15
+ # bash check-ai-policy.sh [--branch <name>] [--out] owner/repo [owner/repo ...]
16
+ # bash check-ai-policy.sh --help
17
+ #
18
+ # --branch <name> read this branch instead of the default; some projects
19
+ # keep the policy on a development branch
20
+ # --out also save the printout to $HOUSEBROKEN_HOME/policy/
21
+ # policy-<owner>-<repo>.md, which file-pr.sh reads
22
+ #
23
+ # Every hit is printed with its file and line so the sentence is read, not a
24
+ # verdict. The RESULT line is the reading: BAN when a sentence forbids or
25
+ # closes AI-written contributions, DISCLOSE when a sentence requires that AI
26
+ # use be disclosed, MENTION when AI is named without either, CLEAN when no
27
+ # policy file names it. Exit 0 for CLEAN or MENTION, 3 for DISCLOSE, 4 for BAN,
28
+ # 1 when the repository cannot be read. With several repositories the exit is
29
+ # the highest.
30
+ #
31
+ # Environment:
32
+ # HOUSEBROKEN_HOME work directory, default $HOME/.housebroken; --out writes
33
+ # to its policy/ subdirectory, created if missing.
34
+ #
35
+ # Assumes gh is installed and authenticated. Read-only: GET calls only.
36
+ set -u
37
+
38
+ usage() { awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0"; }
39
+
40
+ # Policy files, matched against the repository's own tree listing without
41
+ # regard to case, so CONTRIBUTING.rst, contributing.md and Contributing.md
42
+ # are all read.
43
+ NAME_RE='(^|/)(contributing([._-][a-z]+)?\.(md|rst|txt|adoc)|contributing|code[_-]?of[_-]?conduct\.(md|rst|txt)|readme\.(md|rst|txt|adoc)|ai([_-]policy)?\.md|policy\.md|agents?\.md|claude\.md|gemini\.md|copilot-instructions\.md|\.cursorrules|\.cursor/rules/[^/]+|pull_request_template(\.md)?|pull_request_template/[^/]+\.md|issue_template/[^/]+\.(md|yml|yaml)|governance\.md|maintainers\.md)$'
44
+ MAX_FILES=40
45
+
46
+ # The word itself, plus its usual spellings and the tools by name.
47
+ PATTERN='\bAI\b|\bA\.I\.|[Aa]rtificial [Ii]ntelligence|\bLLMs?\b|[Ll]arge [Ll]anguage [Mm]odels?|Copilot|ChatGPT|[Cc]laude|[Gg]enerative|[Mm]achine-generated|[Cc]odex\b|[Cc]ursor\b|[Aa]gentic'
48
+ BAN_RE='not (be )?(accept|allow|permit|welcome)|prohibit|forbid|banned|\bban\b|will be closed|closed without|do not (use|submit|open)|don.t (use|submit)|refrain from|no (AI|LLM|AI-generated|machine-generated)|must not|may not (use|submit)|are not (allowed|accepted|permitted)|zero tolerance'
49
+ DISCLOSE_RE='disclos|disclaimer|declare|must (state|say|note|indicate|mention)|be transparent|label(l)?ed as|clearly (state|indicate|mark)|acknowledg|(if|whether) (you )?(use|used|using) (AI|an AI|LLM)|which (AI|LLM) tool|AI transcript|used AI to'
50
+
51
+ branch_opt=""
52
+ out=0
53
+ repos=()
54
+ while [ "$#" -gt 0 ]; do
55
+ case "$1" in
56
+ --help|-h) usage; exit 0 ;;
57
+ --branch) shift; [ "$#" -gt 0 ] || { echo "check-ai-policy.sh: --branch needs a name" >&2; exit 1; }
58
+ branch_opt="$1" ;;
59
+ --branch=*) branch_opt="${1#--branch=}" ;;
60
+ --out) out=1 ;;
61
+ --*) usage >&2; echo "check-ai-policy.sh: unknown option $1" >&2; exit 1 ;;
62
+ */*) repos+=("$1") ;;
63
+ *) usage >&2; echo "check-ai-policy.sh: '$1' is not owner/repo" >&2; exit 1 ;;
64
+ esac
65
+ shift
66
+ done
67
+ [ "${#repos[@]}" -gt 0 ] || { usage >&2; exit 1; }
68
+
69
+ command -v gh >/dev/null 2>&1 || { echo "check-ai-policy.sh: gh is not on PATH" >&2; exit 1; }
70
+ home="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
71
+
72
+ # List the policy files at one ref of one repository, from its tree listing.
73
+ policy_files() {
74
+ gh api "repos/$1/git/trees/$2?recursive=1" --jq '.tree[] | select(.type == "blob") | .path' 2>/dev/null \
75
+ | grep -Ei "$NAME_RE" | head -"$MAX_FILES"
76
+ }
77
+
78
+ # Print every hit in one repository at one ref; sets found, ban, disclose.
79
+ scan() {
80
+ local target="$1" ref="$2" label="$3" f body hits
81
+ while IFS= read -r f; do
82
+ [ -n "$f" ] || continue
83
+ body=$(gh api "repos/$target/contents/$f?ref=$ref" --jq '.content' 2>/dev/null | base64 -d 2>/dev/null)
84
+ [ -n "$body" ] || continue
85
+ hits=$(printf '%s' "$body" | grep -nE "$PATTERN" | head -20)
86
+ if [ -n "$hits" ]; then
87
+ found=1
88
+ echo " --- $label$f ---"
89
+ printf '%s\n' "$hits" | cut -c1-300 | sed 's/^/ /'
90
+ printf '%s\n' "$hits" | grep -qiE "$BAN_RE" && ban=1
91
+ printf '%s\n' "$hits" | grep -qiE "$DISCLOSE_RE" && disclose=1
92
+ else
93
+ echo " (clean) $label$f"
94
+ fi
95
+ done < <(policy_files "$target" "$ref")
96
+ }
97
+
98
+ status=0
99
+ for repo in "${repos[@]}"; do
100
+ found=0; ban=0; disclose=0
101
+ report=$(
102
+ echo "## AI policy: $repo"
103
+ echo "read: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
104
+ branch="$branch_opt"
105
+ if [ -z "$branch" ]; then
106
+ branch=$(gh api "repos/$repo" --jq .default_branch 2>/dev/null) || branch=""
107
+ fi
108
+ if [ -z "$branch" ]; then
109
+ echo "ERROR: cannot read $repo from GitHub"
110
+ exit 1
111
+ fi
112
+ echo "branch: $branch"
113
+ scan "$repo" "$branch" ""
114
+ org="${repo%%/*}"
115
+ org_branch=$(gh api "repos/$org/.github" --jq .default_branch 2>/dev/null) || org_branch=""
116
+ if [ -n "$org_branch" ]; then
117
+ echo "org repo: $org/.github ($org_branch)"
118
+ scan "$org/.github" "$org_branch" "$org/.github: "
119
+ fi
120
+ if [ "$ban" -eq 1 ]; then echo "RESULT: BAN (a policy file forbids or closes AI-written contributions; never file here)"
121
+ elif [ "$disclose" -eq 1 ]; then echo "RESULT: DISCLOSE (a policy file requires AI use to be disclosed; say so in the body)"
122
+ elif [ "$found" -eq 1 ]; then echo "RESULT: MENTION (a policy file names AI; read the lines above and decide)"
123
+ else echo "RESULT: CLEAN (no policy file names AI)"
124
+ fi
125
+ )
126
+ rc=$?
127
+ printf '%s\n' "$report"
128
+ if [ "$rc" -ne 0 ]; then status=1; continue; fi
129
+ case "$report" in
130
+ *"RESULT: BAN"*) [ "$status" -lt 4 ] && status=4 ;;
131
+ *"RESULT: DISCLOSE"*) [ "$status" -lt 3 ] && status=3 ;;
132
+ esac
133
+ if [ "$out" -eq 1 ]; then
134
+ mkdir -p "$home/policy"
135
+ printf '%s\n' "$report" > "$home/policy/policy-${repo%%/*}-${repo##*/}.md"
136
+ echo "saved: $home/policy/policy-${repo%%/*}-${repo##*/}.md"
137
+ fi
138
+ done
139
+ exit "$status"
@@ -2,19 +2,12 @@
2
2
  # claim-check.sh - lists every claim in outbound text that a maintainer can
3
3
  # falsify, so each one gets re-derived before it is sent.
4
4
  #
5
- # On microsoft/GSL #1272 a reply draft said "The five new static_asserts fail on
6
- # main." A reviewer falsified it in one pass. The number had come from a red arm
7
- # that grepped only for `static assertion failed`, so it counted five matches
8
- # and never saw the twenty-one other errors beside them; and against the commit
9
- # a maintainer would actually revert to, only four of the five fail. The
10
- # sentence named a basis and was still wrong about it.
11
- #
12
- # So this does not try to judge a claim. Text checks cannot: the bad sentence
13
- # named its revision and read perfectly. It lists them, and the rule in step 6
14
- # is that every listed line is re-measured, at the moment of writing, against
15
- # the exact revision the sentence names. The gap this closes is that until now
16
- # every gate here read code and nothing read the prose, while the prose is what
17
- # the maintainer reads first.
5
+ # A count or an absolute in outbound prose is a claim the maintainer can check
6
+ # in one command, and a sentence that names its revision can still be wrong
7
+ # about it. This does not judge a claim, because a text check cannot; it lists
8
+ # every one, and the rule is that each listed line is re-measured, at the
9
+ # moment of writing, against the exact revision the sentence names. The prose
10
+ # is what the maintainer reads first.
18
11
  #
19
12
  # Usage:
20
13
  # bash claim-check.sh FILE