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.
Files changed (21) hide show
  1. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/PKG-INFO +1 -1
  2. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/bin/housebroken +2 -0
  3. {housebroken_cli-0.2.1 → 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.1 → housebroken_cli-0.2.3}/skills/housebroken/SKILL.md +31 -4
  6. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/.gitignore +0 -0
  7. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/LICENSE +0 -0
  8. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/README.md +0 -0
  9. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/hatch_build.py +0 -0
  10. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/check-ai-policy.sh +0 -0
  11. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/comment-census.sh +0 -0
  12. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/diff-defaults.sh +0 -0
  13. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/distinct-outside.sh +0 -0
  14. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/file-pr.sh +0 -0
  15. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/fork-hygiene.sh +0 -0
  16. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/notes.sh +0 -0
  17. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/pr-sweep.sh +0 -0
  18. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/prior-art.sh +0 -0
  19. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/scripts/verify-filed-pr.sh +0 -0
  20. {housebroken_cli-0.2.1 → housebroken_cli-0.2.3}/src/housebroken/__init__.py +0 -0
  21. {housebroken_cli-0.2.1 → 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.1
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.1"
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
@@ -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. Answer every maintainer comment the same day, in the
120
- user's voice, after reading the whole thread. Rework what is asked for as
121
- a second commit so the reviewer sees the change. When the maintainer is
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