housebroken-cli 0.6.9__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.9 → housebroken_cli-0.7.0}/PKG-INFO +13 -10
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/README.md +12 -9
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/bin/housebroken +2 -0
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/pyproject.toml +1 -1
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/branch-check.sh +111 -12
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/check-ai-policy.sh +80 -19
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/file-issue.sh +10 -2
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/file-pr.sh +80 -2
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/fork-hygiene.sh +11 -0
- {housebroken_cli-0.6.9 → 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.9 → housebroken_cli-0.7.0}/scripts/review.sh +16 -1
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/verify-filed-pr.sh +30 -0
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/skills/housebroken/SKILL.md +47 -21
- housebroken_cli-0.7.0/src/housebroken/__init__.py +1 -0
- housebroken_cli-0.6.9/src/housebroken/__init__.py +0 -1
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/.gitignore +0 -0
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/LICENSE +0 -0
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/hatch_build.py +0 -0
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/claim-check.sh +0 -0
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/comment-census.sh +0 -0
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/comment-check.sh +0 -0
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/diff-defaults.sh +0 -0
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/distinct-outside.sh +0 -0
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/inbox.sh +0 -0
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/notes.sh +0 -0
- {housebroken_cli-0.6.9 → housebroken_cli-0.7.0}/scripts/pr-sweep.sh +0 -0
- {housebroken_cli-0.6.9 → 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, any comment added to code, a warning suppression no build log shows firing,
|
|
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 and the gate refuses one that does not. 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, 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, any comment added to code, a warning suppression no build log shows firing,
|
|
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 and the gate refuses one that does not. 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, 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 ;;
|
|
@@ -43,6 +43,7 @@ base=""
|
|
|
43
43
|
asked=""
|
|
44
44
|
warnlog=""
|
|
45
45
|
aotlog=""
|
|
46
|
+
allows=()
|
|
46
47
|
while [ "$#" -gt 0 ]; do
|
|
47
48
|
case "$1" in
|
|
48
49
|
--help|-h) usage; exit 0 ;;
|
|
@@ -68,6 +69,11 @@ while [ "$#" -gt 0 ]; do
|
|
|
68
69
|
aotlog="${1:-}"
|
|
69
70
|
[ -f "$aotlog" ] || { echo "REFUSED: --aot-log needs an AOT publish log file" >&2; exit 2; }
|
|
70
71
|
;;
|
|
72
|
+
--allow)
|
|
73
|
+
shift
|
|
74
|
+
[ -n "${1:-}" ] || { echo "REFUSED: --allow needs a path" >&2; exit 2; }
|
|
75
|
+
allows+=("$1")
|
|
76
|
+
;;
|
|
71
77
|
-*) echo "REFUSED: unknown option $1" >&2; exit 2 ;;
|
|
72
78
|
*) clone="$1" ;;
|
|
73
79
|
esac
|
|
@@ -87,6 +93,9 @@ git rev-parse --verify -q "$base" >/dev/null 2>&1 || { echo "REFUSED: base $base
|
|
|
87
93
|
|
|
88
94
|
blocked=0
|
|
89
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)" ]; }
|
|
90
99
|
|
|
91
100
|
echo "== branch check: $(git rev-parse --abbrev-ref HEAD) against $base =="
|
|
92
101
|
|
|
@@ -97,6 +106,21 @@ if [ -n "$dirty" ]; then
|
|
|
97
106
|
printf '%s\n' "$dirty" | sed 's/^/ /'
|
|
98
107
|
fi
|
|
99
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
|
+
|
|
100
124
|
if ! git merge-base --is-ancestor "$base" HEAD 2>/dev/null; then
|
|
101
125
|
block "HEAD is not on top of $base - rebase before filing"
|
|
102
126
|
fi
|
|
@@ -136,16 +160,22 @@ while IFS= read -r f; do
|
|
|
136
160
|
# forced. NVIDIA/go-nvml ignores "dl" for a built binary, and that pattern also
|
|
137
161
|
# matches the directory pkg/dl, so forcing the check called two upstream source
|
|
138
162
|
# files build output.
|
|
139
|
-
if !
|
|
163
|
+
if ! in_base "$f"; then
|
|
140
164
|
if git check-ignore -q --no-index -- "$f" 2>/dev/null; then
|
|
141
165
|
block "$f is added by this branch and the repository's .gitignore excludes it"
|
|
142
166
|
fi
|
|
143
167
|
fi
|
|
144
168
|
|
|
145
169
|
case "$f" in
|
|
146
|
-
.jeffy/*|*/.jeffy/*|PLAN.md|*/PLAN.md|JOURNAL.md|*/JOURNAL.md|*.orig|*.rej)
|
|
170
|
+
.jeffy/*|*/.jeffy/*|PLAN.md|*/PLAN.md|JOURNAL.md|*/JOURNAL.md|*.orig|*.rej|*.bak|*.swp|.DS_Store|*/.DS_Store|Thumbs.db|*/Thumbs.db)
|
|
147
171
|
block "$f is a working artifact and has no place in the diff" ;;
|
|
148
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
|
|
149
179
|
done <<EOF
|
|
150
180
|
$files
|
|
151
181
|
EOF
|
|
@@ -187,15 +217,27 @@ printf '%s\n' "$comments" | grep -E '^CHECK: ' | sed 's/^/ /'
|
|
|
187
217
|
# that is wrong in a way the analyzer cannot see.
|
|
188
218
|
added=$(git diff -U0 "$base..HEAD" -- . ':(exclude)*.md' ':(exclude)*.rst' ':(exclude)*.txt' 2>/dev/null \
|
|
189
219
|
| grep '^+' | grep -v '^+++' | sed 's/^+//')
|
|
190
|
-
suppressions=$(printf '%s\n' "$added" | grep -E 'UnconditionalSuppressMessage|SuppressMessage\(|#pragma warning disable
|
|
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')
|
|
191
221
|
if [ -n "$suppressions" ]; then
|
|
192
|
-
|
|
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.
|
|
193
225
|
unshown=""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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"
|
|
199
241
|
if [ -n "$unshown" ]; then
|
|
200
242
|
block "the branch adds a warning suppression, and no build log shows the warning firing ($unshown)"
|
|
201
243
|
printf '%s\n' "$suppressions" | sed 's/^/ /'
|
|
@@ -209,12 +251,67 @@ aotdecl=$(git grep -lE '<(IsAotCompatible|IsTrimmable|PublishAot|EnableTrimAnaly
|
|
|
209
251
|
if [ -n "$aotdecl" ]; then
|
|
210
252
|
reflection=$(git diff -U0 "$base..HEAD" -- '*.cs' 2>/dev/null | grep '^+' | grep -v '^+++' | sed 's/^+//' \
|
|
211
253
|
| grep -E '\.Get(Property|Properties|Method|Methods|Field|Fields|Member|Members|Interfaces)\(|Activator\.CreateInstance|MakeGenericType|MakeGenericMethod|Type\.GetType\(|\.InvokeMember\(')
|
|
212
|
-
|
|
213
|
-
|
|
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"
|
|
214
268
|
printf '%s\n' "$reflection" | sed 's/^/ /'
|
|
215
269
|
fi
|
|
216
270
|
fi
|
|
217
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
|
+
|
|
218
315
|
# Absolutes in added prose are the claims a maintainer can falsify in one grep.
|
|
219
316
|
claims=$(git diff -U0 "$base..HEAD" -- '*.md' '*.mdx' '*.rst' '*.txt' 2>/dev/null \
|
|
220
317
|
| grep '^+' | grep -v '^+++' | sed 's/^+//' \
|
|
@@ -235,9 +332,11 @@ if [ "$blocked" -ne 0 ]; then
|
|
|
235
332
|
echo "branch-check: refused. Fix the causes above; a refusal is a result."
|
|
236
333
|
exit 1
|
|
237
334
|
fi
|
|
238
|
-
# The stamp file-pr.sh reads: this head passed, against this base, at this
|
|
335
|
+
# The stamp file-pr.sh reads: this head passed, against this base, at this
|
|
336
|
+
# time, changing these files.
|
|
239
337
|
head=$(git rev-parse HEAD)
|
|
240
338
|
mkdir -p "$home/branch-checks"
|
|
241
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"
|
|
242
341
|
echo
|
|
243
342
|
echo "branch-check: nothing mechanical blocks this branch."
|
|
@@ -12,21 +12,29 @@
|
|
|
12
12
|
# repository.
|
|
13
13
|
#
|
|
14
14
|
# Usage:
|
|
15
|
-
# bash check-ai-policy.sh [--branch <name>] [--out] owner/repo [owner/repo ...]
|
|
15
|
+
# bash check-ai-policy.sh [--branch <name>] [--out] [--accept-truncated] owner/repo [owner/repo ...]
|
|
16
16
|
# bash check-ai-policy.sh --help
|
|
17
17
|
#
|
|
18
18
|
# --branch <name> read this branch instead of the default; some projects
|
|
19
19
|
# keep the policy on a development branch
|
|
20
20
|
# --out also save the printout to $HOUSEBROKEN_HOME/policy/
|
|
21
|
-
# policy-<owner>-<repo>.md, which file-pr.sh reads
|
|
21
|
+
# policy-<owner>-<repo>.md, which file-pr.sh reads, and
|
|
22
|
+
# the pull request template to template-<owner>-<repo>.md,
|
|
23
|
+
# whose lines file-pr.sh leaves out of the body's word count
|
|
24
|
+
# --accept-truncated read a repository whose tree GitHub truncates (over
|
|
25
|
+
# 100,000 entries) after its policy was read by hand; the
|
|
26
|
+
# printout records that it was
|
|
22
27
|
#
|
|
23
28
|
# Every hit is printed with its file and line so the sentence is read, not a
|
|
24
29
|
# verdict. The RESULT line is the reading: BAN when a sentence forbids or
|
|
25
30
|
# closes AI-written contributions, DISCLOSE when a sentence requires that AI
|
|
26
31
|
# use be disclosed, MENTION when AI is named without either, CLEAN when no
|
|
27
|
-
# policy file names it
|
|
28
|
-
#
|
|
29
|
-
# the
|
|
32
|
+
# policy file names it, UNREAD when GitHub refused, throttled or truncated any
|
|
33
|
+
# listing or file, so a failure is never read as silence. Every policy-named
|
|
34
|
+
# file is read; READMEs are capped at 40 per repository and the cap is printed.
|
|
35
|
+
# Exit 0 for CLEAN or MENTION, 1 for UNREAD or when the repository cannot be
|
|
36
|
+
# read, 3 for DISCLOSE, 4 for BAN. With several repositories the exit is the
|
|
37
|
+
# highest.
|
|
30
38
|
#
|
|
31
39
|
# Environment:
|
|
32
40
|
# HOUSEBROKEN_HOME work directory, default $HOME/.housebroken; --out writes
|
|
@@ -41,7 +49,7 @@ usage() { awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0"; }
|
|
|
41
49
|
# regard to case, so CONTRIBUTING.rst, contributing.md and Contributing.md
|
|
42
50
|
# are all read.
|
|
43
51
|
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
|
-
|
|
52
|
+
MAX_READMES=40
|
|
45
53
|
|
|
46
54
|
# The word itself, plus its usual spellings and the tools by name.
|
|
47
55
|
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'
|
|
@@ -50,6 +58,7 @@ DISCLOSE_RE='disclos|disclaimer|declare|must (state|say|note|indicate|mention)|b
|
|
|
50
58
|
|
|
51
59
|
branch_opt=""
|
|
52
60
|
out=0
|
|
61
|
+
accept_truncated=0
|
|
53
62
|
repos=()
|
|
54
63
|
while [ "$#" -gt 0 ]; do
|
|
55
64
|
case "$1" in
|
|
@@ -58,6 +67,7 @@ while [ "$#" -gt 0 ]; do
|
|
|
58
67
|
branch_opt="$1" ;;
|
|
59
68
|
--branch=*) branch_opt="${1#--branch=}" ;;
|
|
60
69
|
--out) out=1 ;;
|
|
70
|
+
--accept-truncated) accept_truncated=1 ;;
|
|
61
71
|
--*) usage >&2; echo "check-ai-policy.sh: unknown option $1" >&2; exit 1 ;;
|
|
62
72
|
*/*) repos+=("$1") ;;
|
|
63
73
|
*) usage >&2; echo "check-ai-policy.sh: '$1' is not owner/repo" >&2; exit 1 ;;
|
|
@@ -68,20 +78,50 @@ done
|
|
|
68
78
|
|
|
69
79
|
command -v gh >/dev/null 2>&1 || { echo "check-ai-policy.sh: gh is not on PATH" >&2; exit 1; }
|
|
70
80
|
home="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
|
|
81
|
+
errfile=$(mktemp)
|
|
82
|
+
tmpl_repo=$(mktemp)
|
|
83
|
+
tmpl_org=$(mktemp)
|
|
84
|
+
trap 'rm -f "$errfile" "$tmpl_repo" "$tmpl_org"' EXIT
|
|
71
85
|
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
| grep -Ei "$NAME_RE" | head -"$MAX_FILES"
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
# Print every hit in one repository at one ref; sets found, ban, disclose.
|
|
86
|
+
# Print every hit in one repository at one ref; sets found, ban, disclose,
|
|
87
|
+
# and counts what GitHub would not give us in unread and truncated, so a
|
|
88
|
+
# failed or throttled call is never read as a clean file.
|
|
79
89
|
scan() {
|
|
80
|
-
local target="$1" ref="$2" label="$3" f body hits
|
|
90
|
+
local target="$1" ref="$2" label="$3" f body hits tree listed files readmes
|
|
91
|
+
if ! tree=$(gh api "repos/$target/git/trees/$ref?recursive=1" 2>"$errfile"); then
|
|
92
|
+
# An empty repository answers 409: it has no files, so no policy to read.
|
|
93
|
+
if grep -q "409" "$errfile"; then
|
|
94
|
+
echo " (empty) $label$target"
|
|
95
|
+
return
|
|
96
|
+
fi
|
|
97
|
+
echo " UNREAD: the file listing of $target@$ref"
|
|
98
|
+
unread=$((unread + 1))
|
|
99
|
+
return
|
|
100
|
+
fi
|
|
101
|
+
if [ "$(printf '%s' "$tree" | jq -r '.truncated')" = "true" ]; then
|
|
102
|
+
echo " TRUNCATED: GitHub listed only part of the tree of $target@$ref"
|
|
103
|
+
truncated=1
|
|
104
|
+
fi
|
|
105
|
+
listed=$(printf '%s' "$tree" | jq -r '.tree[] | select(.type == "blob") | .path' | grep -Ei "$NAME_RE")
|
|
106
|
+
# Every policy-named file is read; only READMEs, which monorepos carry by the
|
|
107
|
+
# hundred, are capped, and the cap is printed.
|
|
108
|
+
readmes=$(printf '%s\n' "$listed" | grep -iE '(^|/)readme\.[a-z]+$')
|
|
109
|
+
files=$( { printf '%s\n' "$listed" | grep -viE '(^|/)readme\.[a-z]+$'
|
|
110
|
+
printf '%s\n' "$readmes" | head -"$MAX_READMES"; } | grep -v '^$')
|
|
111
|
+
if [ "$(printf '%s\n' "$readmes" | grep -c .)" -gt "$MAX_READMES" ]; then
|
|
112
|
+
echo " (only the first $MAX_READMES of $(printf '%s\n' "$readmes" | grep -c .) READMEs in $label${target} were read)"
|
|
113
|
+
fi
|
|
81
114
|
while IFS= read -r f; do
|
|
82
115
|
[ -n "$f" ] || continue
|
|
83
|
-
body=$(gh api "repos/$target/contents/$f?ref=$ref" --jq '.content' 2>/dev/null
|
|
84
|
-
|
|
116
|
+
if ! body=$(gh api "repos/$target/contents/$f?ref=$ref" --jq '.content' 2>/dev/null); then
|
|
117
|
+
echo " UNREAD: $label$f"
|
|
118
|
+
unread=$((unread + 1))
|
|
119
|
+
continue
|
|
120
|
+
fi
|
|
121
|
+
body=$(printf '%s' "$body" | base64 -d 2>/dev/null)
|
|
122
|
+
if printf '%s' "$f" | grep -qiE '(^|/)pull_request_template'; then
|
|
123
|
+
printf '%s\n' "$body" >> "$tmplout"
|
|
124
|
+
fi
|
|
85
125
|
hits=$(printf '%s' "$body" | grep -nE "$PATTERN" | head -20)
|
|
86
126
|
if [ -n "$hits" ]; then
|
|
87
127
|
found=1
|
|
@@ -92,12 +132,13 @@ scan() {
|
|
|
92
132
|
else
|
|
93
133
|
echo " (clean) $label$f"
|
|
94
134
|
fi
|
|
95
|
-
done
|
|
135
|
+
done <<< "$files"
|
|
96
136
|
}
|
|
97
137
|
|
|
98
138
|
status=0
|
|
99
139
|
for repo in "${repos[@]}"; do
|
|
100
|
-
found=0; ban=0; disclose=0
|
|
140
|
+
found=0; ban=0; disclose=0; unread=0; truncated=0
|
|
141
|
+
: > "$tmpl_repo"; : > "$tmpl_org"
|
|
101
142
|
report=$(
|
|
102
143
|
echo "## AI policy: $repo"
|
|
103
144
|
echo "read: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
@@ -110,14 +151,26 @@ for repo in "${repos[@]}"; do
|
|
|
110
151
|
exit 1
|
|
111
152
|
fi
|
|
112
153
|
echo "branch: $branch"
|
|
154
|
+
tmplout="$tmpl_repo"
|
|
113
155
|
scan "$repo" "$branch" ""
|
|
114
156
|
org="${repo%%/*}"
|
|
115
|
-
|
|
157
|
+
# A missing .github repository is a 404; any other failure is a policy we did not read.
|
|
158
|
+
if ! org_branch=$(gh api "repos/$org/.github" --jq .default_branch 2>"$errfile"); then
|
|
159
|
+
org_branch=""
|
|
160
|
+
grep -q "404" "$errfile" || { echo " UNREAD: $org/.github"; unread=$((unread + 1)); }
|
|
161
|
+
fi
|
|
116
162
|
if [ -n "$org_branch" ]; then
|
|
117
163
|
echo "org repo: $org/.github ($org_branch)"
|
|
164
|
+
tmplout="$tmpl_org"
|
|
118
165
|
scan "$org/.github" "$org_branch" "$org/.github: "
|
|
119
166
|
fi
|
|
167
|
+
if [ "$truncated" -eq 1 ] && [ "$accept_truncated" -eq 1 ]; then
|
|
168
|
+
echo "truncated tree accepted by --accept-truncated: files GitHub did not list were not read"
|
|
169
|
+
truncated=0
|
|
170
|
+
fi
|
|
120
171
|
if [ "$ban" -eq 1 ]; then echo "RESULT: BAN (a policy file forbids or closes AI-written contributions; never file here)"
|
|
172
|
+
elif [ "$unread" -gt 0 ]; then echo "RESULT: UNREAD ($unread policy file(s) or listing(s) could not be fetched; rerun when GitHub answers)"
|
|
173
|
+
elif [ "$truncated" -eq 1 ]; then echo "RESULT: UNREAD (GitHub truncated the file listing; read the policy by hand, then rerun with --accept-truncated)"
|
|
121
174
|
elif [ "$disclose" -eq 1 ]; then echo "RESULT: DISCLOSE (a policy file requires AI use to be disclosed; say so in the body)"
|
|
122
175
|
elif [ "$found" -eq 1 ]; then echo "RESULT: MENTION (a policy file names AI; read the lines above and decide)"
|
|
123
176
|
else echo "RESULT: CLEAN (no policy file names AI)"
|
|
@@ -129,11 +182,19 @@ for repo in "${repos[@]}"; do
|
|
|
129
182
|
case "$report" in
|
|
130
183
|
*"RESULT: BAN"*) [ "$status" -lt 4 ] && status=4 ;;
|
|
131
184
|
*"RESULT: DISCLOSE"*) [ "$status" -lt 3 ] && status=3 ;;
|
|
185
|
+
*"RESULT: UNREAD"*) [ "$status" -lt 1 ] && status=1 ;;
|
|
132
186
|
esac
|
|
133
187
|
if [ "$out" -eq 1 ]; then
|
|
134
188
|
mkdir -p "$home/policy"
|
|
135
189
|
printf '%s\n' "$report" > "$home/policy/policy-${repo%%/*}-${repo##*/}.md"
|
|
136
190
|
echo "saved: $home/policy/policy-${repo%%/*}-${repo##*/}.md"
|
|
191
|
+
# The pull request template, the repository's own or else its
|
|
192
|
+
# organization's, is what file-pr.sh leaves out of the body's word count.
|
|
193
|
+
template="$home/policy/template-${repo%%/*}-${repo##*/}.md"
|
|
194
|
+
if [ -s "$tmpl_repo" ]; then cp "$tmpl_repo" "$template"
|
|
195
|
+
elif [ -s "$tmpl_org" ]; then cp "$tmpl_org" "$template"
|
|
196
|
+
else rm -f "$template"
|
|
197
|
+
fi
|
|
137
198
|
fi
|
|
138
199
|
done
|
|
139
200
|
exit "$status"
|
|
@@ -101,6 +101,7 @@ policy_result=$(grep -m1 '^RESULT: ' "$policy" | awk '{print $2}')
|
|
|
101
101
|
case "$policy_result" in
|
|
102
102
|
BAN) echo "REFUSED: $repo forbids AI-written contributions ($policy); never file here" >&2; exit 2 ;;
|
|
103
103
|
DISCLOSE|MENTION|CLEAN) ;;
|
|
104
|
+
UNREAD) echo "REFUSED: $policy reads UNREAD - GitHub did not give every policy file; rerun check-ai-policy.sh --out" >&2; exit 2 ;;
|
|
104
105
|
*) echo "REFUSED: $policy has no RESULT line - rerun check-ai-policy.sh --out" >&2; exit 2 ;;
|
|
105
106
|
esac
|
|
106
107
|
|
|
@@ -150,9 +151,16 @@ if [ "$policy_result" = "DISCLOSE" ] && ! grep -qiE '\bAI\b|LLM|Claude|Copilot|C
|
|
|
150
151
|
echo "REFUSED: $repo requires AI use to be disclosed ($policy) and the body does not name it" >&2
|
|
151
152
|
exit 2
|
|
152
153
|
fi
|
|
154
|
+
if ! refs=$(bash "$(dirname "$0")/ref-check.sh" "$repo" "$bodyfile" 2>&1); then
|
|
155
|
+
echo "REFUSED: the body names something that does not exist:" >&2
|
|
156
|
+
printf '%s\n' "$refs" | grep -E 'NOT FOUND|REFUSED' >&2
|
|
157
|
+
exit 2
|
|
158
|
+
fi
|
|
153
159
|
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
# An offer is a first-person promise of work, whatever noun it uses; "patch
|
|
161
|
+
# release" in a bug report is not one. The link may be the filed pull request.
|
|
162
|
+
offer_re='\b(I|we) (have|had|can|could|will|would|am|are)( be happy to| be glad to)? (send|open|submit|provide|offer|push|put up|prepare|write)\b|\b(I|we) (have|wrote|made) (a|the) (fix|patch|change)\b|\bsay the word\b|\bif you (want|would like|are interested)\b|\bhappy to (send|open|submit|provide)\b|\b(fix|fixes|patch|pull request|PR|change) (is |are )?(ready|available|done)\b'
|
|
163
|
+
link_re='https://github\.com/[^/[:space:]]+/[^/[:space:]]+/(tree|commit|compare|blob|pull)/[^[:space:])]+'
|
|
156
164
|
offer=$(printf '%s' "$body" | grep -ioE "$offer_re" | head -1) || true
|
|
157
165
|
if [ -n "$offer" ] && ! printf '%s' "$body" | grep -qE "$link_re"; then
|
|
158
166
|
echo "REFUSED: the body says '$offer' and links no branch or commit - push the work to your fork and link it, or take it out of the body" >&2
|
|
@@ -106,6 +106,7 @@ policy_result=$(grep -m1 '^RESULT: ' "$policy" | awk '{print $2}')
|
|
|
106
106
|
case "$policy_result" in
|
|
107
107
|
BAN) echo "REFUSED: $repo forbids AI-written contributions ($policy); never file here" >&2; exit 2 ;;
|
|
108
108
|
DISCLOSE|MENTION|CLEAN) ;;
|
|
109
|
+
UNREAD) echo "REFUSED: $policy reads UNREAD - GitHub did not give every policy file; rerun check-ai-policy.sh --out" >&2; exit 2 ;;
|
|
109
110
|
*) echo "REFUSED: $policy has no RESULT line - rerun check-ai-policy.sh --out" >&2; exit 2 ;;
|
|
110
111
|
esac
|
|
111
112
|
|
|
@@ -157,21 +158,53 @@ if [ "$bodyfile" = "-" ] || [ ! -f "$bodyfile" ]; then
|
|
|
157
158
|
fi
|
|
158
159
|
check_text "body file $bodyfile" "$(cat "$bodyfile")"
|
|
159
160
|
check_text "--title" "$title"
|
|
160
|
-
|
|
161
|
+
# The project's own template text is kept verbatim and is not ours to count:
|
|
162
|
+
# HTML comments and every line the saved template carries, a ticked box read
|
|
163
|
+
# as its unticked line, are left out before the words are counted.
|
|
164
|
+
template="$home/policy/template-${repo%%/*}-${repo##*/}.md"
|
|
165
|
+
[ -f "$template" ] || template=/dev/null
|
|
166
|
+
words=$(awk -v T="$template" '
|
|
167
|
+
function norm(s) { gsub(/\[[xX]\]/, "[ ]", s); gsub(/^[[:space:]]+|[[:space:]]+$/, "", s); return s }
|
|
168
|
+
BEGIN { while ((getline l < T) > 0) { l = norm(l); if (l != "") t[l] = 1 } }
|
|
169
|
+
{ l = norm($0); if (l in t) next
|
|
170
|
+
for (k in t) if (index(l, k) == 1) { l = substr(l, length(k) + 1); break }
|
|
171
|
+
print l }' "$bodyfile" | wc -w | tr -d ' ')
|
|
161
172
|
if [ "$words" -gt 120 ]; then
|
|
162
|
-
echo "REFUSED: the body is $words words - say what was wrong, what the change does and how it was verified in 120 or fewer" >&2
|
|
173
|
+
echo "REFUSED: the body is $words words outside the project's template - say what was wrong, what the change does and how it was verified in 120 or fewer" >&2
|
|
163
174
|
exit 2
|
|
164
175
|
fi
|
|
165
176
|
if [ "$policy_result" = "DISCLOSE" ] && ! grep -qiE '\bAI\b|LLM|Claude|Copilot|ChatGPT|language model|assist' "$bodyfile"; then
|
|
166
177
|
echo "REFUSED: $repo requires AI use to be disclosed ($policy) and the body does not name it" >&2
|
|
167
178
|
exit 2
|
|
168
179
|
fi
|
|
180
|
+
if ! refs=$(bash "$(dirname "$0")/ref-check.sh" "$repo" "$bodyfile" --clone . 2>&1); then
|
|
181
|
+
echo "REFUSED: the body names something that does not exist:" >&2
|
|
182
|
+
printf '%s\n' "$refs" | grep -E 'NOT FOUND|REFUSED' >&2
|
|
183
|
+
exit 2
|
|
184
|
+
fi
|
|
169
185
|
|
|
170
186
|
head=$(git rev-parse HEAD 2>/dev/null) || { echo "REFUSED: run file-pr.sh from inside the clone being filed" >&2; exit 2; }
|
|
171
187
|
if [ ! -f "$home/branch-checks/$head" ]; then
|
|
172
188
|
echo "REFUSED: branch-check.sh has not passed head ${head:0:12} - run it from this clone first" >&2
|
|
173
189
|
exit 2
|
|
174
190
|
fi
|
|
191
|
+
# The prior art has to be about this change: every file the branch changes,
|
|
192
|
+
# tests aside, is a term in the printout, by path or by name. One printout per
|
|
193
|
+
# repository means another branch's run replaces it.
|
|
194
|
+
searched=$(sed -n 's/^### //p' "$art" | tr -d '\r')
|
|
195
|
+
ours=$(sed -n 's/^file: //p' "$home/branch-checks/$head" | tr -d '\r')
|
|
196
|
+
[ -n "$ours" ] || { echo "REFUSED: the branch-check stamp for head ${head:0:12} lists no files - rerun branch-check.sh" >&2; exit 2; }
|
|
197
|
+
unsearched=""
|
|
198
|
+
while IFS= read -r f; do
|
|
199
|
+
case "$f" in ""|*test*|*Test*|*spec*) continue ;; esac
|
|
200
|
+
printf '%s\n' "$searched" | grep -qxF -e "$f" -e "$(basename "$f")" || unsearched="$unsearched $f"
|
|
201
|
+
done <<EOF
|
|
202
|
+
$ours
|
|
203
|
+
EOF
|
|
204
|
+
if [ -n "$unsearched" ]; then
|
|
205
|
+
echo "REFUSED: the prior art at $art never searched files this branch changes:$unsearched - rerun prior-art.sh $repo with them --out" >&2
|
|
206
|
+
exit 2
|
|
207
|
+
fi
|
|
175
208
|
# Every diff gate read the diff against the stamp's base; it must be the branch this files into.
|
|
176
209
|
checked=$(sed -n 's/^base-ref: //p' "$home/branch-checks/$head" | tr -d '\r')
|
|
177
210
|
if [ -z "$checked" ]; then
|
|
@@ -188,6 +221,48 @@ case "$checked" in
|
|
|
188
221
|
*) echo "REFUSED: branch-check.sh measured this head against $checked, but it is filed into $target - rerun branch-check.sh --base <remote>/$target" >&2
|
|
189
222
|
exit 2 ;;
|
|
190
223
|
esac
|
|
224
|
+
# The upstream may have moved since the check. A move that touches none of the
|
|
225
|
+
# branch's files is reported; one that touches them means the branch and its
|
|
226
|
+
# review were measured against code that is no longer there.
|
|
227
|
+
stamped=$(sed -n 's/^base: //p' "$home/branch-checks/$head" | tr -d '\r')
|
|
228
|
+
upstream=$(gh api "repos/$repo/branches/$target" --jq .commit.sha 2>/dev/null | tr -d '\r')
|
|
229
|
+
[ -n "$upstream" ] || { echo "REFUSED: cannot read $target of $repo to confirm the base is current" >&2; exit 2; }
|
|
230
|
+
if [ "$stamped" != "$upstream" ]; then
|
|
231
|
+
if ! cmp=$(gh api "repos/$repo/compare/$stamped...$upstream" 2>/dev/null); then
|
|
232
|
+
echo "REFUSED: the checked base ${stamped:0:12} is not on $repo $target - fetch, rebase and rerun branch-check.sh" >&2
|
|
233
|
+
exit 2
|
|
234
|
+
fi
|
|
235
|
+
# GitHub lists at most 300 files here; past that the overlap cannot be known.
|
|
236
|
+
if [ "$(printf '%s' "$cmp" | jq '.files | length')" -ge 300 ]; then
|
|
237
|
+
echo "REFUSED: $target moved by 300 files or more since branch-check (${stamped:0:12} to ${upstream:0:12}) - fetch, rebase, rerun branch-check.sh and the review" >&2
|
|
238
|
+
exit 2
|
|
239
|
+
fi
|
|
240
|
+
moved=$(printf '%s' "$cmp" | jq -r '.files[].filename')
|
|
241
|
+
overlap=$(printf '%s\n' "$moved" | grep -Fx -f <(printf '%s\n' "$ours"))
|
|
242
|
+
if [ -n "$overlap" ]; then
|
|
243
|
+
echo "REFUSED: $target moved since branch-check (${stamped:0:12} to ${upstream:0:12}) and changed files this branch changes - fetch, rebase, rerun branch-check.sh and the review:" >&2
|
|
244
|
+
printf '%s\n' "$overlap" | sed 's/^/ /' >&2
|
|
245
|
+
exit 2
|
|
246
|
+
fi
|
|
247
|
+
echo "note: $target moved since branch-check (${stamped:0:12} to ${upstream:0:12}); none of its new commits touch this branch's files" >&2
|
|
248
|
+
fi
|
|
249
|
+
# Three open pull requests per repository, and ten minutes between filings:
|
|
250
|
+
# GitHub lets maintainers cap a non-collaborator's open pull requests, and two
|
|
251
|
+
# stamped the same minute read as a bot.
|
|
252
|
+
if ! mine=$(gh pr list -R "$repo" --author @me --state open --json createdAt --jq '[.[].createdAt] | sort' 2>/dev/null); then
|
|
253
|
+
echo "REFUSED: cannot read your open pull requests on $repo" >&2
|
|
254
|
+
exit 2
|
|
255
|
+
fi
|
|
256
|
+
open_count=$(printf '%s' "$mine" | jq 'length')
|
|
257
|
+
if [ "$open_count" -ge 3 ]; then
|
|
258
|
+
echo "REFUSED: you have $open_count open pull requests on $repo - three is the most a repository gets" >&2
|
|
259
|
+
exit 2
|
|
260
|
+
fi
|
|
261
|
+
since=$(printf '%s' "$mine" | jq -r --argjson now "$(date -u +%s)" 'if length > 0 then $now - (last | fromdateiso8601) else 99999 end')
|
|
262
|
+
if [ "$since" -lt 600 ]; then
|
|
263
|
+
echo "REFUSED: your last pull request on $repo was filed ${since}s ago - wait until ten minutes have passed" >&2
|
|
264
|
+
exit 2
|
|
265
|
+
fi
|
|
191
266
|
review="$home/reviews/review-${repo%%/*}-${repo##*/}-${head:0:12}.md"
|
|
192
267
|
if [ ! -f "$review" ]; then
|
|
193
268
|
echo "REFUSED: no adversarial review at $review - run review.sh brief and have a different model attack this head" >&2
|
|
@@ -201,4 +276,7 @@ if [ "${FILE_PR_DRY:-0}" = "1" ]; then
|
|
|
201
276
|
printf '\n'
|
|
202
277
|
exit 0
|
|
203
278
|
fi
|
|
279
|
+
# What was filed, kept so verify-filed-pr.sh can compare GitHub's copy to it.
|
|
280
|
+
mkdir -p "$home/filed"
|
|
281
|
+
cp "$bodyfile" "$home/filed/${repo%%/*}-${repo##*/}-$head.md"
|
|
204
282
|
gh pr create -R "$repo" "${pass[@]}"
|
|
@@ -53,6 +53,17 @@ while read -r repo n; do
|
|
|
53
53
|
IFS='|' read -r owner fork branch state <<< "$info"
|
|
54
54
|
if [ "$owner" != "$ME" ] || [ -z "$branch" ]; then continue; fi
|
|
55
55
|
gh api "repos/$ME/$fork/git/refs/heads/$branch" >/dev/null 2>&1 || continue
|
|
56
|
+
# A closed pull request refiled from the same branch still needs it, and
|
|
57
|
+
# deleting the branch would close the open one.
|
|
58
|
+
if ! users=$(gh pr list --repo "$repo" --head "$branch" --state open --json number,headRepositoryOwner \
|
|
59
|
+
--jq "[.[] | select(.headRepositoryOwner.login == \"$ME\") | .number] | join(\" \")" 2>/dev/null); then
|
|
60
|
+
echo "fork-hygiene.sh: cannot list open pull requests on $repo; $branch kept" >&2
|
|
61
|
+
continue
|
|
62
|
+
fi
|
|
63
|
+
if [ -n "$users" ]; then
|
|
64
|
+
echo "kept $ME/$fork $branch: open pull request $repo#$users still uses it"
|
|
65
|
+
continue
|
|
66
|
+
fi
|
|
56
67
|
if [ "$DRY" = 1 ]; then
|
|
57
68
|
echo "would delete $ME/$fork $branch ($state $repo#$n)"
|
|
58
69
|
continue
|
|
@@ -31,14 +31,16 @@ set -u
|
|
|
31
31
|
|
|
32
32
|
usage() {
|
|
33
33
|
cat <<'EOF'
|
|
34
|
-
usage: prior-art.sh owner/repo <term>... [--out]
|
|
34
|
+
usage: prior-art.sh owner/repo <term>... [--out [--partial]]
|
|
35
35
|
prior-art.sh --help
|
|
36
36
|
|
|
37
37
|
owner/repo the upstream repository to search
|
|
38
38
|
<term> a symbol, phrase or file path; a term with a slash or a dot is
|
|
39
39
|
also treated as a path: its commit history is scanned and
|
|
40
40
|
every open PR that changes it is listed
|
|
41
|
-
--out save the printout under $HOUSEBROKEN_HOME/prior-art
|
|
41
|
+
--out save the printout under $HOUSEBROKEN_HOME/prior-art/; a printout
|
|
42
|
+
whose searches matched more than they returned is not saved
|
|
43
|
+
--partial save a partial printout anyway, marked as such
|
|
42
44
|
|
|
43
45
|
environment:
|
|
44
46
|
HOUSEBROKEN_HOME work directory, default $HOME/.housebroken
|
|
@@ -54,10 +56,12 @@ command -v jq >/dev/null 2>&1 || { echo "prior-art.sh: jq is not on PATH" >&2; e
|
|
|
54
56
|
|
|
55
57
|
repo=""
|
|
56
58
|
out=0
|
|
59
|
+
accept_partial=0
|
|
57
60
|
terms=()
|
|
58
61
|
for arg in "$@"; do
|
|
59
62
|
case "$arg" in
|
|
60
63
|
--out) out=1 ;;
|
|
64
|
+
--partial) accept_partial=1 ;;
|
|
61
65
|
--*) usage >&2; echo "prior-art.sh: unknown option $arg" >&2; exit 1 ;;
|
|
62
66
|
*/*) if [ -z "$repo" ]; then repo="$arg"; else terms+=("$arg"); fi ;;
|
|
63
67
|
*) if [ -z "$repo" ]; then usage >&2; exit 1; fi
|
|
@@ -70,6 +74,8 @@ fi
|
|
|
70
74
|
|
|
71
75
|
home="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
|
|
72
76
|
calls=0
|
|
77
|
+
partial=0
|
|
78
|
+
notes=""
|
|
73
79
|
items=0
|
|
74
80
|
tmp=$(mktemp) || { echo "prior-art.sh: cannot create temp file" >&2; exit 1; }
|
|
75
81
|
row=$(mktemp) || { echo "prior-art.sh: cannot create temp file" >&2; exit 1; }
|
|
@@ -84,10 +90,20 @@ last_word() {
|
|
|
84
90
|
printf '%s' "$body" | tr '\n\r\t|' ' ' | tr -s ' ' | cut -c1-160
|
|
85
91
|
}
|
|
86
92
|
|
|
87
|
-
emit() {
|
|
88
|
-
local kind num state date author title word=""
|
|
93
|
+
emit() { # label for the counts: issues or pull requests
|
|
94
|
+
local label="$1" kind num state date author title word=""
|
|
89
95
|
while IFS=$'\t' read -r kind num state date author title; do
|
|
90
96
|
[ -n "$num" ] || continue
|
|
97
|
+
# The search's own count against the rows it returned: a term matching more
|
|
98
|
+
# than one page is a printout that has not read everything.
|
|
99
|
+
if [ "$kind" = "TOTAL" ]; then
|
|
100
|
+
if [ "$num" -gt "$state" ]; then
|
|
101
|
+
notes="${notes}showing $state of $num $label for '$term'; narrow the term
|
|
102
|
+
"
|
|
103
|
+
partial=1
|
|
104
|
+
fi
|
|
105
|
+
continue
|
|
106
|
+
fi
|
|
91
107
|
word=""
|
|
92
108
|
if [ "$state" = "closed" ]; then word=$(last_word "$num" "$author"); calls=$((calls+1)); fi
|
|
93
109
|
printf '| %s | #%s | %s | %s | %s | %s | %s |\n' \
|
|
@@ -106,20 +122,22 @@ for term in "${terms[@]}"; do
|
|
|
106
122
|
} >>"$tmp"
|
|
107
123
|
|
|
108
124
|
gh api -X GET search/issues -f q="repo:$repo type:issue \"$term\"" -F per_page=10 \
|
|
109
|
-
--jq '
|
|
110
|
-
|
|
125
|
+
--jq '(["TOTAL", (.total_count|tostring), (if .incomplete_results then "0" else (.items|length|tostring) end)] | @tsv),
|
|
126
|
+
(.items[] | ["issue", (.number|tostring), .state, ((.closed_at // .created_at)[0:10]),
|
|
127
|
+
.user.login, (.title|gsub("[|]";"/")|gsub("[\n\r\t]";" "))] | @tsv)' >"$row" ||
|
|
111
128
|
{ echo "prior-art.sh: issue search failed for $repo term '$term'" >&2; exit 1; }
|
|
112
129
|
calls=$((calls+1))
|
|
113
|
-
emit
|
|
130
|
+
emit issues
|
|
114
131
|
|
|
115
132
|
gh api -X GET search/issues -f q="repo:$repo type:pr \"$term\"" -F per_page=10 \
|
|
116
|
-
--jq '
|
|
133
|
+
--jq '(["TOTAL", (.total_count|tostring), (if .incomplete_results then "0" else (.items|length|tostring) end)] | @tsv),
|
|
134
|
+
(.items[] | ["PR", (.number|tostring),
|
|
117
135
|
(if .pull_request.merged_at then "merged" else .state end),
|
|
118
136
|
((.pull_request.merged_at // .closed_at // .created_at)[0:10]),
|
|
119
|
-
.user.login, (.title|gsub("[|]";"/")|gsub("[\n\r\t]";" "))] | @tsv' >"$row" ||
|
|
137
|
+
.user.login, (.title|gsub("[|]";"/")|gsub("[\n\r\t]";" "))] | @tsv)' >"$row" ||
|
|
120
138
|
{ echo "prior-art.sh: PR search failed for $repo term '$term'" >&2; exit 1; }
|
|
121
139
|
calls=$((calls+1))
|
|
122
|
-
emit
|
|
140
|
+
emit "pull requests"
|
|
123
141
|
|
|
124
142
|
# a path term also gets the PRs whose merge commits touched that path
|
|
125
143
|
case "$term" in
|
|
@@ -146,6 +164,13 @@ for term in "${terms[@]}"; do
|
|
|
146
164
|
($p.title|gsub("[|]";"/")|gsub("[\n\r\t]";" ")), .] | @tsv' >"$openfiles" ||
|
|
147
165
|
{ echo "prior-art.sh: open PR listing failed for $repo" >&2; exit 1; }
|
|
148
166
|
calls=$((calls+1))
|
|
167
|
+
open_total=$(gh api -X GET search/issues -f q="repo:$repo is:pr is:open" --jq .total_count 2>/dev/null) || open_total=""
|
|
168
|
+
calls=$((calls+1))
|
|
169
|
+
if [ -z "$open_total" ] || [ "$open_total" -gt 200 ]; then
|
|
170
|
+
notes="${notes}OPEN PR LIST INCOMPLETE: 200 of ${open_total:-an unknown number of} open pull requests read; one changing these paths may be missing
|
|
171
|
+
"
|
|
172
|
+
partial=1
|
|
173
|
+
fi
|
|
149
174
|
# GitHub returns at most 100 files per PR here; a larger PR is read in full
|
|
150
175
|
awk -F'\t' '$5 == "MORE"' "$openfiles" | while IFS=$'\t' read -r num date author title _; do
|
|
151
176
|
gh api --paginate "repos/$repo/pulls/$num/files?per_page=100" --jq '.[].filename' |
|
|
@@ -167,8 +192,16 @@ done
|
|
|
167
192
|
printf '\n%d items across %d terms. Read every closed item before filing; this script lists, it does not judge.\n' \
|
|
168
193
|
"$items" "${#terms[@]}" >>"$tmp"
|
|
169
194
|
printf 'gh calls: %d\n' "$calls" >>"$tmp"
|
|
195
|
+
if [ "$partial" -eq 1 ]; then
|
|
196
|
+
printf '\nPARTIAL: these searches returned less than they matched:\n%s' "$notes" >>"$tmp"
|
|
197
|
+
[ "$accept_partial" -eq 1 ] && printf 'saved with --partial: the rest was not read\n' >>"$tmp"
|
|
198
|
+
fi
|
|
170
199
|
|
|
171
200
|
cat "$tmp"
|
|
201
|
+
if [ "$out" -eq 1 ] && [ "$partial" -eq 1 ] && [ "$accept_partial" -eq 0 ]; then
|
|
202
|
+
echo "prior-art.sh: not saved - the printout is partial; narrow the terms, or pass --partial to save it as read" >&2
|
|
203
|
+
exit 1
|
|
204
|
+
fi
|
|
172
205
|
if [ "$out" -eq 1 ]; then
|
|
173
206
|
dir="$home/prior-art"
|
|
174
207
|
mkdir -p "$dir" || { echo "prior-art.sh: cannot create $dir" >&2; exit 1; }
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ref-check.sh - every reference in outbound text points at something that
|
|
3
|
+
# exists.
|
|
4
|
+
#
|
|
5
|
+
# A model writes "Fixes #412", a link, a test name or a commit hash with the
|
|
6
|
+
# same fluency whether the thing exists or not, and a maintainer who follows
|
|
7
|
+
# one to nothing stops reading. Each reference is looked up: an issue or pull
|
|
8
|
+
# request number on GitHub (its type and state printed, so "Fixes" naming a
|
|
9
|
+
# merged pull request is visible), a github.com link, and, with --clone, a
|
|
10
|
+
# file path, a test name or a commit hash named in backticks. Fenced code and
|
|
11
|
+
# quoted lines are evidence, not references, and are skipped.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# bash ref-check.sh owner/repo FILE [--clone DIR]
|
|
15
|
+
# bash ref-check.sh --help
|
|
16
|
+
#
|
|
17
|
+
# Exits 0 when every reference was found, 1 when any was not, 2 on a usage
|
|
18
|
+
# error. A lookup GitHub refuses reads NOT FOUND: unconfirmed is not found.
|
|
19
|
+
#
|
|
20
|
+
# Assumes gh is installed and authenticated. Read-only: GET calls only.
|
|
21
|
+
set -u
|
|
22
|
+
|
|
23
|
+
usage() { awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0"; }
|
|
24
|
+
|
|
25
|
+
case "${1:-}" in
|
|
26
|
+
--help|-h) usage; exit 0 ;;
|
|
27
|
+
esac
|
|
28
|
+
repo="${1:-}"; file="${2:-}"
|
|
29
|
+
case "$repo" in */*) ;; *) echo "REFUSED: name the repository as owner/repo" >&2; exit 2 ;; esac
|
|
30
|
+
[ -f "$file" ] || { echo "REFUSED: $file does not exist" >&2; exit 2; }
|
|
31
|
+
shift 2
|
|
32
|
+
clone=""
|
|
33
|
+
while [ "$#" -gt 0 ]; do
|
|
34
|
+
case "$1" in
|
|
35
|
+
--clone) clone="${2:-}"; [ -d "$clone" ] || { echo "REFUSED: --clone needs a directory" >&2; exit 2; }; shift ;;
|
|
36
|
+
*) echo "REFUSED: unknown argument $1" >&2; exit 2 ;;
|
|
37
|
+
esac
|
|
38
|
+
shift
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
text=$(awk '/^(```|~~~)/ {fence = !fence; next} !fence && !/^ *>/ && !/^ / {print}' "$file")
|
|
42
|
+
found() { echo " found $1"; }
|
|
43
|
+
lost() { echo " NOT FOUND $1"; }
|
|
44
|
+
|
|
45
|
+
# owner/repo#N and bare #N, the bare ones in this repository
|
|
46
|
+
issue() { # owner/repo number label
|
|
47
|
+
local got
|
|
48
|
+
if got=$(gh api "repos/$1/issues/$2" --jq '(if .pull_request then "pull request" else "issue" end) + " " + .state + ": " + .title' 2>/dev/null); then
|
|
49
|
+
found "$3 ($got)"
|
|
50
|
+
else
|
|
51
|
+
lost "$3"
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
out=$(
|
|
55
|
+
printf '%s\n' "$text" | grep -oE '\b[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[0-9]+\b' | sort -u | while IFS= read -r ref; do
|
|
56
|
+
issue "${ref%#*}" "${ref##*#}" "$ref"
|
|
57
|
+
done
|
|
58
|
+
printf '%s\n' "$text" | sed -E 's/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+#[0-9]+//g' | grep -oE '(^|[^A-Za-z0-9&/])#[0-9]+\b' \
|
|
59
|
+
| grep -oE '[0-9]+' | sort -un | while IFS= read -r n; do
|
|
60
|
+
issue "$repo" "$n" "#$n"
|
|
61
|
+
done
|
|
62
|
+
printf '%s\n' "$text" | grep -oE 'https://github\.com/[^ )>"]+' | sed 's/[.,;:]$//' | sort -u | while IFS= read -r url; do
|
|
63
|
+
path="${url#https://github.com/}"
|
|
64
|
+
case "$path" in
|
|
65
|
+
*/*/pull/[0-9]*|*/*/issues/[0-9]*)
|
|
66
|
+
r=$(printf '%s' "$path" | cut -d/ -f1-2); n=$(printf '%s' "$path" | cut -d/ -f4 | grep -oE '^[0-9]+')
|
|
67
|
+
issue "$r" "$n" "$url" ;;
|
|
68
|
+
*/*/commit/*)
|
|
69
|
+
r=$(printf '%s' "$path" | cut -d/ -f1-2); c=$(printf '%s' "$path" | cut -d/ -f4)
|
|
70
|
+
if gh api "repos/$r/commits/$c" --jq .sha >/dev/null 2>&1; then found "$url"; else lost "$url"; fi ;;
|
|
71
|
+
*/*/blob/*|*/*/tree/*)
|
|
72
|
+
r=$(printf '%s' "$path" | cut -d/ -f1-2); ref=$(printf '%s' "$path" | cut -d/ -f4); p=$(printf '%s' "$path" | cut -d/ -f5- | sed 's/#.*//')
|
|
73
|
+
if gh api "repos/$r/contents/$p?ref=$ref" --jq 'if type == "array" then "dir" else .path end' >/dev/null 2>&1; then found "$url"; else lost "$url"; fi ;;
|
|
74
|
+
esac
|
|
75
|
+
done
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Names in backticks, checked against the clone: a path, a test, a commit.
|
|
79
|
+
if [ -n "$clone" ]; then
|
|
80
|
+
# shellcheck disable=SC2016 # the backticks are the markdown being matched
|
|
81
|
+
names=$(printf '%s\n' "$text" | grep -oE '`[^`]+`' | tr -d '`' | sort -u)
|
|
82
|
+
local_out=$(printf '%s\n' "$names" | while IFS= read -r n; do
|
|
83
|
+
[ -n "$n" ] || continue
|
|
84
|
+
if printf '%s' "$n" | grep -qE '^[0-9a-f]{7,40}$'; then
|
|
85
|
+
if git -C "$clone" cat-file -e "$n^{commit}" 2>/dev/null; then found "commit $n"; else lost "commit $n"; fi
|
|
86
|
+
elif printf '%s' "$n" | grep -qE '^[A-Za-z0-9_./-]+\.[A-Za-z0-9]{1,6}$'; then
|
|
87
|
+
# A dotted name is a file when the clone has it; without a slash it may
|
|
88
|
+
# be an identifier (os.path, t.Run, v1.2.3), so only a path is NOT FOUND.
|
|
89
|
+
if git -C "$clone" ls-files --error-unmatch -- "$n" >/dev/null 2>&1 \
|
|
90
|
+
|| git -C "$clone" ls-files | grep -qE "(^|/)$(printf '%s' "$n" | sed 's/[.[\*^$]/\\&/g')$"; then
|
|
91
|
+
found "file $n"
|
|
92
|
+
elif printf '%s' "$n" | grep -q /; then
|
|
93
|
+
lost "file $n"
|
|
94
|
+
fi
|
|
95
|
+
elif printf '%s' "$n" | grep -qE '^(Test[A-Z][A-Za-z0-9_]*|test_[A-Za-z0-9_]+|[A-Za-z0-9_]+_test|[A-Z][A-Za-z0-9_]*Tests?)$'; then
|
|
96
|
+
if git -C "$clone" grep -qw -- "$n" 2>/dev/null; then found "test $n"; else lost "test $n"; fi
|
|
97
|
+
fi
|
|
98
|
+
done)
|
|
99
|
+
out=$(printf '%s\n%s\n' "$out" "$local_out")
|
|
100
|
+
fi
|
|
101
|
+
out=$(printf '%s\n' "$out" | sed '/^$/d')
|
|
102
|
+
|
|
103
|
+
if [ -z "$out" ]; then
|
|
104
|
+
echo "ref-check: no references in $file"
|
|
105
|
+
exit 0
|
|
106
|
+
fi
|
|
107
|
+
echo "ref-check: every reference in $file, looked up:"
|
|
108
|
+
printf '%s\n' "$out"
|
|
109
|
+
if printf '%s\n' "$out" | grep -q 'NOT FOUND'; then
|
|
110
|
+
echo
|
|
111
|
+
echo "ref-check: a reference points at nothing; fix it or drop it."
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
exit 0
|
|
@@ -216,9 +216,10 @@ EOF
|
|
|
216
216
|
done
|
|
217
217
|
printf 'author-model: %s\nreviewer-model: %s\n' "$author" "$reviewer"
|
|
218
218
|
[ -n "$head" ] && printf 'blind-fix: %s\n' "$blind"
|
|
219
|
+
printf 'skipped: none | <the item numbers you did not attack, each with why>\n'
|
|
219
220
|
printf 'verdict: POST AS IS | POST WITH CHANGES | DO NOT POST\n\n'
|
|
220
221
|
cat <<'EOF'
|
|
221
|
-
POST AS IS means nothing BLOCKING or SHOULD FIX remains. POST WITH CHANGES means the author fixes what you found and the fixed version comes back for another review. DO NOT POST means it should not go at all. After the header, number your findings, each with
|
|
222
|
+
POST AS IS means nothing BLOCKING or SHOULD FIX remains and nothing was skipped. POST WITH CHANGES means the author fixes what you found and the fixed version comes back for another review. DO NOT POST means it should not go at all. An item that does not apply to this change is not skipped: say in its place why it does not apply. A skipped item is one you did not do, for time, tools or access, and the gate refuses POST AS IS over it. After the header, number your findings, each on its own line as "N. SEVERITY: title" with SEVERITY one of BLOCKING, SHOULD FIX or NIT, then the evidence, and the exact fix, with replacement text in full for any wording. The gate reads those lines: a BLOCKING or SHOULD FIX line under POST AS IS is refused, so a later round that lists an earlier round's findings as fixed renumbers them as NIT or drops the severity. A report with no findings says what was checked and how. The blind fix is a unified diff with its hunks.
|
|
222
223
|
EOF
|
|
223
224
|
}
|
|
224
225
|
|
|
@@ -261,8 +262,22 @@ cmd_check() {
|
|
|
261
262
|
blind=$(field blind-fix)
|
|
262
263
|
[ -n "$blind" ] || refuse "$report reviews code at ${head:0:12} and names no blind-fix: the reviewer's own fix, written before reading ours"
|
|
263
264
|
[ -f "$blind" ] || refuse "the blind fix $blind named in $report does not exist"
|
|
265
|
+
if ! grep -qE '^(diff --git |--- )' "$blind" || ! grep -q '^@@' "$blind"; then
|
|
266
|
+
refuse "the blind fix $blind is not a diff: it must carry the reviewer's own change, hunks and all"
|
|
267
|
+
fi
|
|
264
268
|
fi
|
|
265
269
|
|
|
270
|
+
# The verdict is the header; the findings are the body. A POST AS IS over an
|
|
271
|
+
# open BLOCKING or SHOULD FIX finding, or over items the reviewer never
|
|
272
|
+
# attacked, is a header that disagrees with its own report.
|
|
273
|
+
local skipped open
|
|
274
|
+
skipped=$(field skipped)
|
|
275
|
+
[ -n "$skipped" ] || refuse "$report has no skipped: line in its header - 'none', or the items not attacked"
|
|
276
|
+
printf '%s' "$skipped" | grep -qiE '^none\.?$' ||
|
|
277
|
+
refuse "the skipped: line must read exactly none for POST AS IS, or list the items not attacked ($skipped): attack them or review again"
|
|
278
|
+
open=$(grep -iE '^[[:space:]#*]*([0-9]+[.)]|[-*])[[:space:]*[]*(BLOCKING|SHOULD FIX)\b|^[[:space:]*-]*severity:[[:space:]*]*(BLOCKING|SHOULD FIX)\b' "$report")
|
|
279
|
+
[ -z "$open" ] || refuse "the verdict is POST AS IS but the report lists open findings: $(printf '%s' "$open" | head -1 | cut -c1-120)"
|
|
280
|
+
|
|
266
281
|
if [ -n "$clone" ]; then
|
|
267
282
|
now=$(git -C "$clone" rev-parse HEAD 2>/dev/null) || bad_usage "$clone is not a git clone"
|
|
268
283
|
[ -n "$head" ] || refuse "$report records no head, so it cannot clear a branch"
|
|
@@ -66,6 +66,36 @@ echo ""
|
|
|
66
66
|
echo "-- files in the PR (what a reviewer sees)"
|
|
67
67
|
view files '.files[] | " " + .path + " +" + (.additions|tostring) + "/-" + (.deletions|tostring)'
|
|
68
68
|
|
|
69
|
+
# The intended files are the branch-check stamp's, and the intended body is the
|
|
70
|
+
# copy file-pr.sh kept; GitHub's versions are compared to both.
|
|
71
|
+
home="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
|
|
72
|
+
stamp="$home/branch-checks/$head"
|
|
73
|
+
intended=""
|
|
74
|
+
[ -f "$stamp" ] && intended=$(sed -n 's/^file: //p' "$stamp" | tr -d '\r' | sort)
|
|
75
|
+
if [ -n "$intended" ]; then
|
|
76
|
+
filed=$(view files '.files[].path' | sort)
|
|
77
|
+
if [ "$intended" = "$filed" ]; then
|
|
78
|
+
note "files match the stamp" "yes"
|
|
79
|
+
else
|
|
80
|
+
note "FILES DIFFER FROM STAMP" "$(diff <(printf '%s\n' "$intended") <(printf '%s\n' "$filed") | grep '^[<>]' | tr '\n' ' ')"
|
|
81
|
+
fail=1
|
|
82
|
+
fi
|
|
83
|
+
else
|
|
84
|
+
note "files" "no branch-check stamp listing files for ${head:0:12}; not compared"
|
|
85
|
+
fi
|
|
86
|
+
kept="$home/filed/${repo%%/*}-${repo##*/}-$head.md"
|
|
87
|
+
if [ -f "$kept" ]; then
|
|
88
|
+
norm() { tr -d '\r' | sed -e 's/[[:space:]]*$//' | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}'; }
|
|
89
|
+
if [ "$(view body .body | norm)" = "$(norm < "$kept")" ]; then
|
|
90
|
+
note "body matches what was filed" "yes"
|
|
91
|
+
else
|
|
92
|
+
note "BODY DIFFERS" "from $kept; read the diff, a maintainer may have edited it"
|
|
93
|
+
diff <(norm < "$kept") <(view body .body | norm) | head -20 | sed 's/^/ /'
|
|
94
|
+
fi
|
|
95
|
+
else
|
|
96
|
+
note "body" "no filed copy for ${head:0:12}; not compared"
|
|
97
|
+
fi
|
|
98
|
+
|
|
69
99
|
echo ""
|
|
70
100
|
echo "-- checks (waits until every check has settled)"
|
|
71
101
|
gh pr checks "$pr" --repo "$repo" --watch --interval 20 >/dev/null 2>&1 || true
|
|
@@ -43,7 +43,10 @@ report it in one line and do not proceed to the next step.
|
|
|
43
43
|
patch is worth. DISCLOSE means the body says, in one truthful sentence
|
|
44
44
|
and in the template's own words where it has them, that AI was used and
|
|
45
45
|
which tool. MENTION means read the lines and decide which of the two it
|
|
46
|
-
is.
|
|
46
|
+
is. UNREAD means GitHub did not give every policy file: wait and run it
|
|
47
|
+
again, never file on it; a repository too large for GitHub to list in
|
|
48
|
+
full reads UNREAD until you have read its policy by hand and passed
|
|
49
|
+
`--accept-truncated`. The printout is required by step 9. If the project accepts pull
|
|
47
50
|
requests only for labelled issues, the deliverable is an issue with the
|
|
48
51
|
fix offered, not a pull request. `housebroken outside owner/repo` counts
|
|
49
52
|
outside contributors merged in the last 120 days. Zero means the project
|
|
@@ -56,8 +59,11 @@ report it in one line and do not proceed to the next step.
|
|
|
56
59
|
every closed item. Read every closed item. An open pull request that fixes
|
|
57
60
|
the same thing means you stop and say so. A closed issue that ruled the
|
|
58
61
|
behavior intended means you stop and say so; you never argue with a
|
|
59
|
-
ruling.
|
|
60
|
-
|
|
62
|
+
ruling. A term that matches more than one page is printed as "showing N
|
|
63
|
+
of M" and the printout is not saved: narrow the term, or pass `--partial`
|
|
64
|
+
when you have read enough to say so. The printout is required by step 9;
|
|
65
|
+
write it before the branch exists, with every file the patch changes
|
|
66
|
+
among its terms, because the filing gate checks them against the branch.
|
|
61
67
|
|
|
62
68
|
3. **Is it a fix or an opinion?**
|
|
63
69
|
A change that rejects an input the project tolerated, changes a default,
|
|
@@ -136,7 +142,10 @@ report it in one line and do not proceed to the next step.
|
|
|
136
142
|
the body and re-measure every line it lists, now, against the exact
|
|
137
143
|
revision that sentence names. A claim that names its revision can still
|
|
138
144
|
be wrong about it, which is why the listing is the whole gate: the
|
|
139
|
-
re-derivation is yours to do.
|
|
145
|
+
re-derivation is yours to do. Run `housebroken refs owner/repo <file>
|
|
146
|
+
--clone <clone>` too: every issue number, link, test name, file and
|
|
147
|
+
commit the text names is looked up, and one that does not exist stops
|
|
148
|
+
the filing.
|
|
140
149
|
|
|
141
150
|
7. **Know the paperwork.**
|
|
142
151
|
Before the first pull request to an organization, find its agreement: CLA,
|
|
@@ -157,11 +166,19 @@ report it in one line and do not proceed to the next step.
|
|
|
157
166
|
whose mode disagrees with its own siblings, a file the branch adds that the
|
|
158
167
|
repository ignores, a commit carrying a tool trailer, a session link or a
|
|
159
168
|
tool identity, a sign-off that names a username, a branch that is not on
|
|
160
|
-
top of its base,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
169
|
+
top of its base, a base that is not a remote-tracking ref (it notes one the
|
|
170
|
+
upstream has moved past; the filing refuses it when the move touches your
|
|
171
|
+
files), any comment added to code
|
|
172
|
+
(`--asked <URL>` when the maintainer asked for it), a warning suppression
|
|
173
|
+
that names no warning or whose warning `--warning-log` does not show
|
|
174
|
+
firing, reflection added to a .NET project that declares AOT or trim
|
|
175
|
+
support until `--aot-log` shows an AOT publish of that project you ran the
|
|
176
|
+
changed path under, a credential or a path under your own home in an
|
|
177
|
+
added line, a
|
|
178
|
+
lockfile, vendored, generated or binary file (`--allow <path>` when the
|
|
179
|
+
change is about it), an agent's working file such as an added CLAUDE.md
|
|
180
|
+
or `.claude/`, and a file rewritten for its line endings or whitespace;
|
|
181
|
+
it prints every absolute claim your added prose
|
|
165
182
|
makes so you can falsify each one against the code, and it stamps the
|
|
166
183
|
head it passed, which the filing requires. Then the adversarial review:
|
|
167
184
|
`housebroken review brief --author <your model> --repo owner/repo --clone <clone> --text <body file>`
|
|
@@ -180,26 +197,34 @@ report it in one line and do not proceed to the next step.
|
|
|
180
197
|
the changed block the way the strongest engineer in that language would,
|
|
181
198
|
and a smaller or clearer version is a finding you fix before filing. It
|
|
182
199
|
writes its report where the brief says, with its own evidence behind
|
|
183
|
-
every finding
|
|
200
|
+
every finding, each on its own line as "N. SEVERITY: title", and a
|
|
201
|
+
`skipped:` header line that reads `none` or lists what it did not attack.
|
|
202
|
+
The gate refuses a blind fix that is not a diff, a POST AS IS over a
|
|
203
|
+
BLOCKING or SHOULD FIX line, and a POST AS IS over skipped items.
|
|
204
|
+
Every fix it asks for is a new head,
|
|
184
205
|
and a new head is reviewed again. When it passes, re-read every claim it
|
|
185
206
|
did not re-derive itself: a passed review is not a proof. Then
|
|
186
207
|
`housebroken file owner/repo --title ... --body-file ...`, the only way a
|
|
187
208
|
pull request is opened. Never run the pull request creation command
|
|
188
209
|
directly. The gate refuses when the prior-art printout for that repository
|
|
189
|
-
is missing
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
210
|
+
is missing, older than a day or never searched a file the branch changes,
|
|
211
|
+
when the policy printout is missing, older than a day or reads BAN or
|
|
212
|
+
UNREAD, when the policy reads DISCLOSE and the body does not disclose,
|
|
213
|
+
when the body carries a footer, a trailer or a dash character, runs past
|
|
214
|
+
120 words of its own beside the project's template, or names an issue,
|
|
215
|
+
link, test, file or commit that does not exist, when `housebroken branch`
|
|
216
|
+
has not stamped the exact head against the branch it is filed into
|
|
217
|
+
(`--base` to both when the project takes pull requests on a branch other
|
|
218
|
+
than its default), when the upstream has since changed a file the branch
|
|
219
|
+
changes, when there is no POST AS IS review of the exact head and body
|
|
220
|
+
being filed, when you already have three open pull requests on the
|
|
221
|
+
repository, and when your last one there is under ten minutes old.
|
|
197
222
|
A refusal is a result; fix the cause.
|
|
198
223
|
|
|
199
224
|
10. **Watch it land.**
|
|
200
225
|
`housebroken verify owner/repo PR` re-derives from GitHub that the head is
|
|
201
|
-
the intended commit, the
|
|
202
|
-
settled. `housebroken sweep` lists every open pull request where the ball
|
|
226
|
+
the intended commit, the files are the ones the branch stamp lists, the
|
|
227
|
+
body is the one that was filed, and CI settled. `housebroken sweep` lists every open pull request where the ball
|
|
203
228
|
is in your court, and prints a `TIDY` line for every merged or closed
|
|
204
229
|
pull request whose fork branch still exists, on every run until
|
|
205
230
|
`housebroken hygiene` has deleted it. `housebroken inbox` is the first thing a session does
|
|
@@ -237,7 +262,8 @@ report it in one line and do not proceed to the next step.
|
|
|
237
262
|
|
|
238
263
|
11. **Clean up.**
|
|
239
264
|
`housebroken hygiene` deletes the fork branch of every merged or closed
|
|
240
|
-
pull request
|
|
265
|
+
pull request, unless an open one still uses it, and lists forks with no
|
|
266
|
+
pull request left; delete those when
|
|
241
267
|
the work is over. No planning file, agent directory, journal or build
|
|
242
268
|
output ever enters a diff.
|
|
243
269
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.6.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|