housebroken-cli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,75 @@
1
+ #!/bin/bash
2
+ # pr-sweep.sh - every open upstream PR of ours, and which ones need us to act.
3
+ #
4
+ # Every maintainer comment gets a same-day answer; without a sweep the answer
5
+ # depends on remembering which PR was touched. Ball in our court means any of:
6
+ # the newest comment or review is not ours (bots excluded), a review requests
7
+ # changes, CI has a failing check, or the PR is CONFLICTING (needs a rebase).
8
+ # Read-only; acting is the operator's.
9
+ #
10
+ # Usage:
11
+ # bash pr-sweep.sh # prints only PRs where the ball is in our court
12
+ # bash pr-sweep.sh --all # prints every open PR with its state
13
+ # bash pr-sweep.sh --help
14
+ #
15
+ # Environment:
16
+ # HOUSEBROKEN_USER GitHub login to sweep, default the authenticated user
17
+ #
18
+ # Assumes: gh is installed and authenticated, and that the PRs of interest are
19
+ # the ones authored by that login. Exits non-zero when gh fails or the search
20
+ # returns nothing.
21
+ set -u
22
+
23
+ usage() {
24
+ sed -n '2,/^set -u$/p' "$0" | sed 's/^# \{0,1\}//; /^set -u$/d'
25
+ }
26
+
27
+ ALL=0
28
+ case "${1:-}" in
29
+ --help|-h) usage; exit 0 ;;
30
+ --all) ALL=1 ;;
31
+ "") ;;
32
+ *) echo "pr-sweep.sh: unknown argument: $1" >&2; usage >&2; exit 2 ;;
33
+ esac
34
+
35
+ ME="${HOUSEBROKEN_USER:-$(gh api user --jq .login)}"
36
+ if [ -z "$ME" ]; then
37
+ echo "pr-sweep.sh: cannot determine the GitHub login; set HOUSEBROKEN_USER" >&2
38
+ exit 1
39
+ fi
40
+
41
+ if ! list="$(gh search prs --author "$ME" --state open --limit 100 --json repository,number,title,updatedAt \
42
+ --jq '.[] | "\(.repository.nameWithOwner)\t\(.number)\t\(.updatedAt)\t\(.title)"' | sort)"; then
43
+ echo "pr-sweep.sh: gh search prs failed for author $ME" >&2
44
+ exit 1
45
+ fi
46
+ [ -n "$list" ] || { echo "pr-sweep.sh: no open PRs found for $ME" >&2; exit 1; }
47
+ n=0; act=0
48
+ while IFS=$'\t' read -r repo num updated title; do
49
+ [ -n "$repo" ] || continue
50
+ n=$((n + 1))
51
+ if ! j="$(gh pr view "$num" --repo "$repo" --json mergeable,reviewDecision,statusCheckRollup,comments,reviews,latestReviews)"; then
52
+ printf '%-40s #%-5s (gh pr view failed)\n' "$repo" "$num" >&2
53
+ continue
54
+ fi
55
+ last_actor="$(printf '%s' "$j" | jq -r '
56
+ [ (.comments[]? | {a: .author.login, t: .createdAt}), (.reviews[]? | {a: .author.login, t: .submittedAt}) ]
57
+ | map(select(.a != null and (.a | test("bot|\\[bot\\]|github-actions|codecov|dependabot|cla-|-cla|claassistant|coderabbit|devin|coveralls|vercel|asf-|apache-"; "i") | not)))
58
+ | sort_by(.t) | last | "\(.a // "-") \(.t // "-")"')"
59
+ la_user="${last_actor%% *}"; la_time="${last_actor#* }"
60
+ ci="$(printf '%s' "$j" | jq -r '[.statusCheckRollup[]? | (.conclusion // .state // "PENDING")] | if length == 0 then "none" elif any(. == "FAILURE" or . == "ERROR") then "FAILING" elif any(. == "PENDING" or . == "IN_PROGRESS" or . == "QUEUED" or . == "EXPECTED") then "pending" else "green" end')"
61
+ mergeable="$(printf '%s' "$j" | jq -r '.mergeable // "-"')"
62
+ decision="$(printf '%s' "$j" | jq -r '.reviewDecision // "-"')"
63
+ why=""
64
+ [ "$la_user" != "-" ] && [ "$la_user" != "$ME" ] && why="$why last-word:$la_user@${la_time:0:10}"
65
+ [ "$decision" = "CHANGES_REQUESTED" ] && why="$why changes-requested"
66
+ [ "$ci" = "FAILING" ] && why="$why ci-failing"
67
+ [ "$mergeable" = "CONFLICTING" ] && why="$why conflicting"
68
+ if [ -n "$why" ]; then
69
+ act=$((act + 1))
70
+ printf 'ACT %-38s #%-5s %s [%s] %s\n' "$repo" "$num" "${title:0:60}" "${why# }" "https://github.com/$repo/pull/$num"
71
+ elif [ "$ALL" = 1 ]; then
72
+ printf 'ok %-38s #%-5s %s ci=%s mergeable=%s updated=%s\n' "$repo" "$num" "${title:0:60}" "$ci" "$mergeable" "${updated:0:10}"
73
+ fi
74
+ done <<< "$list"
75
+ echo "open PRs: $n; needing action: $act"
@@ -0,0 +1,149 @@
1
+ #!/bin/bash
2
+ # prior-art.sh - list every prior issue and PR on a repo that touches a term,
3
+ # before a PR is written, so the duplicate check is a printout and not a memory.
4
+ #
5
+ # Two PRs were closed as duplicates because this was done by hand. typer #1946
6
+ # (closed 2026-08-31) duplicated OPEN PR typer #1881: gh search issues returns
7
+ # issues and PRs mixed and the type was never checked, so this script prints the
8
+ # type from the API field and never infers it. swift-http-types #153 (closed
9
+ # 2026-09-04) argued against a ruling already made in CLOSED issue #98, which
10
+ # nobody read, so this script prints the last non-author comment on every closed
11
+ # item. A term containing a slash or a dot is treated as a file path and the
12
+ # commit history of that path is scanned for the PRs that touched it.
13
+ # Written 2026-09-07.
14
+ #
15
+ # Usage:
16
+ # bash prior-art.sh owner/repo <term> [<term> ...] [--out]
17
+ # bash prior-art.sh --help
18
+ #
19
+ # --out also save the printout to $HOUSEBROKEN_HOME/prior-art/, where
20
+ # file-pr.sh looks for it before it will file anything.
21
+ #
22
+ # Environment:
23
+ # HOUSEBROKEN_HOME work directory, default $HOME/.housebroken. --out writes
24
+ # to its prior-art/ subdirectory, created if missing.
25
+ #
26
+ # Assumes gh is installed and authenticated, and jq is on PATH. Read-only: it
27
+ # makes GET calls and writes nothing to GitHub.
28
+ set -u
29
+
30
+ usage() {
31
+ cat <<'EOF'
32
+ usage: prior-art.sh owner/repo <term>... [--out]
33
+ prior-art.sh --help
34
+
35
+ owner/repo the upstream repository to search
36
+ <term> a symbol, phrase or file path; a term with a slash or a dot is
37
+ also treated as a path and its commit history is scanned
38
+ --out save the printout under $HOUSEBROKEN_HOME/prior-art/
39
+
40
+ environment:
41
+ HOUSEBROKEN_HOME work directory, default $HOME/.housebroken
42
+ EOF
43
+ }
44
+
45
+ for arg in "$@"; do
46
+ if [ "$arg" = "--help" ] || [ "$arg" = "-h" ]; then usage; exit 0; fi
47
+ done
48
+
49
+ command -v gh >/dev/null 2>&1 || { echo "prior-art.sh: gh is not on PATH" >&2; exit 1; }
50
+ command -v jq >/dev/null 2>&1 || { echo "prior-art.sh: jq is not on PATH" >&2; exit 1; }
51
+
52
+ repo=""
53
+ out=0
54
+ terms=()
55
+ for arg in "$@"; do
56
+ case "$arg" in
57
+ --out) out=1 ;;
58
+ --*) usage >&2; echo "prior-art.sh: unknown option $arg" >&2; exit 1 ;;
59
+ */*) if [ -z "$repo" ]; then repo="$arg"; else terms+=("$arg"); fi ;;
60
+ *) if [ -z "$repo" ]; then usage >&2; exit 1; fi
61
+ terms+=("$arg") ;;
62
+ esac
63
+ done
64
+ if [ -z "$repo" ] || [ "${#terms[@]}" -eq 0 ]; then
65
+ usage >&2; exit 1
66
+ fi
67
+
68
+ home="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
69
+ calls=0
70
+ items=0
71
+ tmp=$(mktemp) || { echo "prior-art.sh: cannot create temp file" >&2; exit 1; }
72
+ row=$(mktemp) || { echo "prior-art.sh: cannot create temp file" >&2; exit 1; }
73
+ trap 'rm -f "$tmp" "$row"' EXIT
74
+
75
+ # last comment on a closed item written by somebody other than its author
76
+ last_word() {
77
+ local num="$1" author="$2" body
78
+ body=$(gh api "repos/$repo/issues/$num/comments?per_page=100" 2>/dev/null |
79
+ jq -r --arg a "$author" '[.[] | select(.user.login != $a)] | last | .body // ""') || return 0
80
+ printf '%s' "$body" | tr '\n\r\t|' ' ' | tr -s ' ' | cut -c1-160
81
+ }
82
+
83
+ emit() {
84
+ local kind num state date author title word=""
85
+ while IFS=$'\t' read -r kind num state date author title; do
86
+ [ -n "$num" ] || continue
87
+ word=""
88
+ if [ "$state" = "closed" ]; then word=$(last_word "$num" "$author"); calls=$((calls+1)); fi
89
+ printf '| %s | #%s | %s | %s | %s | %s | %s |\n' \
90
+ "$kind" "$num" "$state" "$date" "$author" "$title" "$word" >>"$tmp"
91
+ items=$((items+1))
92
+ done <"$row"
93
+ }
94
+
95
+ printf '## Prior art: %s (%s)\n' "$repo" "$(date '+%Y-%m-%d %H:%M %Z')" >"$tmp"
96
+
97
+ for term in "${terms[@]}"; do
98
+ {
99
+ printf '\n### %s\n\n' "$term"
100
+ printf '| type | number | state | date | author | title | last maintainer word |\n'
101
+ printf '| --- | --- | --- | --- | --- | --- | --- |\n'
102
+ } >>"$tmp"
103
+
104
+ gh api -X GET search/issues -f q="repo:$repo type:issue \"$term\"" -F per_page=10 \
105
+ --jq '.items[] | ["issue", (.number|tostring), .state, ((.closed_at // .created_at)[0:10]),
106
+ .user.login, (.title|gsub("[|]";"/")|gsub("[\n\r\t]";" "))] | @tsv' >"$row" ||
107
+ { echo "prior-art.sh: issue search failed for $repo term '$term'" >&2; exit 1; }
108
+ calls=$((calls+1))
109
+ emit
110
+
111
+ gh api -X GET search/issues -f q="repo:$repo type:pr \"$term\"" -F per_page=10 \
112
+ --jq '.items[] | ["PR", (.number|tostring),
113
+ (if .pull_request.merged_at then "merged" else .state end),
114
+ ((.pull_request.merged_at // .closed_at // .created_at)[0:10]),
115
+ .user.login, (.title|gsub("[|]";"/")|gsub("[\n\r\t]";" "))] | @tsv' >"$row" ||
116
+ { echo "prior-art.sh: PR search failed for $repo term '$term'" >&2; exit 1; }
117
+ calls=$((calls+1))
118
+ emit
119
+
120
+ # a path term also gets the PRs whose merge commits touched that path
121
+ case "$term" in
122
+ */*|*.*)
123
+ gh api "repos/$repo/commits?path=$term&per_page=30" \
124
+ --jq '.[] | [(.commit.message|split("\n")[0]|gsub("[|]";"/")),
125
+ (.commit.author.date[0:10]), (.author.login // .commit.author.name)] | @tsv' \
126
+ 2>/dev/null >"$row" || true
127
+ calls=$((calls+1))
128
+ while IFS=$'\t' read -r subject date author; do
129
+ num=$(printf '%s' "$subject" | grep -oE '#[0-9]+' | head -1 | tr -d '#')
130
+ [ -n "${num:-}" ] || continue
131
+ grep -q "^| PR | #$num |" "$tmp" && continue
132
+ printf '| PR | #%s | merged | %s | %s | %s | |\n' "$num" "$date" "$author" "$subject" >>"$tmp"
133
+ items=$((items+1))
134
+ done <"$row"
135
+ ;;
136
+ esac
137
+ done
138
+
139
+ printf '\n%d items across %d terms. Read every closed item before filing; this script lists, it does not judge.\n' \
140
+ "$items" "${#terms[@]}" >>"$tmp"
141
+ printf 'gh calls: %d\n' "$calls" >>"$tmp"
142
+
143
+ cat "$tmp"
144
+ if [ "$out" -eq 1 ]; then
145
+ dir="$home/prior-art"
146
+ mkdir -p "$dir" || { echo "prior-art.sh: cannot create $dir" >&2; exit 1; }
147
+ cp "$tmp" "$dir/prior-art-${repo%%/*}-${repo##*/}.md" ||
148
+ { echo "prior-art.sh: cannot write $dir/prior-art-${repo%%/*}-${repo##*/}.md" >&2; exit 1; }
149
+ fi
@@ -0,0 +1,90 @@
1
+ #!/bin/bash
2
+ # verify-filed-pr.sh - prove a filed PR actually landed as intended.
3
+ #
4
+ # Filing is not the end of the job. This re-derives, from GitHub rather than
5
+ # from local memory, that: the PR head is the commit meant to be there, the
6
+ # diff contains exactly the intended files, CI settled green, and the rendered
7
+ # body and comments are what was written. Written 2026-09-01 after console
8
+ # PR #296 went red on CI and the maintainer saw it before the author did.
9
+ # The check wait was added 2026-09-02, when Valinor #838 was left at "still
10
+ # running" and its mutation job failed unseen.
11
+ #
12
+ # Usage:
13
+ # bash verify-filed-pr.sh owner/repo PR_NUMBER [expected-head-sha]
14
+ # bash verify-filed-pr.sh --help
15
+ #
16
+ # Environment:
17
+ # none
18
+ #
19
+ # Assumes: gh is installed and authenticated with read access to the repo, and
20
+ # that the caller knows which head sha and which files were intended. Exits
21
+ # non-zero when the head does not match, when checks are red, or when they do
22
+ # not settle before the watch gives up.
23
+ set -u
24
+
25
+ usage() {
26
+ sed -n '2,/^set -u$/p' "$0" | sed 's/^# \{0,1\}//; /^set -u$/d'
27
+ }
28
+
29
+ case "${1:-}" in
30
+ --help|-h) usage; exit 0 ;;
31
+ esac
32
+
33
+ if [ "$#" -lt 2 ]; then
34
+ echo "usage: verify-filed-pr.sh owner/repo PR [sha]" >&2
35
+ exit 2
36
+ fi
37
+ repo="$1"
38
+ pr="$2"
39
+ want="${3:-}"
40
+ fail=0
41
+ note() { printf '%-26s %s\n' "$1" "$2"; }
42
+
43
+ view() {
44
+ local field="$1" jqexpr="$2"
45
+ if ! gh pr view "$pr" --repo "$repo" --json "$field" --jq "$jqexpr"; then
46
+ echo "verify-filed-pr.sh: gh pr view $repo#$pr ($field) failed" >&2
47
+ exit 1
48
+ fi
49
+ }
50
+
51
+ state=$(view state .state)
52
+ head=$(view headRefOid .headRefOid)
53
+ note "state" "$state"
54
+ note "head" "${head:0:12}"
55
+ if [ -n "$want" ]; then
56
+ case "$head" in
57
+ "$want"*) note "head matches expected" "yes" ;;
58
+ *) note "HEAD MISMATCH" "expected $want"; fail=1 ;;
59
+ esac
60
+ fi
61
+
62
+ echo ""
63
+ echo "-- files in the PR (what a reviewer sees)"
64
+ view files '.files[] | " " + .path + " +" + (.additions|tostring) + "/-" + (.deletions|tostring)'
65
+
66
+ echo ""
67
+ echo "-- checks (waits until every check has settled)"
68
+ gh pr checks "$pr" --repo "$repo" --watch --interval 20 >/dev/null 2>&1 || true
69
+ rollup=$(view statusCheckRollup '[.statusCheckRollup[] | (.conclusion // .status)] | group_by(.) | map({k:.[0], n:length}) | .[] | .k + "=" + (.n|tostring)')
70
+ if [ -z "$rollup" ]; then
71
+ note "checks" "none reported"
72
+ else
73
+ echo "$rollup" | awk '{print " " $0}'
74
+ case "$rollup" in
75
+ *FAILURE*|*TIMED_OUT*|*CANCELLED*|*ACTION_REQUIRED*) note "CHECKS NOT GREEN" "read the logs"; fail=1 ;;
76
+ *IN_PROGRESS*|*QUEUED*|*PENDING*) note "CHECKS DID NOT SETTLE" "watch timed out"; fail=1 ;;
77
+ esac
78
+ fi
79
+
80
+ echo ""
81
+ echo "-- rendered body, first 15 lines"
82
+ view body .body | head -15 | sed 's/^/ /'
83
+
84
+ echo ""
85
+ echo "-- comments, newest last"
86
+ view comments '.comments[] | " " + .author.login + ": " + (.body | gsub("\n"; " ") | .[0:150])'
87
+
88
+ echo ""
89
+ if [ "$fail" -eq 0 ]; then echo "PR-VERIFY: OK"; else echo "PR-VERIFY: PROBLEMS ABOVE"; fi
90
+ exit "$fail"
@@ -0,0 +1,144 @@
1
+ ---
2
+ name: housebroken
3
+ description: >
4
+ The door every pull request to a repository you do not own must pass
5
+ through before it is opened, and the manners it keeps after. Use whenever a
6
+ task will end, or might end, in a pull request or issue on someone else's
7
+ repository: fixing a bug found upstream, contributing a patch, filing a
8
+ finding, answering a maintainer's review, or cleaning up after a merge or a
9
+ close. Also use when the user says "housebroken", "file this upstream",
10
+ "open a PR on", "send this to the maintainers", or asks whether a patch is
11
+ ready to file. Do NOT use for pull requests inside the user's own
12
+ repositories.
13
+ license: MIT
14
+ ---
15
+
16
+ # housebroken
17
+
18
+ You are about to knock on a maintainer's door. She runs the project on
19
+ evenings and weekends and closes most agent pull requests without reading
20
+ them, because most of them deserve it. Every rule below was paid for with a
21
+ closed pull request. You do not skip a rule because the patch is small.
22
+
23
+ ## Persistence
24
+
25
+ Active from the moment a task might produce a pull request or an issue on a
26
+ repository the user does not own, until the pull request is merged or closed
27
+ and the fork branch is deleted. Still active if unsure. Off only when the user
28
+ says "not upstream" or the repository is the user's own.
29
+
30
+ ## The door, in order
31
+
32
+ Stop at the first step that fails. A failed step is a result, not an obstacle:
33
+ report it in one line and do not proceed to the next step.
34
+
35
+ 1. **Read the house rules before knocking.**
36
+ `housebroken policy owner/repo` reads the contribution policy on the
37
+ default branch and in the organization's `.github` repository and prints
38
+ the sentence. Read it, then read it on the development branch too
39
+ (`--branch dev`) when one exists. If the policy asks contributors not to
40
+ use AI for pull request or issue text, stop: never file there with text
41
+ you wrote. If the project accepts pull requests only for labelled issues,
42
+ the deliverable is an issue with the fix offered, not a pull request.
43
+ `housebroken outside owner/repo` counts outside contributors merged in the
44
+ last 120 days. Zero means the project is closed to outsiders whatever its
45
+ README says; stop.
46
+
47
+ 2. **Is it already on the table?**
48
+ `housebroken prior-art owner/repo <file or symbol> ... --out` lists every
49
+ issue and pull request, open, closed and merged, that touches what the
50
+ patch touches, with the type stated and the closing ruling quoted for
51
+ every closed item. Read every closed item. An open pull request that fixes
52
+ the same thing means you stop and say so. A closed issue that ruled the
53
+ behavior intended means you stop and say so; you never argue with a
54
+ ruling. The printout is required by step 9; write it before the branch
55
+ exists.
56
+
57
+ 3. **Is it a fix or an opinion?**
58
+ A change that rejects an input the project tolerated, or changes a
59
+ default, is a breaking change on a stable major, however wrong the old
60
+ behavior looks. It becomes an issue that describes the behavior and offers
61
+ the patch, never a pull request. Classify every patch before writing a
62
+ test for it.
63
+
64
+ 4. **Prove it red first.**
65
+ On a fresh clone of the upstream default branch, not the tree you found
66
+ the bug in, write the test that fails. Run it and keep the failing output.
67
+ Apply the patch. Run it again. Both results go into the pull request as
68
+ the test itself; the body states the fact in one sentence.
69
+
70
+ 5. **Run their CI, not yours.**
71
+ Run the project's own test target on the fresh clone, and every gate its
72
+ pull request workflow runs: lint, format, mutation, API check. Read the
73
+ workflow file to find them. A gate you did not run is a red check the
74
+ maintainer sees before you do. When the repository ships a Dangerfile, a
75
+ pre-commit config or a commitlint config, those are its acceptance
76
+ criteria in machine-readable form; run them.
77
+
78
+ 6. **Match the house style.**
79
+ One finding, one pull request, the smallest diff that fixes it. No
80
+ comment in code that has none: run `housebroken census <clone root>` and
81
+ compare the added-comment ratio with the file. The body is under 120 words,
82
+ in the project's template if it has one, and says what was wrong, what the
83
+ change does, and how it was verified. No tool footer, no session link, no
84
+ co-author trailer, no typographic dashes. When a template asks whether AI
85
+ was used, answer in one truthful sentence.
86
+
87
+ 7. **Know the paperwork.**
88
+ Before the first pull request to an organization, find its agreement: CLA,
89
+ DCO sign-off, signed-commit requirement, disclosure trailer. Some CLA
90
+ actions post nothing on the pull request and write the instructions into a
91
+ failed job's log; read the log when a check named cla or license is red.
92
+ Some organizations want a trailer such as Assisted-by or Generated-by and
93
+ some forbid one; use exactly what the organization asks.
94
+
95
+ 8. **Security goes through the side door.**
96
+ A memory-safety, remote-abort, injection or path-escape finding in a
97
+ library goes by the project's SECURITY.md route, privately, with the
98
+ reproduction and the patch attached. It never becomes a public issue or
99
+ pull request until the project answers.
100
+
101
+ 9. **File through the gate.**
102
+ `housebroken file owner/repo --title ... --body-file ...` is the only way a
103
+ pull request is opened. Never run the pull request creation command
104
+ directly. The gate refuses when the prior-art printout for that repository
105
+ is missing or older than a day, or when the body carries a footer, a
106
+ trailer or a dash character. A refusal is a result; fix the cause.
107
+
108
+ 10. **Watch it land.**
109
+ `housebroken verify owner/repo PR` re-derives from GitHub that the head is
110
+ the intended commit, the diff is exactly the intended files, and CI
111
+ settled. `housebroken sweep` lists every open pull request where the ball
112
+ is in your court. Answer every maintainer comment the same day, in the
113
+ user's voice, after reading the whole thread. Rework what is asked for as
114
+ a second commit so the reviewer sees the change. When the maintainer is
115
+ right, concede in one sentence and let them close it. Never nudge a
116
+ silent maintainer without the user's explicit word.
117
+
118
+ 11. **Clean up.**
119
+ `housebroken hygiene` deletes the fork branch of every merged or closed
120
+ pull request and lists forks with no pull request left; delete those when
121
+ the work is over. No planning file, agent directory, journal or build
122
+ output ever enters a diff.
123
+
124
+ 12. **Three per repository, one finding each.**
125
+ Never more than three open pull requests on one repository. Each one
126
+ finding, each passing every step above on its own. GitHub lets
127
+ maintainers cap open pull requests from non-collaborators and counts
128
+ agent-opened ones against that cap.
129
+
130
+ ## What you say to the user
131
+
132
+ Before filing: the repository, the finding in one sentence, the prior-art
133
+ result in one sentence, the red-then-green result, the agreement the
134
+ organization wants, and the exact command you are about to run. After
135
+ filing: the pull request URL and the verify result. On a refusal at any
136
+ step: which step, and the one sentence that explains it.
137
+
138
+ ## Never
139
+
140
+ Never open a pull request without the prior-art printout. Never argue with a
141
+ ruling in a closed issue. Never file where the policy asks you not to. Never
142
+ put a footer, a session link or a co-author trailer on anything that leaves
143
+ the user's repositories. Never post a comment upstream that the user has not
144
+ seen. Never claim a test ran that you did not run.
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.5
2
+ Name: housebroken-cli
3
+ Version: 0.1.0
4
+ Summary: Agent-written pull requests that do not make a mess in someone else's house.
5
+ Project-URL: Repository, https://github.com/lenamonj/housebroken
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+
13
+ <p align="center">
14
+ <img src="assets/door.jpg" width="560" alt="A white ceramic robot dog sits at an open, lit door, a scroll in its mouth, its paws stopped at a glowing amber line on the threshold">
15
+ </p>
16
+
17
+ <h1 align="center">housebroken</h1>
18
+
19
+ <p align="center"><em>Agent-written pull requests that do not make a mess in someone else's house.</em></p>
20
+
21
+ <p align="center">
22
+ <img src="https://img.shields.io/github/stars/lenamonj/housebroken?style=flat-square&color=111111&label=stars" alt="Stars">
23
+ <img src="https://img.shields.io/github/v/release/lenamonj/housebroken?style=flat-square&color=111111&label=release" alt="Release">
24
+ <img src="https://img.shields.io/pypi/v/housebroken-cli?style=flat-square&color=111111&label=pypi" alt="PyPI">
25
+ <img src="https://img.shields.io/badge/works%20with-Claude%20Code-111111?style=flat-square" alt="Works with Claude Code">
26
+ <img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT license">
27
+ </p>
28
+
29
+ <p align="center">
30
+ <img src="https://img.shields.io/badge/gates-12-E8A23B?style=flat-square" alt="12 gates">
31
+ <img src="https://img.shields.io/badge/merged%20upstream-24%20PRs%20in%2021%20projects-E8A23B?style=flat-square" alt="24 merged pull requests across 21 projects">
32
+ <img src="https://img.shields.io/badge/closures%20turned%20into%20gates-5-E8A23B?style=flat-square" alt="5 closures turned into gates">
33
+ <img src="https://img.shields.io/badge/shellcheck-clean-111111?style=flat-square" alt="shellcheck clean">
34
+ </p>
35
+
36
+ <p align="center">
37
+ <strong>24 merged upstream &middot; 21 projects &middot; 5 closures, each one now a gate</strong><br>
38
+ <sub>Derived from GitHub on 7 September 2026: one account, every patch written by an agent, every filing governed by these rules as they were learned. Apple, Microsoft, Google, Apache and JetBrains are among the mergers. <a href="#numbers">The numbers</a> &middot; <a href="#how-it-works">the twelve gates</a>.</sub>
39
+ </p>
40
+
41
+ You know the maintainer. She runs the project on evenings and weekends, has for nine years, and opened GitHub this morning to four pull requests from accounts created last month. Each one rewrites a function she wrote in 2019. Each carries a paragraph of comments in code that had none, a six-hundred-word body, and a footer naming the tool that wrote it. One of them re-argues a decision she closed in April. She closes all four without reading them.
42
+
43
+ She is right to.
44
+
45
+ housebroken is the set of rules that get a pull request past her, written as scripts that refuse to file until every rule is met. The rules are not opinions. Each one was paid for with a closed pull request, and the closure is named next to the rule.
46
+
47
+ ## Before / after
48
+
49
+ Before: one branch, fourteen files, three unrelated fixes, a planning document and an agent config directory in the diff, a comment on every added line, a body that explains the tool, filed against a ruling already made in a closed issue, the project's own test target never run.
50
+
51
+ After:
52
+
53
+ ```
54
+ one finding, one pull request
55
+ 9 lines changed in the project's own style, no comment where the file has none
56
+ 1 test that fails on their main and passes with the patch
57
+ body under 120 words, in their template, no footer
58
+ prior art read and quoted before the branch existed
59
+ their CI target green on a fresh clone of their main
60
+ the CLA known before filing, the SECURITY.md route taken when it applies
61
+ ```
62
+
63
+ The second one gets merged. Sometimes in twelve minutes.
64
+
65
+ ## Numbers
66
+
67
+ Every rule here was learned on real repositories with real maintainers. Between late July and 7 September 2026, one account filed pull requests on projects it had never touched before, every patch written by an autonomous agent, every filing governed by these gates as they were learned. Derived from GitHub on 7 September 2026:
68
+
69
+ | | count |
70
+ |---|--:|
71
+ | pull requests filed | 102 |
72
+ | merged | 24, across 21 projects |
73
+ | open, waiting on a maintainer | 73 |
74
+ | closed without merging | 5 |
75
+
76
+ The merged patches include ones accepted by Apple, Microsoft, Google, Apache, JetBrains, and the URL parser that Node.js ships. The fastest merge came twelve minutes after filing. Several came the same day.
77
+
78
+ The five closures matter more than the merges. Each became a gate. Two of the five were the same class, three days apart, because the first lesson was written as prose and prose is followed when remembered. That is why the rules here are scripts that refuse, not a checklist that suggests.
79
+
80
+ ## How it works
81
+
82
+ <img align="right" width="230" src="assets/treatise.jpg" alt="An obsidian book with chrome corners titled The Housebroken Agent, a treatise on manners for machines calling at the homes of maintainers">
83
+
84
+ A pull request passes through the door in order. Each step is a script or a rule, and each names the closure that put it there.
85
+
86
+ **1. Read the house rules before knocking.**
87
+ `check-ai-policy.sh` reads the repository's contribution policy on its development branch and in the organization's `.github` repository, and prints the sentence, not a verdict. Some projects ask contributors not to use AI for pull request text; those are never filed. Some accept pull requests only for issues they have labelled; those get an issue with the fix offered.
88
+ `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.
89
+
90
+ **2. Is it already on the table?**
91
+ `prior-art.sh` lists every issue and pull request, open, closed and merged, that touches the file or symbol, with the type taken from the API field and the closing ruling quoted for every closed item. Two closures built it: a pull request that duplicated an open pull request because a search mixed issues and pull requests and nobody checked the type, and a pull request that argued against a ruling in a closed issue nobody had read.
92
+
93
+ **3. Is it a fix or an opinion?**
94
+ A change that rejects an input the project tolerated, or changes a default, is a breaking change on a stable major. It becomes an issue, never a pull request. Two closures, one class, two projects, before this was a rule.
95
+
96
+ **4. Prove it red first.**
97
+ On a fresh clone of the upstream default branch, the new test fails. With the patch, it passes. The proof lives in the pull request as the test, not in the body as a claim.
98
+
99
+ **5. Run their CI, not yours.**
100
+ The project's own test target, on the fresh clone, including the lint, format and mutation gates its workflow runs. Two pull requests went red on gates the author had never run, and the maintainer saw it before the author did.
101
+
102
+ **6. Match the house style.**
103
+ `comment-census.sh` counts added code lines against added comment lines and compares them with the file. No comment in code that has none. Body under 120 words, in the project's template if it has one. No tool footer, no session link, no co-author trailer. When a template asks whether AI was used, the answer is one truthful sentence. Three maintainers said the same thing about comment density before it became a script.
104
+
105
+ **7. Know the paperwork.**
106
+ Every organization gets a card before the first filing: CLA, DCO, signed-commit requirement, template. Some CLA bots post nothing on the pull request and put the instructions in a failed job's log; the card is where that is written down.
107
+
108
+ **8. Security goes through the side door.**
109
+ A memory-safety or remote-abort finding in a library goes by the project's SECURITY.md route, privately, and never becomes a public pull request until the project answers.
110
+
111
+ **9. File through the gate.**
112
+ `file-pr.sh` wraps the pull request creation and refuses when the prior-art printout for that repository is missing or older than a day, or when the body carries a footer, a trailer, or a typographic dash.
113
+
114
+ **10. Watch it land.**
115
+ `verify-filed-pr.sh` re-derives from GitHub that the head is the intended commit, the diff is exactly the intended files, and CI settled green. `pr-sweep.sh` lists every open pull request where the ball is in your court: a maintainer's comment unanswered, a review requesting changes, a red check, a conflict. Every maintainer comment gets a same-day answer. A ruling in a closed issue is never argued with. When the maintainer is right, concede and let them close it.
116
+
117
+ **11. Clean up.**
118
+ `fork-hygiene.sh` deletes the fork branch of every merged or closed pull request and lists forks with no pull request left, which are deleted when the work is over. No planning file, agent directory or build output ever enters a diff.
119
+
120
+ **12. Three per repository, one finding each.**
121
+ A repository gets at most three pull requests, each one finding, each meeting every rule above on its own.
122
+
123
+ ## Install
124
+
125
+ You need `bash`, `gh` (signed in) and `jq`. Then:
126
+
127
+ ```
128
+ uv tool install housebroken-cli
129
+ housebroken install-skill
130
+ ```
131
+
132
+ The first line puts the `housebroken` command on your path. The second puts the skill where Claude Code loads it, so the agent runs the door itself before it opens anything upstream. Until the package is on PyPI, install from the repository instead:
133
+
134
+ ```
135
+ uv tool install git+https://github.com/lenamonj/housebroken
136
+ ```
137
+
138
+ `housebroken help` prints the door in order. Every gate is also a plain bash script under `scripts/`, runnable on its own.
139
+
140
+ ## What is in the repository
141
+
142
+ | script | gate |
143
+ |---|---|
144
+ | `scripts/check-ai-policy.sh` | reads the AI-contribution policy where it actually lives |
145
+ | `scripts/distinct-outside.sh` | outside contributors merged in 120 days |
146
+ | `scripts/prior-art.sh` | issues and pull requests on the touched files, type stated, rulings quoted |
147
+ | `scripts/comment-census.sh` | added code versus added comments, per branch |
148
+ | `scripts/file-pr.sh` | the only way a pull request gets filed |
149
+ | `scripts/verify-filed-pr.sh` | the filed pull request is what was meant, and CI settled |
150
+ | `scripts/pr-sweep.sh` | every open pull request where the ball is in your court |
151
+ | `scripts/fork-hygiene.sh` | branches deleted after merge or close, orphan forks listed |
152
+
153
+ The scripts are bash and need `gh` and `jq`. They came out of one operator's workshop and some still carry that operator's assumptions; each script's header says what it assumes. Generalizing them is the current work.
154
+
155
+ ## Not in scope
156
+
157
+ 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.
158
+
159
+ ## FAQ
160
+
161
+ **Is this only for AI-written pull requests?**
162
+ No. Every rule here predates agents. Agents made it cheap to break all of them at once, on a hundred repositories, before breakfast.
163
+
164
+ **Does it make maintainers like agent pull requests?**
165
+ No. It makes the pull request indistinguishable from a careful human's, and answers truthfully when they ask.
166
+
167
+ **What if the maintainer closes it anyway?**
168
+ Then the maintainer is the judge and the ruling stands. Read why, write it down, and if it is a class of mistake, make it a gate.
169
+
170
+ **Why "housebroken"?**
171
+ Because the alternative is what maintainers are calling it.
172
+
173
+ ## License
174
+
175
+ MIT.
@@ -0,0 +1,17 @@
1
+ housebroken/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ housebroken/cli.py,sha256=ADd1Q07beO7H1MPurm4J8R1_AIQKsz9DYpCV152wKlo,2668
3
+ housebroken/scripts/check-ai-policy.sh,sha256=bwFVs1xCb7o6inWUQu6mu-tkNUGm_-5V-gfCmoBVKoA,4157
4
+ housebroken/scripts/comment-census.sh,sha256=9LvRfoFe4upc7BGyX3izgbKmTgOMR4_Fplx-ESfd2-Q,3208
5
+ housebroken/scripts/distinct-outside.sh,sha256=eJ6hjFUjkDAHBikUh5-4XAO8PIjJKKc-goSGCJ_QUe0,3580
6
+ housebroken/scripts/file-pr.sh,sha256=V6FtonHEiq7_jmnSC6BDl7WBhXcmgjod5kNuJzLxxF4,3968
7
+ housebroken/scripts/fork-hygiene.sh,sha256=0YJ-7Usbz0_kpbA25ZAm7U5OgtkJgjnRBv72IB6RnkA,3199
8
+ housebroken/scripts/pr-sweep.sh,sha256=wzN1sLkbTcqN2ksJtDoiyjcCEYPJbVSy41eLSU3P8VQ,3572
9
+ housebroken/scripts/prior-art.sh,sha256=tWNYKdN8-SA_WJcY-bk-HhI1GV_jMfA0ab4bui2uHFU,5989
10
+ housebroken/scripts/verify-filed-pr.sh,sha256=arXeZbdAavcbHBjUTZFDeOpPdQT2TaXN6b-RNxvZrxo,2944
11
+ housebroken/scripts/housebroken,sha256=uS8U_MjOtBFxU_iM5eclFnxJJTa0OlPQd0zL8iO1wpI,3576
12
+ housebroken/skill/SKILL.md,sha256=ITHQL-h46QQzs-xSlhBUCJVHb-vcIoyKP7wXL-d2j34,7512
13
+ housebroken_cli-0.1.0.dist-info/METADATA,sha256=9yNJVI_kUV36YtWzH0pb9erhbFOIaG-gPVew7NDZTg0,11414
14
+ housebroken_cli-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
15
+ housebroken_cli-0.1.0.dist-info/entry_points.txt,sha256=hdjaU5C4ymyzypEnIGFEEeZCxV5J0IcgW5Hf0q6vvXk,53
16
+ housebroken_cli-0.1.0.dist-info/licenses/LICENSE,sha256=fPHbuTJO3PJp8BlBoRP3es2egezKw2uOpYmbcHa4Rwo,1110
17
+ housebroken_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ housebroken = housebroken.cli:main