housebroken-cli 0.2.1__tar.gz → 0.2.3__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.2.1 → housebroken_cli-0.2.3}/PKG-INFO +1 -1
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/bin/housebroken +2 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/pyproject.toml +1 -1
- housebroken_cli-0.2.3/scripts/inbox.sh +137 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/skills/housebroken/SKILL.md +31 -4
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/.gitignore +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/LICENSE +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/README.md +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/hatch_build.py +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/check-ai-policy.sh +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/comment-census.sh +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/diff-defaults.sh +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/distinct-outside.sh +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/file-pr.sh +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/fork-hygiene.sh +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/notes.sh +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/pr-sweep.sh +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/prior-art.sh +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/verify-filed-pr.sh +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/src/housebroken/__init__.py +0 -0
- {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/src/housebroken/cli.py +0 -0
|
@@ -51,6 +51,7 @@ usage: housebroken <subcommand> [args...]
|
|
|
51
51
|
9 file the only way a pull request gets filed
|
|
52
52
|
10 verify the filed pull request is what was meant, and CI settled
|
|
53
53
|
10 sweep every open pull request where the ball is in your court
|
|
54
|
+
10 inbox what GitHub has to tell you since you last looked, bodies included
|
|
54
55
|
10 notes what this repository's maintainers have asked for before
|
|
55
56
|
11 hygiene delete fork branches after merge or close, list orphan forks
|
|
56
57
|
|
|
@@ -91,6 +92,7 @@ case "$cmd" in
|
|
|
91
92
|
file) script=file-pr.sh ;;
|
|
92
93
|
verify) script=verify-filed-pr.sh ;;
|
|
93
94
|
sweep) script=pr-sweep.sh ;;
|
|
95
|
+
inbox) script=inbox.sh ;;
|
|
94
96
|
notes) script=notes.sh ;;
|
|
95
97
|
hygiene) script=fork-hygiene.sh ;;
|
|
96
98
|
*)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# inbox.sh - what GitHub has to tell you since you last looked, bodies included.
|
|
3
|
+
#
|
|
4
|
+
# A commenter list is not a read. This prints every notification on the
|
|
5
|
+
# threads you are subscribed to (everything you authored) newer than a cursor,
|
|
6
|
+
# expands each to the full text of its latest comment or review, states the
|
|
7
|
+
# thread's current state (open, draft, merged, closed), and then takes a census
|
|
8
|
+
# of every open pull request of yours for failing, blocked or conflicting
|
|
9
|
+
# checks, which the notification feed does not carry for upstream pulls.
|
|
10
|
+
# Written 2026-09-08 after a two-bug review on apache/commons-lang #1783 sat
|
|
11
|
+
# unread for forty minutes because a check printed who had commented instead
|
|
12
|
+
# of what they said, and after cisco/libsrtp #821 sat red for two days with
|
|
13
|
+
# nobody reading its checks.
|
|
14
|
+
#
|
|
15
|
+
# The cursor is a timestamp, not GitHub's unread flag: reading a thread on the
|
|
16
|
+
# phone marks it read, and that must not hide it from the desk.
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# bash inbox.sh # since the stored cursor (first run: 24 h ago)
|
|
20
|
+
# bash inbox.sh --since <ISO8601> # since an explicit time, cursor untouched
|
|
21
|
+
# bash inbox.sh --no-ci # skip the census of open pull requests
|
|
22
|
+
# bash inbox.sh --ack # store now as the cursor; run after acting
|
|
23
|
+
# bash inbox.sh --help
|
|
24
|
+
#
|
|
25
|
+
# Environment:
|
|
26
|
+
# HOUSEBROKEN_USER your GitHub login, default the authenticated user
|
|
27
|
+
# HOUSEBROKEN_HOME workshop root, default $HOME/.housebroken; the cursor
|
|
28
|
+
# lives at $HOUSEBROKEN_HOME/inbox-cursor and every run is
|
|
29
|
+
# appended to $HOUSEBROKEN_HOME/inbox-ledger.md
|
|
30
|
+
#
|
|
31
|
+
# Assumes: gh is installed and authenticated with the notifications scope, jq
|
|
32
|
+
# is on PATH. Read-only apart from the cursor and the ledger. Exits non-zero
|
|
33
|
+
# when gh fails.
|
|
34
|
+
set -u
|
|
35
|
+
|
|
36
|
+
usage() { sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//'; }
|
|
37
|
+
|
|
38
|
+
home="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
|
|
39
|
+
cursor_file="$home/inbox-cursor"
|
|
40
|
+
ledger="$home/inbox-ledger.md"
|
|
41
|
+
since=""
|
|
42
|
+
do_ci=1
|
|
43
|
+
ack=0
|
|
44
|
+
while [ $# -gt 0 ]; do
|
|
45
|
+
case "$1" in
|
|
46
|
+
--help|-h) usage; exit 0 ;;
|
|
47
|
+
--since) shift; since="${1:-}"; [ -n "$since" ] || { echo "inbox.sh: --since needs a time" >&2; exit 2; } ;;
|
|
48
|
+
--no-ci) do_ci=0 ;;
|
|
49
|
+
--ack) ack=1 ;;
|
|
50
|
+
*) echo "inbox.sh: unknown argument $1" >&2; usage >&2; exit 2 ;;
|
|
51
|
+
esac
|
|
52
|
+
shift
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
56
|
+
if [ "$ack" -eq 1 ]; then
|
|
57
|
+
mkdir -p "$home" && printf '%s\n' "$now" > "$cursor_file" && echo "inbox cursor is now $now" && exit 0
|
|
58
|
+
echo "inbox.sh: could not write $cursor_file" >&2; exit 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
user="${HOUSEBROKEN_USER:-$(gh api user --jq .login 2>/dev/null)}"
|
|
62
|
+
[ -n "$user" ] || { echo "inbox.sh: gh is not authenticated" >&2; exit 1; }
|
|
63
|
+
if [ -z "$since" ]; then
|
|
64
|
+
if [ -s "$cursor_file" ]; then since="$(head -1 "$cursor_file")"; else since="$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-24H +%Y-%m-%dT%H:%M:%SZ)"; fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
is_bot() { case "$1" in *"[bot]"|copilot-pull-request-reviewer|codecov-commenter|coveralls|CLAassistant|meta-cla*|vercel*|coderabbitai*|copy-pr-bot*|oracle-contributor-agreement*|dependabot*|github-actions*|sonarcloud*|netlify*) return 0 ;; *) return 1 ;; esac; }
|
|
68
|
+
|
|
69
|
+
echo "== inbox for $user since $since (now $now) =="
|
|
70
|
+
if ! feed="$(gh api "notifications?all=true&since=$since&per_page=100" --paginate 2>/dev/null)"; then
|
|
71
|
+
echo "inbox.sh: gh api notifications failed (is the notifications scope granted?)" >&2; exit 1
|
|
72
|
+
fi
|
|
73
|
+
threads="$(printf '%s' "$feed" | jq -c '.[] | select(.subject.type == "PullRequest" or .subject.type == "Issue") | {repo: .repository.full_name, type: .subject.type, num: (.subject.url | split("/") | last), title: .subject.title, reason: .reason, at: .updated_at, latest: (.subject.latest_comment_url // "")}' | sort -u)"
|
|
74
|
+
count=$(printf '%s\n' "$threads" | grep -c . || true)
|
|
75
|
+
human=0
|
|
76
|
+
echo "$count thread(s) with activity"
|
|
77
|
+
while IFS= read -r t; do
|
|
78
|
+
[ -n "$t" ] || continue
|
|
79
|
+
repo=$(jq -r .repo <<<"$t"); num=$(jq -r .num <<<"$t"); type=$(jq -r .type <<<"$t"); reason=$(jq -r .reason <<<"$t"); at=$(jq -r .at <<<"$t"); latest=$(jq -r .latest <<<"$t"); title=$(jq -r .title <<<"$t")
|
|
80
|
+
if [ "$type" = "PullRequest" ]; then
|
|
81
|
+
state=$(gh pr view "$num" --repo "$repo" --json state,isDraft,mergedBy --jq 'if .state == "MERGED" then "MERGED by " + (.mergedBy.login // "?") elif .isDraft then "OPEN, DRAFT" else .state end' 2>/dev/null || echo "?")
|
|
82
|
+
else
|
|
83
|
+
state=$(gh issue view "$num" --repo "$repo" --json state --jq .state 2>/dev/null || echo "?")
|
|
84
|
+
fi
|
|
85
|
+
echo
|
|
86
|
+
echo "--- $repo#$num [$reason, $at] $state"
|
|
87
|
+
echo " $title"
|
|
88
|
+
if [ -n "$latest" ]; then
|
|
89
|
+
if c="$(gh api "$latest" 2>/dev/null)"; then
|
|
90
|
+
who=$(jq -r '.user.login // "?"' <<<"$c"); when=$(jq -r '.created_at // .submitted_at // ""' <<<"$c"); where=$(jq -r 'if .path then " on " + .path + ":" + ((.line // .original_line // 0)|tostring) else "" end' <<<"$c"); rstate=$(jq -r '.state // ""' <<<"$c")
|
|
91
|
+
if [ "$who" = "$user" ]; then
|
|
92
|
+
echo " latest: yours, $when"
|
|
93
|
+
elif is_bot "$who"; then
|
|
94
|
+
echo " latest: $who (bot), $when$where"
|
|
95
|
+
else
|
|
96
|
+
human=$((human + 1))
|
|
97
|
+
echo " latest: $who, $when$where${rstate:+ [$rstate]}"
|
|
98
|
+
jq -r '.body // ""' <<<"$c" | sed 's/^/ | /'
|
|
99
|
+
fi
|
|
100
|
+
else
|
|
101
|
+
echo " latest comment could not be fetched: $latest"
|
|
102
|
+
fi
|
|
103
|
+
else
|
|
104
|
+
echo " (no comment attached: a review, a merge, a close, a mention or a review request; see the state above)"
|
|
105
|
+
if [ "$type" = "PullRequest" ]; then
|
|
106
|
+
gh api "repos/$repo/pulls/$num/reviews?per_page=100" --jq --arg since "$since" --arg me "$user" '.[] | select(.submitted_at > $since and .user.login != $me) | " review " + .state + " by " + .user.login + ", " + .submitted_at + (if (.body // "") != "" then "\n | " + (.body | gsub("\r\n"; "\n | ")) else "" end)' 2>/dev/null
|
|
107
|
+
fi
|
|
108
|
+
fi
|
|
109
|
+
done <<<"$threads"
|
|
110
|
+
|
|
111
|
+
if [ "$do_ci" -eq 1 ]; then
|
|
112
|
+
echo
|
|
113
|
+
echo "== census of open pull requests: failing, blocked, conflicting, changes requested, draft =="
|
|
114
|
+
prs="$(gh search prs --author "$user" --state open --limit 100 --json repository,number,isDraft --jq '.[] | "\(.repository.nameWithOwner) \(.number) \(.isDraft)"' 2>/dev/null)"
|
|
115
|
+
total=$(printf '%s\n' "$prs" | grep -c . || true)
|
|
116
|
+
flagged=0
|
|
117
|
+
while read -r repo num draft; do
|
|
118
|
+
[ -n "$repo" ] || continue
|
|
119
|
+
j="$(gh pr view "$num" --repo "$repo" --json mergeable,reviewDecision,statusCheckRollup 2>/dev/null)" || continue
|
|
120
|
+
why="$(jq -r --arg draft "$draft" '
|
|
121
|
+
[ ( [.statusCheckRollup[]? | (.conclusion // .state // "")] | if any(. == "FAILURE" or . == "ERROR") then "FAILING" elif any(. == "ACTION_REQUIRED") then "needs-run-approval" else empty end ),
|
|
122
|
+
( if .mergeable == "CONFLICTING" then "CONFLICTING" else empty end ),
|
|
123
|
+
( if .reviewDecision == "CHANGES_REQUESTED" then "changes-requested" else empty end ),
|
|
124
|
+
( if $draft == "true" then "draft" else empty end ) ] | join(" ")' <<<"$j")"
|
|
125
|
+
if [ -n "$why" ]; then
|
|
126
|
+
flagged=$((flagged + 1))
|
|
127
|
+
fails="$(jq -r '[.statusCheckRollup[]? | select((.conclusion // .state // "") == "FAILURE" or (.conclusion // .state // "") == "ERROR") | (.name // .context)] | unique | join(", ")' <<<"$j")"
|
|
128
|
+
echo "$repo#$num: $why${fails:+ [$fails]}"
|
|
129
|
+
fi
|
|
130
|
+
done <<<"$prs"
|
|
131
|
+
echo "$flagged of $total open pull requests flagged"
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
echo
|
|
135
|
+
echo "$human human item(s) since $since. Act on each, then: housebroken inbox --ack"
|
|
136
|
+
mkdir -p "$home" 2>/dev/null && { echo "## $now (since $since): $count thread(s), $human human"; } >> "$ledger"
|
|
137
|
+
exit 0
|
|
@@ -68,7 +68,11 @@ report it in one line and do not proceed to the next step.
|
|
|
68
68
|
On a fresh clone of the upstream default branch, not the tree you found
|
|
69
69
|
the bug in, write the test that fails. Run it and keep the failing output.
|
|
70
70
|
Apply the patch. Run it again. Both results go into the pull request as
|
|
71
|
-
the test itself; the body states the fact in one sentence.
|
|
71
|
+
the test itself; the body states the fact in one sentence. When the
|
|
72
|
+
patched function serves several public entry points or has several
|
|
73
|
+
branches, the test is a grid: every entry point through every branch,
|
|
74
|
+
plus the zero, identity and signed-boundary rows. A reviewer lists the
|
|
75
|
+
empty cells otherwise.
|
|
72
76
|
|
|
73
77
|
5. **Run their CI, not yours.**
|
|
74
78
|
Run the project's own test target on the fresh clone, and every gate its
|
|
@@ -116,13 +120,34 @@ report it in one line and do not proceed to the next step.
|
|
|
116
120
|
`housebroken verify owner/repo PR` re-derives from GitHub that the head is
|
|
117
121
|
the intended commit, the diff is exactly the intended files, and CI
|
|
118
122
|
settled. `housebroken sweep` lists every open pull request where the ball
|
|
119
|
-
is in your court.
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
is in your court. `housebroken inbox` is the first thing a session does
|
|
124
|
+
and the only way you learn a thread's state: it prints every event on
|
|
125
|
+
your threads since the last acknowledged cursor with the full body, the
|
|
126
|
+
thread's state, and a census of failing, blocked and conflicting checks
|
|
127
|
+
on every open pull request. A list of who commented is not a read, and a
|
|
128
|
+
conversion to draft is a review. Act on every human item, then run
|
|
129
|
+
`housebroken inbox --ack`. Answer every maintainer comment the same day,
|
|
130
|
+
in the user's voice. Rework what is asked for as a second commit so the reviewer sees
|
|
131
|
+
the change. When the maintainer is
|
|
122
132
|
right, concede in one sentence and let them close it. Never nudge a
|
|
123
133
|
silent maintainer without the user's explicit word. After answering a
|
|
124
134
|
review, record what the maintainer asked for with
|
|
125
135
|
`housebroken notes owner/repo add "..."`.
|
|
136
|
+
Write the reply the way one maintainer writes to another. Lead with the
|
|
137
|
+
fact or the action, never with a verdict on the reviewer: no "you are
|
|
138
|
+
right", "good catch", "great point", "thanks for the feedback", no
|
|
139
|
+
apology, no restating their question, no closing summary or offer of
|
|
140
|
+
further help. A concession is the corrected fact followed by what
|
|
141
|
+
changed: "Every loader checks its read, so a truncated file fails on
|
|
142
|
+
main; the description is corrected." A disagreement is the command and
|
|
143
|
+
its result, without adjectives. An answer to a question is the answer
|
|
144
|
+
first and the evidence second. One to three sentences, no bullet list
|
|
145
|
+
under a paragraph's worth of content, no bold, no exclamation mark, no
|
|
146
|
+
handle unless the thread has several people. Offer closure as a plain
|
|
147
|
+
statement ("Fine to close."), and stop when the content stops.
|
|
148
|
+
Never post two replies to one repository in the same minute: space them
|
|
149
|
+
at least ten minutes apart, the one that matters most first, so a
|
|
150
|
+
maintainer's inbox does not show three answers stamped the same second.
|
|
126
151
|
|
|
127
152
|
11. **Clean up.**
|
|
128
153
|
`housebroken hygiene` deletes the fork branch of every merged or closed
|
|
@@ -135,6 +160,8 @@ report it in one line and do not proceed to the next step.
|
|
|
135
160
|
finding, each passing every step above on its own. GitHub lets
|
|
136
161
|
maintainers cap open pull requests from non-collaborators and counts
|
|
137
162
|
agent-opened ones against that cap.
|
|
163
|
+
The same spacing applies to filing: pull requests and issues on one
|
|
164
|
+
repository go out at least ten minutes apart, the strongest first.
|
|
138
165
|
|
|
139
166
|
13. **Write it down before you leave.**
|
|
140
167
|
A run through the door ends with its ledger, not with the pull
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|