housebroken-cli 0.2.2__tar.gz → 0.2.4__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.2 → housebroken_cli-0.2.4}/PKG-INFO +1 -1
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/bin/housebroken +2 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/pyproject.toml +1 -1
- housebroken_cli-0.2.4/scripts/inbox.sh +144 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/skills/housebroken/SKILL.md +8 -5
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/.gitignore +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/LICENSE +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/README.md +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/hatch_build.py +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/scripts/check-ai-policy.sh +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/scripts/comment-census.sh +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/scripts/diff-defaults.sh +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/scripts/distinct-outside.sh +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/scripts/file-pr.sh +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/scripts/fork-hygiene.sh +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/scripts/notes.sh +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/scripts/pr-sweep.sh +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/scripts/prior-art.sh +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/scripts/verify-filed-pr.sh +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/src/housebroken/__init__.py +0 -0
- {housebroken_cli-0.2.2 → housebroken_cli-0.2.4}/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,144 @@
|
|
|
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|copilot*|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
|
+
ic="$(gh api "repos/$repo/issues/$num/comments?since=$since&per_page=100" 2>/dev/null || echo '[]')"
|
|
89
|
+
if [ "$type" = "PullRequest" ]; then
|
|
90
|
+
rc="$(gh api "repos/$repo/pulls/$num/comments?since=$since&per_page=100" 2>/dev/null || echo '[]')"
|
|
91
|
+
rv="$(gh api "repos/$repo/pulls/$num/reviews?per_page=100" 2>/dev/null || echo '[]')"
|
|
92
|
+
else
|
|
93
|
+
rc='[]'; rv='[]'
|
|
94
|
+
fi
|
|
95
|
+
events="$(jq -nc --argjson ic "$ic" --argjson rc "$rc" --argjson rv "$rv" --arg since "$since" '
|
|
96
|
+
[ ($ic[] | {t: .created_at, who: (.user.login // "?"), kind: "comment", where: "", rst: "", body: (.body // "")}),
|
|
97
|
+
($rc[] | {t: .created_at, who: (.user.login // "?"), kind: "inline", where: (" on " + .path + ":" + ((.line // .original_line // 0) | tostring)), rst: "", body: (.body // "")}),
|
|
98
|
+
($rv[] | select((.submitted_at // "") > $since) | {t: .submitted_at, who: (.user.login // "?"), kind: "review", where: "", rst: (.state // ""), body: (.body // "")}) ]
|
|
99
|
+
| sort_by(.t) | .[]' 2>/dev/null)"
|
|
100
|
+
shown=0
|
|
101
|
+
while IFS= read -r e; do
|
|
102
|
+
[ -n "$e" ] || continue
|
|
103
|
+
who=$(jq -r .who <<<"$e"); when=$(jq -r .t <<<"$e"); kind=$(jq -r .kind <<<"$e"); where=$(jq -r .where <<<"$e"); rst=$(jq -r .rst <<<"$e")
|
|
104
|
+
shown=$((shown + 1))
|
|
105
|
+
if [ "$who" = "$user" ]; then
|
|
106
|
+
echo " $when yours: $kind$where"
|
|
107
|
+
elif is_bot "$who"; then
|
|
108
|
+
echo " $when $who (bot): $kind$where${rst:+ [$rst]}"
|
|
109
|
+
else
|
|
110
|
+
human=$((human + 1))
|
|
111
|
+
echo " $when $who: $kind$where${rst:+ [$rst]}"
|
|
112
|
+
jq -r .body <<<"$e" | tr -d '\r' | sed 's/^/ | /'
|
|
113
|
+
fi
|
|
114
|
+
done <<<"$events"
|
|
115
|
+
[ "$shown" -gt 0 ] || echo " (no comment or review since the cursor: a merge, a close, a push, a mention or a review request; see the state above)"
|
|
116
|
+
done <<<"$threads"
|
|
117
|
+
|
|
118
|
+
if [ "$do_ci" -eq 1 ]; then
|
|
119
|
+
echo
|
|
120
|
+
echo "== census of open pull requests: failing, blocked, conflicting, changes requested, draft =="
|
|
121
|
+
prs="$(gh search prs --author "$user" --state open --limit 100 --json repository,number,isDraft --jq '.[] | "\(.repository.nameWithOwner) \(.number) \(.isDraft)"' 2>/dev/null)"
|
|
122
|
+
total=$(printf '%s\n' "$prs" | grep -c . || true)
|
|
123
|
+
flagged=0
|
|
124
|
+
while read -r repo num draft; do
|
|
125
|
+
[ -n "$repo" ] || continue
|
|
126
|
+
j="$(gh pr view "$num" --repo "$repo" --json mergeable,reviewDecision,statusCheckRollup 2>/dev/null)" || continue
|
|
127
|
+
why="$(jq -r --arg draft "$draft" '
|
|
128
|
+
[ ( [.statusCheckRollup[]? | (.conclusion // .state // "")] | if any(. == "FAILURE" or . == "ERROR") then "FAILING" elif any(. == "ACTION_REQUIRED") then "needs-run-approval" else empty end ),
|
|
129
|
+
( if .mergeable == "CONFLICTING" then "CONFLICTING" else empty end ),
|
|
130
|
+
( if .reviewDecision == "CHANGES_REQUESTED" then "changes-requested" else empty end ),
|
|
131
|
+
( if $draft == "true" then "draft" else empty end ) ] | join(" ")' <<<"$j")"
|
|
132
|
+
if [ -n "$why" ]; then
|
|
133
|
+
flagged=$((flagged + 1))
|
|
134
|
+
fails="$(jq -r '[.statusCheckRollup[]? | select((.conclusion // .state // "") == "FAILURE" or (.conclusion // .state // "") == "ERROR") | (.name // .context)] | unique | join(", ")' <<<"$j")"
|
|
135
|
+
echo "$repo#$num: $why${fails:+ [$fails]}"
|
|
136
|
+
fi
|
|
137
|
+
done <<<"$prs"
|
|
138
|
+
echo "$flagged of $total open pull requests flagged"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
echo
|
|
142
|
+
echo "$human human item(s) since $since. Act on each, then: housebroken inbox --ack"
|
|
143
|
+
mkdir -p "$home" 2>/dev/null && { echo "## $now (since $since): $count thread(s), $human human"; } >> "$ledger"
|
|
144
|
+
exit 0
|
|
@@ -120,11 +120,14 @@ report it in one line and do not proceed to the next step.
|
|
|
120
120
|
`housebroken verify owner/repo PR` re-derives from GitHub that the head is
|
|
121
121
|
the intended commit, the diff is exactly the intended files, and CI
|
|
122
122
|
settled. `housebroken sweep` lists every open pull request where the ball
|
|
123
|
-
is in your court.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
128
131
|
the change. When the maintainer is
|
|
129
132
|
right, concede in one sentence and let them close it. Never nudge a
|
|
130
133
|
silent maintainer without the user's explicit word. After answering a
|
|
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
|