housebroken-cli 0.6.8__tar.gz → 0.7.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.
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/PKG-INFO +13 -10
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/README.md +12 -9
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/bin/housebroken +2 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/pyproject.toml +1 -1
- housebroken_cli-0.7.0/scripts/branch-check.sh +342 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/check-ai-policy.sh +80 -19
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/file-issue.sh +10 -2
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/file-pr.sh +80 -2
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/fork-hygiene.sh +11 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/prior-art.sh +43 -10
- housebroken_cli-0.7.0/scripts/ref-check.sh +114 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/review.sh +39 -3
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/verify-filed-pr.sh +30 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/skills/housebroken/SKILL.md +55 -19
- housebroken_cli-0.7.0/src/housebroken/__init__.py +1 -0
- housebroken_cli-0.6.8/scripts/branch-check.sh +0 -189
- housebroken_cli-0.6.8/src/housebroken/__init__.py +0 -1
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/.gitignore +0 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/LICENSE +0 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/hatch_build.py +0 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/claim-check.sh +0 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/comment-census.sh +0 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/comment-check.sh +0 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/diff-defaults.sh +0 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/distinct-outside.sh +0 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/inbox.sh +0 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/notes.sh +0 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/scripts/pr-sweep.sh +0 -0
- {housebroken_cli-0.6.8 → housebroken_cli-0.7.0}/src/housebroken/cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: housebroken-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Agent-written pull requests that do not make a mess in someone else's house.
|
|
5
5
|
Project-URL: Repository, https://github.com/lenamonj/housebroken
|
|
6
6
|
License-Expression: MIT
|
|
@@ -52,10 +52,10 @@ the CLA known before filing, the SECURITY.md route taken when it applies
|
|
|
52
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
53
|
|
|
54
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.
|
|
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 (READMEs capped at 40, and the cap printed), and prints each sentence that names AI with its reading: BAN, DISCLOSE, MENTION or CLEAN. A file GitHub would not give, a throttled call or a truncated listing reads UNREAD, never CLEAN, and nothing files on it. 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
56
|
|
|
57
57
|
**2. Is it already on the table?**
|
|
58
|
-
`prior-art.sh` lists
|
|
58
|
+
`prior-art.sh` lists the issues and pull requests, open, closed and merged, that touch the file or symbol, with the type taken from the API and the closing ruling quoted for every closed item. A search that matched more than it returned says so, and such a printout is saved only when marked partial. The filing gate requires every file the branch changes to be one of its terms. 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
59
|
|
|
60
60
|
**3. Is it a fix or an opinion?**
|
|
61
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.
|
|
@@ -67,7 +67,7 @@ On a fresh clone of the upstream default branch, the new test fails; with the pa
|
|
|
67
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
68
|
|
|
69
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.
|
|
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, and `ref-check.sh` looks up every issue number, link, test name, file and commit the body names, refusing one that does not exist.
|
|
71
71
|
|
|
72
72
|
**7. Know the paperwork.**
|
|
73
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.
|
|
@@ -76,16 +76,16 @@ Before the first pull request to an organization: CLA, DCO, signed-commit requir
|
|
|
76
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
77
|
|
|
78
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,
|
|
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
|
|
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
|
|
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, any comment added to code, a warning suppression that does not name what it hides or that no build log shows firing, reflection added to a .NET project that declares AOT or trim support without an AOT publish log of it, a credential or a path under your own home, a lockfile, generated file or binary nobody named, an agent's working file, and a file rewritten for its line endings; it notes a base the upstream has moved past, a user path that may be a fixture and every absolute claim in added prose, 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 before it reads the diff it writes its own fix from the bug alone, so it is not anchored on ours; the report names that fix as a diff and the gate refuses one that does not, a POST AS IS over an open BLOCKING or SHOULD FIX finding, and a POST AS IS over items the reviewer skipped. Its attack list starts with every claim the prose makes and includes the form of the code: the reviewer writes the changed block the way the strongest engineer in that language would, in the repository's own idioms, and a smaller or clearer version is a finding; 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 that searched every file the branch changes, a policy printout under a day old that reads neither BAN nor UNREAD, a body that discloses when the policy asks and runs under 120 words of its own beside the project's template, a branch stamp for the exact head whose base the upstream has not since changed under the branch's files, a POST AS IS review of that head and that body, fewer than three open pull requests of yours on the repository, and ten minutes since the last.
|
|
82
82
|
`file-issue.sh` is the only way an issue is opened. It refuses without the same prior-art and policy printouts, refuses the same prose, and refuses a body that mentions a fix, a patch or a pull request without a GitHub link to the branch or commit, so work that exists is attributable to whoever did it.
|
|
83
83
|
|
|
84
84
|
**10. Watch it land.**
|
|
85
|
-
`verify-filed-pr.sh` re-derives from GitHub that the head is the intended commit, the
|
|
85
|
+
`verify-filed-pr.sh` re-derives from GitHub that the head is the intended commit, the files are the ones the branch stamp lists, the body is the one that was filed, 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.
|
|
86
86
|
|
|
87
87
|
**11. Clean up.**
|
|
88
|
-
`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
|
+
`fork-hygiene.sh` deletes the fork branch of every merged or closed pull request, unless an open one still uses it, and lists forks with no pull request left. No planning file, agent directory, journal or build output ever enters a diff.
|
|
89
89
|
|
|
90
90
|
**12. Three per repository, one finding each.**
|
|
91
91
|
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.
|
|
@@ -121,7 +121,8 @@ uv tool install git+https://github.com/lenamonj/housebroken
|
|
|
121
121
|
| `scripts/comment-census.sh` | added code versus added comments, per branch |
|
|
122
122
|
| `scripts/diff-defaults.sh` | default arguments and one-line wrappers added to keep call sites untouched |
|
|
123
123
|
| `scripts/claim-check.sh` | every falsifiable claim in a body or a reply, listed to re-derive |
|
|
124
|
-
| `scripts/
|
|
124
|
+
| `scripts/ref-check.sh` | every issue, link, test, file and commit a body names, looked up |
|
|
125
|
+
| `scripts/branch-check.sh` | the branch's mechanical facts: clean tree, live base, modes, artifacts, secrets, generated files, trailers, sign-offs, comments, suppressions, absolute claims |
|
|
125
126
|
| `scripts/review.sh` | the adversarial review: a different model attacks the change, and the filing is held to exactly what it read |
|
|
126
127
|
| `scripts/file-pr.sh` | the only way a pull request gets filed: five gates in front of `gh pr create` |
|
|
127
128
|
| `scripts/file-issue.sh` | the only way an issue gets filed: the same printouts and prose, and a fix offered is a fix linked |
|
|
@@ -137,6 +138,8 @@ The scripts are bash and need `gh` and `jq`. Each script's header says what it a
|
|
|
137
138
|
|
|
138
139
|
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.
|
|
139
140
|
|
|
141
|
+
The gates stop carelessness, not deceit. A stamp, a review report or a blind fix is a file the agent can write, and no script on the same machine can tell an honest one from a forged one. What the gates guarantee is that nothing files by accident: a step skipped, a check that failed, a base that moved, a reference to nothing.
|
|
142
|
+
|
|
140
143
|
## FAQ
|
|
141
144
|
|
|
142
145
|
**Is this only for AI-written pull requests?**
|
|
@@ -40,10 +40,10 @@ the CLA known before filing, the SECURITY.md route taken when it applies
|
|
|
40
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
41
|
|
|
42
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.
|
|
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 (READMEs capped at 40, and the cap printed), and prints each sentence that names AI with its reading: BAN, DISCLOSE, MENTION or CLEAN. A file GitHub would not give, a throttled call or a truncated listing reads UNREAD, never CLEAN, and nothing files on it. 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
44
|
|
|
45
45
|
**2. Is it already on the table?**
|
|
46
|
-
`prior-art.sh` lists
|
|
46
|
+
`prior-art.sh` lists the issues and pull requests, open, closed and merged, that touch the file or symbol, with the type taken from the API and the closing ruling quoted for every closed item. A search that matched more than it returned says so, and such a printout is saved only when marked partial. The filing gate requires every file the branch changes to be one of its terms. 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
47
|
|
|
48
48
|
**3. Is it a fix or an opinion?**
|
|
49
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.
|
|
@@ -55,7 +55,7 @@ On a fresh clone of the upstream default branch, the new test fails; with the pa
|
|
|
55
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
56
|
|
|
57
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.
|
|
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, and `ref-check.sh` looks up every issue number, link, test name, file and commit the body names, refusing one that does not exist.
|
|
59
59
|
|
|
60
60
|
**7. Know the paperwork.**
|
|
61
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.
|
|
@@ -64,16 +64,16 @@ Before the first pull request to an organization: CLA, DCO, signed-commit requir
|
|
|
64
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
65
|
|
|
66
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,
|
|
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
|
|
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
|
|
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, any comment added to code, a warning suppression that does not name what it hides or that no build log shows firing, reflection added to a .NET project that declares AOT or trim support without an AOT publish log of it, a credential or a path under your own home, a lockfile, generated file or binary nobody named, an agent's working file, and a file rewritten for its line endings; it notes a base the upstream has moved past, a user path that may be a fixture and every absolute claim in added prose, 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 before it reads the diff it writes its own fix from the bug alone, so it is not anchored on ours; the report names that fix as a diff and the gate refuses one that does not, a POST AS IS over an open BLOCKING or SHOULD FIX finding, and a POST AS IS over items the reviewer skipped. Its attack list starts with every claim the prose makes and includes the form of the code: the reviewer writes the changed block the way the strongest engineer in that language would, in the repository's own idioms, and a smaller or clearer version is a finding; 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 that searched every file the branch changes, a policy printout under a day old that reads neither BAN nor UNREAD, a body that discloses when the policy asks and runs under 120 words of its own beside the project's template, a branch stamp for the exact head whose base the upstream has not since changed under the branch's files, a POST AS IS review of that head and that body, fewer than three open pull requests of yours on the repository, and ten minutes since the last.
|
|
70
70
|
`file-issue.sh` is the only way an issue is opened. It refuses without the same prior-art and policy printouts, refuses the same prose, and refuses a body that mentions a fix, a patch or a pull request without a GitHub link to the branch or commit, so work that exists is attributable to whoever did it.
|
|
71
71
|
|
|
72
72
|
**10. Watch it land.**
|
|
73
|
-
`verify-filed-pr.sh` re-derives from GitHub that the head is the intended commit, the
|
|
73
|
+
`verify-filed-pr.sh` re-derives from GitHub that the head is the intended commit, the files are the ones the branch stamp lists, the body is the one that was filed, 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.
|
|
74
74
|
|
|
75
75
|
**11. Clean up.**
|
|
76
|
-
`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
|
+
`fork-hygiene.sh` deletes the fork branch of every merged or closed pull request, unless an open one still uses it, and lists forks with no pull request left. No planning file, agent directory, journal or build output ever enters a diff.
|
|
77
77
|
|
|
78
78
|
**12. Three per repository, one finding each.**
|
|
79
79
|
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.
|
|
@@ -109,7 +109,8 @@ uv tool install git+https://github.com/lenamonj/housebroken
|
|
|
109
109
|
| `scripts/comment-census.sh` | added code versus added comments, per branch |
|
|
110
110
|
| `scripts/diff-defaults.sh` | default arguments and one-line wrappers added to keep call sites untouched |
|
|
111
111
|
| `scripts/claim-check.sh` | every falsifiable claim in a body or a reply, listed to re-derive |
|
|
112
|
-
| `scripts/
|
|
112
|
+
| `scripts/ref-check.sh` | every issue, link, test, file and commit a body names, looked up |
|
|
113
|
+
| `scripts/branch-check.sh` | the branch's mechanical facts: clean tree, live base, modes, artifacts, secrets, generated files, trailers, sign-offs, comments, suppressions, absolute claims |
|
|
113
114
|
| `scripts/review.sh` | the adversarial review: a different model attacks the change, and the filing is held to exactly what it read |
|
|
114
115
|
| `scripts/file-pr.sh` | the only way a pull request gets filed: five gates in front of `gh pr create` |
|
|
115
116
|
| `scripts/file-issue.sh` | the only way an issue gets filed: the same printouts and prose, and a fix offered is a fix linked |
|
|
@@ -125,6 +126,8 @@ The scripts are bash and need `gh` and `jq`. Each script's header says what it a
|
|
|
125
126
|
|
|
126
127
|
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.
|
|
127
128
|
|
|
129
|
+
The gates stop carelessness, not deceit. A stamp, a review report or a blind fix is a file the agent can write, and no script on the same machine can tell an honest one from a forged one. What the gates guarantee is that nothing files by accident: a step skipped, a check that failed, a base that moved, a reference to nothing.
|
|
130
|
+
|
|
128
131
|
## FAQ
|
|
129
132
|
|
|
130
133
|
**Is this only for AI-written pull requests?**
|
|
@@ -50,6 +50,7 @@ usage: housebroken <subcommand> [args...]
|
|
|
50
50
|
6 census added code lines against added comment lines, per branch
|
|
51
51
|
6 comments refuse a comment added to code nobody asked for
|
|
52
52
|
6 claims every falsifiable claim in outbound text, listed to re-derive
|
|
53
|
+
6 refs every issue, link, test, file and commit the text names, looked up
|
|
53
54
|
9 branch the mechanical facts about the branch: tree, modes, artifacts, claims
|
|
54
55
|
9 review a different model attacks the change before it is filed
|
|
55
56
|
9 file the only way a pull request gets filed
|
|
@@ -96,6 +97,7 @@ case "$cmd" in
|
|
|
96
97
|
census) script=comment-census.sh ;;
|
|
97
98
|
comments) script=comment-check.sh ;;
|
|
98
99
|
claims) script=claim-check.sh ;;
|
|
100
|
+
refs) script=ref-check.sh ;;
|
|
99
101
|
branch) script=branch-check.sh ;;
|
|
100
102
|
review) script=review.sh ;;
|
|
101
103
|
file) script=file-pr.sh ;;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# branch-check.sh - the mechanical facts about the branch you are about to file.
|
|
3
|
+
#
|
|
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); a warning suppression the
|
|
11
|
+
# branch adds, until a build log shows the warning it suppresses firing; and
|
|
12
|
+
# reflection added to a .NET project that declares itself AOT compatible or
|
|
13
|
+
# trimmable, until an AOT publish log is given. shouldly #1335 suppressed an
|
|
14
|
+
# IL2075 that never fired, and the reflection under it found nothing under
|
|
15
|
+
# Native AOT. Absolute claims in added prose are printed for you to falsify one
|
|
16
|
+
# by one; some of them are true.
|
|
17
|
+
#
|
|
18
|
+
# A branch that passes gets a stamp at $HOUSEBROKEN_HOME/branch-checks/<head>,
|
|
19
|
+
# which file-pr.sh requires for the head it files.
|
|
20
|
+
#
|
|
21
|
+
# Usage:
|
|
22
|
+
# bash branch-check.sh [CLONE] [--base REF] [--asked URL]
|
|
23
|
+
# [--warning-log FILE] [--aot-log FILE]
|
|
24
|
+
# bash branch-check.sh --help
|
|
25
|
+
#
|
|
26
|
+
# CLONE defaults to the current directory. REF defaults to the first of
|
|
27
|
+
# upstream/main, upstream/master, origin/main, origin/master that resolves.
|
|
28
|
+
# --asked URL is the maintainer's request for documentation, which turns the
|
|
29
|
+
# comment refusal into a check. --warning-log FILE is a build log of the
|
|
30
|
+
# branch without its suppressions; it must name every warning id they suppress.
|
|
31
|
+
# --aot-log FILE is the output of an AOT publish (dotnet publish
|
|
32
|
+
# -p:PublishAot=true) of the branch, run and read.
|
|
33
|
+
#
|
|
34
|
+
# Exits 0 when nothing blocks, 1 on a refusal, 2 on a usage error.
|
|
35
|
+
set -u
|
|
36
|
+
|
|
37
|
+
usage() { awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0"; }
|
|
38
|
+
|
|
39
|
+
here=$(cd "$(dirname "$0")" && pwd)
|
|
40
|
+
home="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
|
|
41
|
+
clone="."
|
|
42
|
+
base=""
|
|
43
|
+
asked=""
|
|
44
|
+
warnlog=""
|
|
45
|
+
aotlog=""
|
|
46
|
+
allows=()
|
|
47
|
+
while [ "$#" -gt 0 ]; do
|
|
48
|
+
case "$1" in
|
|
49
|
+
--help|-h) usage; exit 0 ;;
|
|
50
|
+
--base)
|
|
51
|
+
shift
|
|
52
|
+
base="${1:-}"
|
|
53
|
+
[ -n "$base" ] || { echo "REFUSED: --base needs a ref" >&2; exit 2; }
|
|
54
|
+
;;
|
|
55
|
+
--base=*) base="${1#--base=}" ;;
|
|
56
|
+
--asked)
|
|
57
|
+
shift
|
|
58
|
+
asked="${1:-}"
|
|
59
|
+
[ -n "$asked" ] || { echo "REFUSED: --asked needs the URL of the maintainer's ask" >&2; exit 2; }
|
|
60
|
+
;;
|
|
61
|
+
--asked=*) asked="${1#--asked=}" ;;
|
|
62
|
+
--warning-log)
|
|
63
|
+
shift
|
|
64
|
+
warnlog="${1:-}"
|
|
65
|
+
[ -f "$warnlog" ] || { echo "REFUSED: --warning-log needs a build log file" >&2; exit 2; }
|
|
66
|
+
;;
|
|
67
|
+
--aot-log)
|
|
68
|
+
shift
|
|
69
|
+
aotlog="${1:-}"
|
|
70
|
+
[ -f "$aotlog" ] || { echo "REFUSED: --aot-log needs an AOT publish log file" >&2; exit 2; }
|
|
71
|
+
;;
|
|
72
|
+
--allow)
|
|
73
|
+
shift
|
|
74
|
+
[ -n "${1:-}" ] || { echo "REFUSED: --allow needs a path" >&2; exit 2; }
|
|
75
|
+
allows+=("$1")
|
|
76
|
+
;;
|
|
77
|
+
-*) echo "REFUSED: unknown option $1" >&2; exit 2 ;;
|
|
78
|
+
*) clone="$1" ;;
|
|
79
|
+
esac
|
|
80
|
+
shift
|
|
81
|
+
done
|
|
82
|
+
|
|
83
|
+
cd "$clone" 2>/dev/null || { echo "REFUSED: $clone is not a directory" >&2; exit 2; }
|
|
84
|
+
git rev-parse --git-dir >/dev/null 2>&1 || { echo "REFUSED: $clone is not a git clone" >&2; exit 2; }
|
|
85
|
+
|
|
86
|
+
if [ -z "$base" ]; then
|
|
87
|
+
for candidate in upstream/main upstream/master origin/main origin/master; do
|
|
88
|
+
if git rev-parse --verify -q "$candidate" >/dev/null 2>&1; then base="$candidate"; break; fi
|
|
89
|
+
done
|
|
90
|
+
fi
|
|
91
|
+
[ -n "$base" ] || { echo "REFUSED: no base ref resolved - pass --base REF" >&2; exit 2; }
|
|
92
|
+
git rev-parse --verify -q "$base" >/dev/null 2>&1 || { echo "REFUSED: base $base does not resolve" >&2; exit 2; }
|
|
93
|
+
|
|
94
|
+
blocked=0
|
|
95
|
+
block() { echo "REFUSED: $*"; blocked=1; }
|
|
96
|
+
# Whether the base already carries a path. "rev:path" is not used because Git
|
|
97
|
+
# for Windows' shell rewrites an argument shaped like "a/b:.c/d" as a path list.
|
|
98
|
+
in_base() { [ -n "$(git ls-tree --name-only "$base" -- "$1" 2>/dev/null)" ]; }
|
|
99
|
+
|
|
100
|
+
echo "== branch check: $(git rev-parse --abbrev-ref HEAD) against $base =="
|
|
101
|
+
|
|
102
|
+
# What you tested is what you file, or you do not know what you are filing.
|
|
103
|
+
dirty=$(git status --porcelain)
|
|
104
|
+
if [ -n "$dirty" ]; then
|
|
105
|
+
block "the working tree is dirty, so the commits are not what you tested"
|
|
106
|
+
printf '%s\n' "$dirty" | sed 's/^/ /'
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# A base nobody fetched is the upstream as it was, not as it is: ask the remote.
|
|
110
|
+
full=$(git rev-parse --symbolic-full-name "$base" 2>/dev/null)
|
|
111
|
+
case "$full" in
|
|
112
|
+
refs/remotes/*/*)
|
|
113
|
+
rest="${full#refs/remotes/}"; remote="${rest%%/*}"; rbranch="${rest#*/}"
|
|
114
|
+
live=$(git ls-remote "$remote" "refs/heads/$rbranch" 2>/dev/null | awk '{print $1}')
|
|
115
|
+
if [ -z "$live" ]; then
|
|
116
|
+
block "cannot read $rbranch from $remote, so whether $base is current is unknown"
|
|
117
|
+
elif [ "$live" != "$(git rev-parse "$base")" ]; then
|
|
118
|
+
echo "CHECK: the base moved: $remote has $rbranch at ${live:0:12}, $base is $(git rev-parse --short=12 "$base") - file-pr.sh refuses when the new commits touch this branch's files"
|
|
119
|
+
fi
|
|
120
|
+
;;
|
|
121
|
+
*) block "base $base is not a remote-tracking ref, so whether it is current cannot be checked - pass --base <remote>/<branch>" ;;
|
|
122
|
+
esac
|
|
123
|
+
|
|
124
|
+
if ! git merge-base --is-ancestor "$base" HEAD 2>/dev/null; then
|
|
125
|
+
block "HEAD is not on top of $base - rebase before filing"
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
files=$(git diff --name-only "$base..HEAD" 2>/dev/null)
|
|
129
|
+
if [ -z "$files" ]; then
|
|
130
|
+
block "no files differ from $base - there is nothing to file"
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
while IFS= read -r f; do
|
|
134
|
+
[ -n "$f" ] || continue
|
|
135
|
+
entry=$(git ls-files -s -- "$f" 2>/dev/null)
|
|
136
|
+
[ -n "$entry" ] || continue
|
|
137
|
+
mode=$(printf '%s\n' "$entry" | awk '{print $1}')
|
|
138
|
+
|
|
139
|
+
# A mode is only wrong relative to the file's own neighbours, and only the
|
|
140
|
+
# branch's to answer for when the base does not already carry it.
|
|
141
|
+
base_mode=$(git ls-tree "$base" -- "$f" 2>/dev/null | awk '{print $1}')
|
|
142
|
+
if [ "$mode" = "100755" ] && [ "$base_mode" != "100755" ]; then
|
|
143
|
+
case "$f" in
|
|
144
|
+
*.*) ext=".${f##*.}" ;;
|
|
145
|
+
*) ext="" ;;
|
|
146
|
+
esac
|
|
147
|
+
if [ -n "$ext" ]; then
|
|
148
|
+
sibs=$(git ls-files -s -- "*$ext" 2>/dev/null | awk -v self="$f" '$4 != self {print $1}')
|
|
149
|
+
norm=$(printf '%s\n' "$sibs" | grep -c '^100644$')
|
|
150
|
+
exec=$(printf '%s\n' "$sibs" | grep -c '^100755$')
|
|
151
|
+
if [ "$norm" -gt "$exec" ]; then
|
|
152
|
+
block "$f is committed 100755 but $norm of its $ext siblings are 100644"
|
|
153
|
+
fi
|
|
154
|
+
fi
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# The repository's own .gitignore is the authority on what is build output,
|
|
158
|
+
# but only for a file this branch adds. Ignore rules never apply to a file the
|
|
159
|
+
# project already tracks, which is why check-ignore skips tracked files unless
|
|
160
|
+
# forced. NVIDIA/go-nvml ignores "dl" for a built binary, and that pattern also
|
|
161
|
+
# matches the directory pkg/dl, so forcing the check called two upstream source
|
|
162
|
+
# files build output.
|
|
163
|
+
if ! in_base "$f"; then
|
|
164
|
+
if git check-ignore -q --no-index -- "$f" 2>/dev/null; then
|
|
165
|
+
block "$f is added by this branch and the repository's .gitignore excludes it"
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
case "$f" in
|
|
170
|
+
.jeffy/*|*/.jeffy/*|PLAN.md|*/PLAN.md|JOURNAL.md|*/JOURNAL.md|*.orig|*.rej|*.bak|*.swp|.DS_Store|*/.DS_Store|Thumbs.db|*/Thumbs.db)
|
|
171
|
+
block "$f is a working artifact and has no place in the diff" ;;
|
|
172
|
+
esac
|
|
173
|
+
# An agent's instruction or scratch file is an artifact when this branch adds
|
|
174
|
+
# it; a project that already keeps one may have it edited.
|
|
175
|
+
case "$f" in
|
|
176
|
+
.claude/*|*/.claude/*|.cursor/*|*/.cursor/*|CLAUDE.md|*/CLAUDE.md|AGENTS.md|*/AGENTS.md|GEMINI.md|*/GEMINI.md|STATUS.md|*/STATUS.md|NOTES.md|*/NOTES.md|TODO.md|*/TODO.md)
|
|
177
|
+
in_base "$f" || block "$f is added by this branch and reads as an agent's working file" ;;
|
|
178
|
+
esac
|
|
179
|
+
done <<EOF
|
|
180
|
+
$files
|
|
181
|
+
EOF
|
|
182
|
+
|
|
183
|
+
# A tool trailer is the one thing in a commit that cannot be argued as style.
|
|
184
|
+
# Trailers are matched at the start of a line and a tool by its commit
|
|
185
|
+
# identity, so a subject that only names a trailer is not refused.
|
|
186
|
+
trailers=$(git log --format='%h author %an <%ae>, committer %cn <%ce>%n%b' "$base..HEAD" 2>/dev/null \
|
|
187
|
+
| 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')
|
|
188
|
+
if [ -n "$trailers" ]; then
|
|
189
|
+
block "a commit carries a tool trailer or a session link, or a tool identity, which no project asked for"
|
|
190
|
+
printf '%s\n' "$trailers" | sed 's/^/ /'
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
# DCO wants a human being. A one-word name is taken for a username whatever
|
|
194
|
+
# its case; a real one-word name is refused as well, which is the price of
|
|
195
|
+
# catching them.
|
|
196
|
+
usernames=$(git log --format='%b' "$base..HEAD" 2>/dev/null | grep -E '^Signed-off-by: [^ <]+ <')
|
|
197
|
+
if [ -n "$usernames" ]; then
|
|
198
|
+
block "a Signed-off-by names a username; the DCO asks for a real name"
|
|
199
|
+
printf '%s\n' "$usernames" | sed 's/^/ /'
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# A comment added to somebody else's code is refused unless its maintainer
|
|
203
|
+
# asked for it; a reworded comment and a new file's opening header pass.
|
|
204
|
+
comment_args=(--base "$base")
|
|
205
|
+
[ -n "$asked" ] && comment_args+=(--asked "$asked")
|
|
206
|
+
comments=$(bash "$here/comment-check.sh" . "${comment_args[@]}" 2>&1); comment_rc=$?
|
|
207
|
+
case "$comment_rc" in
|
|
208
|
+
0) ;;
|
|
209
|
+
1) block "the branch adds comments to code that nobody asked for"
|
|
210
|
+
printf '%s\n' "$comments" | grep -E '^(REFUSED|CHECK): ' | sed 's/^REFUSED: / /' ;;
|
|
211
|
+
*) block "comment-check.sh could not run: $comments" ;;
|
|
212
|
+
esac
|
|
213
|
+
printf '%s\n' "$comments" | grep -E '^CHECK: ' | sed 's/^/ /'
|
|
214
|
+
|
|
215
|
+
# A suppression hides a warning from the maintainer too, so it has to be shown
|
|
216
|
+
# that the warning exists. A suppression covering nothing usually covers code
|
|
217
|
+
# that is wrong in a way the analyzer cannot see.
|
|
218
|
+
added=$(git diff -U0 "$base..HEAD" -- . ':(exclude)*.md' ':(exclude)*.rst' ':(exclude)*.txt' 2>/dev/null \
|
|
219
|
+
| grep '^+' | grep -v '^+++' | sed 's/^+//')
|
|
220
|
+
suppressions=$(printf '%s\n' "$added" | grep -E 'UnconditionalSuppressMessage|SuppressMessage\(|#pragma warning disable|pragma warning *\( *disable|<NoWarn>|@SuppressWarnings|@SuppressLint|@Suppress\(|#!?\[allow\(|nolint|NOLINT|NOSONAR|noqa|eslint-disable|@ts-ignore|@ts-expect-error|type: *ignore|pylint: *disable|rubocop:disable|swiftlint:disable|phpstan-ignore|psalm-suppress')
|
|
221
|
+
if [ -n "$suppressions" ]; then
|
|
222
|
+
# Each suppression names what it hides: a code such as IL2075 or C4996, else
|
|
223
|
+
# the rule names after its keyword (errcheck, no-unused-vars). One that names
|
|
224
|
+
# nothing hides every warning on its line, and no log can show what it covers.
|
|
225
|
+
unshown=""
|
|
226
|
+
while IFS= read -r line; do
|
|
227
|
+
[ -n "$line" ] || continue
|
|
228
|
+
ids=$(printf '%s\n' "$line" | grep -oE '\b([A-Za-z]{1,6}[0-9]{3,5}|[0-9]{4})\b' | sort -u)
|
|
229
|
+
if [ -z "$ids" ]; then
|
|
230
|
+
ids=$(printf '%s\n' "$line" | sed -E 's/ -- .*//; s/.*(nolint|NOLINT|NOSONAR|noqa|eslint-disable(-next-line|-line)?|pylint: *disable|rubocop:disable|swiftlint:disable(:next|:this)?|phpstan-ignore(-next-line|-line)?|psalm-suppress|#!?\[allow|@Suppress(Warnings|Lint)?|SuppressMessage|ts-ignore|ts-expect-error|type: *ignore)//; s/ *(\/\/|#).*//' \
|
|
231
|
+
| grep -oE '[A-Za-z][A-Za-z0-9_-]{2,}' | grep -vxE 'disable|enable|next|line|this' | sort -u)
|
|
232
|
+
fi
|
|
233
|
+
if [ -z "$ids" ]; then
|
|
234
|
+
unshown="$unshown [names no warning: $(printf '%s' "$line" | sed 's/^[[:space:]]*//' | cut -c1-60)]"
|
|
235
|
+
elif [ -z "$warnlog" ]; then
|
|
236
|
+
unshown="$unshown $(printf '%s' "$ids" | tr '\n' ' ')(no --warning-log given)"
|
|
237
|
+
else
|
|
238
|
+
for id in $ids; do grep -qF -- "$id" "$warnlog" || unshown="$unshown $id"; done
|
|
239
|
+
fi
|
|
240
|
+
done <<< "$suppressions"
|
|
241
|
+
if [ -n "$unshown" ]; then
|
|
242
|
+
block "the branch adds a warning suppression, and no build log shows the warning firing ($unshown)"
|
|
243
|
+
printf '%s\n' "$suppressions" | sed 's/^/ /'
|
|
244
|
+
fi
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
# The trim analyzer does not flag object.GetType().GetProperty(...), so a clean
|
|
248
|
+
# build proves nothing about reflection under Native AOT; only a publish does.
|
|
249
|
+
aotdecl=$(git grep -lE '<(IsAotCompatible|IsTrimmable|PublishAot|EnableTrimAnalyzer|EnableAotAnalyzer)( [^>]*)?>[[:space:]]*true' HEAD -- '*.csproj' '*.props' '*.targets' 2>/dev/null \
|
|
250
|
+
| sed 's/^HEAD://' | tr '\n' ' ')
|
|
251
|
+
if [ -n "$aotdecl" ]; then
|
|
252
|
+
reflection=$(git diff -U0 "$base..HEAD" -- '*.cs' 2>/dev/null | grep '^+' | grep -v '^+++' | sed 's/^+//' \
|
|
253
|
+
| grep -E '\.Get(Property|Properties|Method|Methods|Field|Fields|Member|Members|Interfaces)\(|Activator\.CreateInstance|MakeGenericType|MakeGenericMethod|Type\.GetType\(|\.InvokeMember\(')
|
|
254
|
+
# The log has to be a publish of this repository: it names, as a word, a
|
|
255
|
+
# declaring project, or any project here when the declaration is in a .props.
|
|
256
|
+
published=0
|
|
257
|
+
if [ -n "$aotlog" ] && grep -q "Generating native code" "$aotlog"; then
|
|
258
|
+
for p in $aotdecl; do
|
|
259
|
+
case "$p" in
|
|
260
|
+
*.csproj) projs=$(basename "$p" .csproj) ;;
|
|
261
|
+
*) projs=$(git ls-files '*.csproj' | sed 's#.*/##; s/\.csproj$//') ;;
|
|
262
|
+
esac
|
|
263
|
+
for proj in $projs; do grep -qwF -- "$proj" "$aotlog" && published=1; done
|
|
264
|
+
done
|
|
265
|
+
fi
|
|
266
|
+
if [ -n "$reflection" ] && [ "$published" -eq 0 ]; then
|
|
267
|
+
block "the branch adds reflection to a project that declares AOT or trim support (${aotdecl% }), and no --aot-log shows an AOT publish of it"
|
|
268
|
+
printf '%s\n' "$reflection" | sed 's/^/ /'
|
|
269
|
+
fi
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
# What embarrasses a contributor first, file by file: a secret or a personal
|
|
273
|
+
# path in an added line, a lockfile or generated file, a binary, and a file
|
|
274
|
+
# rewritten for its line endings or whitespace. --allow PATH names a file the
|
|
275
|
+
# change really is about, such as a dependency bump's lockfile.
|
|
276
|
+
allowed() { local a; for a in ${allows[@]+"${allows[@]}"}; do [ "$1" = "$a" ] && return 0; done; return 1; }
|
|
277
|
+
# A credential anywhere is refused, and so is a path under this user's own
|
|
278
|
+
# home; a path under another user's is a fixture or an example, and is noted.
|
|
279
|
+
me=$(basename "${HOME:-/nobody}")
|
|
280
|
+
leak_re="gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY|xox[baprs]-[A-Za-z0-9-]{10,}|sk-[A-Za-z0-9_-]{20,}|[A-Za-z]:[\\\\/]Users[\\\\/]$me([\\\\/ ]|\$)|/home/$me/|/Users/$me/|/mnt/[a-z]/Users/$me/"
|
|
281
|
+
path_re='[A-Za-z]:[\\/]Users[\\/][^\\/ ]+|/home/[a-z][a-z0-9_-]*/|/Users/[A-Za-z][^/ ]*/|/mnt/[a-z]/Users/'
|
|
282
|
+
nfiles=0; nadded=0
|
|
283
|
+
while IFS= read -r f; do
|
|
284
|
+
[ -n "$f" ] || continue
|
|
285
|
+
nfiles=$((nfiles + 1))
|
|
286
|
+
stat=$(git diff --numstat "$base..HEAD" -- "$f" | head -1)
|
|
287
|
+
case "$f" in *test*|*spec*) ;; *) nadded=$((nadded + $(printf '%s' "$stat" | awk '{print ($1 == "-") ? 0 : $1}'))) ;; esac
|
|
288
|
+
allowed "$f" && continue
|
|
289
|
+
addedf=$(git diff -U0 "$base..HEAD" -- "$f" | grep '^+' | grep -v '^+++')
|
|
290
|
+
leaks=$(printf '%s\n' "$addedf" | grep -oE "$leak_re" | sort -u)
|
|
291
|
+
[ -z "$leaks" ] || block "$f adds a personal path or a credential: $(printf '%s' "$leaks" | tr '\n' ' ')"
|
|
292
|
+
paths=$(printf '%s\n' "$addedf" | grep -oE "$path_re" | sort -u)
|
|
293
|
+
[ -z "$paths" ] || echo "CHECK: $f adds a user path, a fixture or an example unless it is yours: $(printf '%s' "$paths" | tr '\n' ' ')"
|
|
294
|
+
case "$f" in
|
|
295
|
+
package-lock.json|*/package-lock.json|yarn.lock|*/yarn.lock|pnpm-lock.yaml|*/pnpm-lock.yaml|Cargo.lock|*/Cargo.lock|go.sum|*/go.sum|poetry.lock|*/poetry.lock|uv.lock|*/uv.lock|Gemfile.lock|*/Gemfile.lock|composer.lock|*/composer.lock|packages.lock.json|*/packages.lock.json|vendor/*|*/vendor/*|node_modules/*|*/node_modules/*|*.min.js|*.pb.go|*_generated.*|*.g.cs|*.designer.cs|*.Designer.cs)
|
|
296
|
+
block "$f is a lockfile, vendored or generated file - a build rewrote it, or pass --allow $f if the change is about it" ;;
|
|
297
|
+
esac
|
|
298
|
+
case "$stat" in
|
|
299
|
+
-*) block "$f is binary - pass --allow $f if the change needs it" ;;
|
|
300
|
+
*)
|
|
301
|
+
total=$(printf '%s' "$stat" | awk '{print $1 + $2}')
|
|
302
|
+
real=$(git diff --numstat --ignore-space-at-eol --ignore-cr-at-eol "$base..HEAD" -- "$f" | awk '{print $1 + $2}')
|
|
303
|
+
if [ "$total" -ge 10 ] && [ $((${real:-0} * 2)) -lt "$total" ]; then
|
|
304
|
+
block "$f changes $total lines, of which ${real:-0} differ beyond whitespace and line endings - restore the file's own endings"
|
|
305
|
+
fi
|
|
306
|
+
;;
|
|
307
|
+
esac
|
|
308
|
+
done <<EOF
|
|
309
|
+
$files
|
|
310
|
+
EOF
|
|
311
|
+
if [ "$nfiles" -gt 4 ] || [ "$nadded" -gt 150 ]; then
|
|
312
|
+
echo "CHECK: $nfiles files and $nadded added lines outside tests - one finding, the smallest diff that fixes it"
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
# Absolutes in added prose are the claims a maintainer can falsify in one grep.
|
|
316
|
+
claims=$(git diff -U0 "$base..HEAD" -- '*.md' '*.mdx' '*.rst' '*.txt' 2>/dev/null \
|
|
317
|
+
| grep '^+' | grep -v '^+++' | sed 's/^+//' \
|
|
318
|
+
| grep -nEi '\b(only|exactly|always|never|guaranteed|nothing|everything)\b')
|
|
319
|
+
if [ -n "$claims" ]; then
|
|
320
|
+
echo "CHECK: added prose makes absolute claims - falsify each one against the code:"
|
|
321
|
+
printf '%s\n' "$claims" | sed 's/^/ /'
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
echo
|
|
325
|
+
echo "commits:"
|
|
326
|
+
git log --oneline "$base..HEAD" | sed 's/^/ /'
|
|
327
|
+
echo "diff (against the merge base, which is what the maintainer sees):"
|
|
328
|
+
git diff --stat "$(git merge-base "$base" HEAD)..HEAD" | sed 's/^/ /'
|
|
329
|
+
|
|
330
|
+
if [ "$blocked" -ne 0 ]; then
|
|
331
|
+
echo
|
|
332
|
+
echo "branch-check: refused. Fix the causes above; a refusal is a result."
|
|
333
|
+
exit 1
|
|
334
|
+
fi
|
|
335
|
+
# The stamp file-pr.sh reads: this head passed, against this base, at this
|
|
336
|
+
# time, changing these files.
|
|
337
|
+
head=$(git rev-parse HEAD)
|
|
338
|
+
mkdir -p "$home/branch-checks"
|
|
339
|
+
printf 'head: %s\nbase: %s\nbase-ref: %s\nchecked: %s\n' "$head" "$(git rev-parse "$base")" "$base" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$home/branch-checks/$head"
|
|
340
|
+
printf '%s\n' "$files" | sed '/^$/d; s/^/file: /' >> "$home/branch-checks/$head"
|
|
341
|
+
echo
|
|
342
|
+
echo "branch-check: nothing mechanical blocks this branch."
|