housebroken-cli 0.2.2__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.
Files changed (21) hide show
  1. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/PKG-INFO +1 -1
  2. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/bin/housebroken +2 -0
  3. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/pyproject.toml +1 -1
  4. housebroken_cli-0.2.3/scripts/inbox.sh +137 -0
  5. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/skills/housebroken/SKILL.md +8 -5
  6. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/.gitignore +0 -0
  7. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/LICENSE +0 -0
  8. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/README.md +0 -0
  9. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/hatch_build.py +0 -0
  10. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/scripts/check-ai-policy.sh +0 -0
  11. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/scripts/comment-census.sh +0 -0
  12. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/scripts/diff-defaults.sh +0 -0
  13. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/scripts/distinct-outside.sh +0 -0
  14. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/scripts/file-pr.sh +0 -0
  15. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/scripts/fork-hygiene.sh +0 -0
  16. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/scripts/notes.sh +0 -0
  17. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/scripts/pr-sweep.sh +0 -0
  18. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/scripts/prior-art.sh +0 -0
  19. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/scripts/verify-filed-pr.sh +0 -0
  20. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/src/housebroken/__init__.py +0 -0
  21. {housebroken_cli-0.2.2 → housebroken_cli-0.2.3}/src/housebroken/cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: housebroken-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
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
@@ -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
  *)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "housebroken-cli"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Agent-written pull requests that do not make a mess in someone else's house."
9
9
  dynamic = ["readme"]
10
10
  license = "MIT"
@@ -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
@@ -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. Before saying anything about a thread, read every
124
- comment body, review and timeline event newer than your last own action;
125
- a list of who commented is not a read, and a conversion to draft is a
126
- review. Answer every maintainer comment the same day, in the user's
127
- voice. Rework what is asked for as a second commit so the reviewer sees
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