housebroken-cli 0.3.0__tar.gz → 0.5.0__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 (25) hide show
  1. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/PKG-INFO +10 -3
  2. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/README.md +9 -2
  3. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/bin/housebroken +4 -0
  4. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/pyproject.toml +1 -1
  5. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/scripts/branch-check.sh +34 -5
  6. housebroken_cli-0.5.0/scripts/claim-check.sh +67 -0
  7. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/scripts/file-pr.sh +29 -2
  8. housebroken_cli-0.5.0/scripts/review.sh +263 -0
  9. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/skills/housebroken/SKILL.md +33 -11
  10. housebroken_cli-0.5.0/src/housebroken/__init__.py +1 -0
  11. housebroken_cli-0.3.0/src/housebroken/__init__.py +0 -1
  12. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/.gitignore +0 -0
  13. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/LICENSE +0 -0
  14. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/hatch_build.py +0 -0
  15. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/scripts/check-ai-policy.sh +0 -0
  16. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/scripts/comment-census.sh +0 -0
  17. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/scripts/diff-defaults.sh +0 -0
  18. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/scripts/distinct-outside.sh +0 -0
  19. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/scripts/fork-hygiene.sh +0 -0
  20. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/scripts/inbox.sh +0 -0
  21. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/scripts/notes.sh +0 -0
  22. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/scripts/pr-sweep.sh +0 -0
  23. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/scripts/prior-art.sh +0 -0
  24. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/scripts/verify-filed-pr.sh +0 -0
  25. {housebroken_cli-0.3.0 → housebroken_cli-0.5.0}/src/housebroken/cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: housebroken-cli
3
- Version: 0.3.0
3
+ Version: 0.5.0
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
@@ -57,6 +57,7 @@ one finding, one pull request
57
57
  body under 120 words, in their template, no footer
58
58
  prior art read and quoted before the branch existed
59
59
  their CI target green on a fresh clone of their main
60
+ attacked by a second model before it left, evidence behind every finding
60
61
  the CLA known before filing, the SECURITY.md route taken when it applies
61
62
  ```
62
63
 
@@ -109,7 +110,11 @@ Every organization gets a card before the first filing: CLA, DCO, signed-commit
109
110
  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
 
111
112
  **9. File through the gate.**
112
- `branch-check.sh` reads the branch before anything leaves the machine: it refuses a dirty working tree, a file whose mode disagrees with its own siblings, a committed file the repository's own `.gitignore` excludes, a working artifact, and a branch that is not on top of its base, and it prints every absolute claim the added prose makes so each can be falsified against the code. Three of those came from one review round on a single pull request. `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
+ `branch-check.sh` reads the branch before anything leaves the machine: it refuses a dirty working tree, a file whose mode disagrees with its own siblings, a file the branch adds that the repository's own `.gitignore` excludes, a working artifact, a commit carrying a tool trailer or a session link, a sign-off that names a username, and a branch that is not on top of its base, and it prints every absolute claim the added prose makes so each can be falsified against the code. Three of those came from one review round on a single pull request.
114
+
115
+ Then a different model attacks the change. `review.sh brief` names the reviewer, never the model that wrote the change: one tier down, Fable to Opus and Opus to Sonnet, and never below Sonnet. The reviewer gets a fresh context, hard read-only limits and an attack list that starts with every claim the prose makes, and every finding it reports carries evidence it produced itself. Its report records the head and the text it read, so a fix it asks for is a new head and gets a new review. The defects reviews like this caught before anything was sent are in [the ledger](https://github.com/lenamonj/housebroken/blob/main/docs/lessons.md).
116
+
117
+ `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, when the body carries a footer, a trailer, or a typographic dash, or when there is no POST AS IS review of the exact head and body being filed.
113
118
 
114
119
  **10. Watch it land.**
115
120
  `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.
@@ -146,7 +151,9 @@ uv tool install git+https://github.com/lenamonj/housebroken
146
151
  | `scripts/prior-art.sh` | issues and pull requests on the touched files, type stated, rulings quoted |
147
152
  | `scripts/comment-census.sh` | added code versus added comments, per branch |
148
153
  | `scripts/diff-defaults.sh` | default arguments and one-line wrappers added to keep call sites untouched |
149
- | `scripts/branch-check.sh` | the branch's mechanical facts: clean tree, file modes, artifacts, absolute claims |
154
+ | `scripts/claim-check.sh` | every falsifiable claim in a body or a reply, listed to re-derive |
155
+ | `scripts/branch-check.sh` | the branch's mechanical facts: clean tree, file modes, artifacts, trailers, sign-offs, absolute claims |
156
+ | `scripts/review.sh` | the adversarial review: a different model attacks the change, and the filing is held to exactly what it read |
150
157
  | `scripts/file-pr.sh` | the only way a pull request gets filed |
151
158
  | `scripts/verify-filed-pr.sh` | the filed pull request is what was meant, and CI settled |
152
159
  | `scripts/pr-sweep.sh` | every open pull request where the ball is in your court |
@@ -45,6 +45,7 @@ one finding, one pull request
45
45
  body under 120 words, in their template, no footer
46
46
  prior art read and quoted before the branch existed
47
47
  their CI target green on a fresh clone of their main
48
+ attacked by a second model before it left, evidence behind every finding
48
49
  the CLA known before filing, the SECURITY.md route taken when it applies
49
50
  ```
50
51
 
@@ -97,7 +98,11 @@ Every organization gets a card before the first filing: CLA, DCO, signed-commit
97
98
  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.
98
99
 
99
100
  **9. File through the gate.**
100
- `branch-check.sh` reads the branch before anything leaves the machine: it refuses a dirty working tree, a file whose mode disagrees with its own siblings, a committed file the repository's own `.gitignore` excludes, a working artifact, and a branch that is not on top of its base, and it prints every absolute claim the added prose makes so each can be falsified against the code. Three of those came from one review round on a single pull request. `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.
101
+ `branch-check.sh` reads the branch before anything leaves the machine: it refuses a dirty working tree, a file whose mode disagrees with its own siblings, a file the branch adds that the repository's own `.gitignore` excludes, a working artifact, a commit carrying a tool trailer or a session link, a sign-off that names a username, and a branch that is not on top of its base, and it prints every absolute claim the added prose makes so each can be falsified against the code. Three of those came from one review round on a single pull request.
102
+
103
+ Then a different model attacks the change. `review.sh brief` names the reviewer, never the model that wrote the change: one tier down, Fable to Opus and Opus to Sonnet, and never below Sonnet. The reviewer gets a fresh context, hard read-only limits and an attack list that starts with every claim the prose makes, and every finding it reports carries evidence it produced itself. Its report records the head and the text it read, so a fix it asks for is a new head and gets a new review. The defects reviews like this caught before anything was sent are in [the ledger](docs/lessons.md).
104
+
105
+ `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, when the body carries a footer, a trailer, or a typographic dash, or when there is no POST AS IS review of the exact head and body being filed.
101
106
 
102
107
  **10. Watch it land.**
103
108
  `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.
@@ -134,7 +139,9 @@ uv tool install git+https://github.com/lenamonj/housebroken
134
139
  | `scripts/prior-art.sh` | issues and pull requests on the touched files, type stated, rulings quoted |
135
140
  | `scripts/comment-census.sh` | added code versus added comments, per branch |
136
141
  | `scripts/diff-defaults.sh` | default arguments and one-line wrappers added to keep call sites untouched |
137
- | `scripts/branch-check.sh` | the branch's mechanical facts: clean tree, file modes, artifacts, absolute claims |
142
+ | `scripts/claim-check.sh` | every falsifiable claim in a body or a reply, listed to re-derive |
143
+ | `scripts/branch-check.sh` | the branch's mechanical facts: clean tree, file modes, artifacts, trailers, sign-offs, absolute claims |
144
+ | `scripts/review.sh` | the adversarial review: a different model attacks the change, and the filing is held to exactly what it read |
138
145
  | `scripts/file-pr.sh` | the only way a pull request gets filed |
139
146
  | `scripts/verify-filed-pr.sh` | the filed pull request is what was meant, and CI settled |
140
147
  | `scripts/pr-sweep.sh` | every open pull request where the ball is in your court |
@@ -48,7 +48,9 @@ usage: housebroken <subcommand> [args...]
48
48
  1 outside count outside contributors merged in the last 120 days
49
49
  2 prior-art issues and pull requests on the touched files, rulings quoted
50
50
  6 census added code lines against added comment lines, per branch
51
+ 6 claims every falsifiable claim in outbound text, listed to re-derive
51
52
  9 branch the mechanical facts about the branch: tree, modes, artifacts, claims
53
+ 9 review a different model attacks the change before it is filed
52
54
  9 file the only way a pull request gets filed
53
55
  10 verify the filed pull request is what was meant, and CI settled
54
56
  10 sweep every open pull request where the ball is in your court
@@ -90,7 +92,9 @@ case "$cmd" in
90
92
  outside) script=distinct-outside.sh ;;
91
93
  prior-art) script=prior-art.sh ;;
92
94
  census) script=comment-census.sh ;;
95
+ claims) script=claim-check.sh ;;
93
96
  branch) script=branch-check.sh ;;
97
+ review) script=review.sh ;;
94
98
  file) script=file-pr.sh ;;
95
99
  verify) script=verify-filed-pr.sh ;;
96
100
  sweep) script=pr-sweep.sh ;;
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "housebroken-cli"
7
- version = "0.3.0"
7
+ version = "0.5.0"
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"
@@ -103,9 +103,16 @@ while IFS= read -r f; do
103
103
  fi
104
104
  fi
105
105
 
106
- # The repository's own .gitignore is the authority on what is build output.
107
- if git check-ignore -q --no-index -- "$f" 2>/dev/null; then
108
- block "$f is committed but the repository's .gitignore excludes it"
106
+ # The repository's own .gitignore is the authority on what is build output,
107
+ # but only for a file this branch adds. Ignore rules never apply to a file the
108
+ # project already tracks, which is why check-ignore skips tracked files unless
109
+ # forced. NVIDIA/go-nvml ignores "dl" for a built binary, and that pattern also
110
+ # matches the directory pkg/dl, so forcing the check called two upstream source
111
+ # files build output.
112
+ if ! git cat-file -e "$base:$f" 2>/dev/null; then
113
+ if git check-ignore -q --no-index -- "$f" 2>/dev/null; then
114
+ block "$f is added by this branch and the repository's .gitignore excludes it"
115
+ fi
109
116
  fi
110
117
 
111
118
  case "$f" in
@@ -116,6 +123,28 @@ done <<EOF
116
123
  $files
117
124
  EOF
118
125
 
126
+ # A tool trailer is the one thing in a commit that cannot be argued as style.
127
+ # Thirteen open pull requests across ten organisations were found carrying
128
+ # "Co-Authored-By: Claude ..." and four of them a live claude.ai session URL,
129
+ # on NVIDIA, Microsoft, Apple, Uber, Tesla, Apache and IBM repositories. No
130
+ # project asked for it; a harness adds it by default and nobody read the commit
131
+ # before pushing. The body is checked for this at step 9; the commits were not.
132
+ trailers=$(git log --format='%H %s%n%b' "$base..HEAD" 2>/dev/null \
133
+ | grep -inE 'Co-Authored-By: *Claude|Claude-Session:|https://claude\.ai/code|Generated with .*Claude')
134
+ if [ -n "$trailers" ]; then
135
+ block "a commit carries a tool trailer or a session link, which no project asked for"
136
+ printf '%s\n' "$trailers" | sed 's/^/ /'
137
+ fi
138
+
139
+ # DCO wants a human being, and several projects say so in as many words.
140
+ if git log --format='%b' "$base..HEAD" 2>/dev/null | grep -qE '^Signed-off-by:'; then
141
+ usernames=$(git log --format='%b' "$base..HEAD" 2>/dev/null | grep -E '^Signed-off-by: [a-z0-9_-]+ <')
142
+ if [ -n "$usernames" ]; then
143
+ block "a Signed-off-by names a username; the DCO asks for a real name"
144
+ printf '%s\n' "$usernames" | sed 's/^/ /'
145
+ fi
146
+ fi
147
+
119
148
  # Absolutes in added prose are the claims a maintainer can falsify in one grep.
120
149
  claims=$(git diff -U0 "$base..HEAD" -- '*.md' '*.mdx' '*.rst' '*.txt' 2>/dev/null \
121
150
  | grep '^+' | grep -v '^+++' | sed 's/^+//' \
@@ -128,8 +157,8 @@ fi
128
157
  echo
129
158
  echo "commits:"
130
159
  git log --oneline "$base..HEAD" | sed 's/^/ /'
131
- echo "diff:"
132
- git diff --stat "$base..HEAD" | sed 's/^/ /'
160
+ echo "diff (against the merge base, which is what the maintainer sees):"
161
+ git diff --stat "$(git merge-base "$base" HEAD)..HEAD" | sed 's/^/ /'
133
162
 
134
163
  if [ "$blocked" -ne 0 ]; then
135
164
  echo
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+ # claim-check.sh - lists every claim in outbound text that a maintainer can
3
+ # falsify, so each one gets re-derived before it is sent.
4
+ #
5
+ # On microsoft/GSL #1272 a reply draft said "The five new static_asserts fail on
6
+ # main." A reviewer falsified it in one pass. The number had come from a red arm
7
+ # that grepped only for `static assertion failed`, so it counted five matches
8
+ # and never saw the twenty-one other errors beside them; and against the commit
9
+ # a maintainer would actually revert to, only four of the five fail. The
10
+ # sentence named a basis and was still wrong about it.
11
+ #
12
+ # So this does not try to judge a claim. Text checks cannot: the bad sentence
13
+ # named its revision and read perfectly. It lists them, and the rule in step 6
14
+ # is that every listed line is re-measured, at the moment of writing, against
15
+ # the exact revision the sentence names. The gap this closes is that until now
16
+ # every gate here read code and nothing read the prose, while the prose is what
17
+ # the maintainer reads first.
18
+ #
19
+ # Usage:
20
+ # bash claim-check.sh FILE
21
+ # bash claim-check.sh --help
22
+ #
23
+ # Exits 1 when it finds claims to re-derive, 0 when it finds none, 2 on a usage
24
+ # error. Exit 1 is not a refusal to send; it is a refusal to send unread.
25
+ set -u
26
+
27
+ usage() { awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0"; }
28
+
29
+ case "${1:-}" in
30
+ --help|-h) usage; exit 0 ;;
31
+ "") echo "REFUSED: name the file holding the text" >&2; exit 2 ;;
32
+ esac
33
+ file="$1"
34
+ [ -f "$file" ] || { echo "REFUSED: $file does not exist" >&2; exit 2; }
35
+
36
+ # Strip fenced code and quoted logs: a number inside pasted output is evidence,
37
+ # not a claim being made.
38
+ body=$(awk '/^```/ {fence = !fence; next} !fence && !/^ *[|>]/ {print NR": "$0}' "$file")
39
+
40
+ quantified=$(printf '%s
41
+ ' "$body" | grep -nEi '[0-9]+ *(of|/) *[0-9]+|\b([0-9]+|one|two|three|four|five|six|seven|eight|nine|ten|no) +([a-z_-]+ +){0,3}(tests?|assertions?|static_asserts?|checks?|errors?|failures?|jobs?|files?|warnings?|projects?|commits?|suites?)\b' | cut -d: -f2-)
42
+
43
+ absolute=$(printf '%s\n' "$body" | grep -nEi \
44
+ '\b(only|exactly|always|never|every|all|none|nothing|everything|guaranteed|any)\b' \
45
+ | cut -d: -f2-)
46
+
47
+ found=0
48
+ if [ -n "$quantified" ]; then
49
+ found=1
50
+ echo "COUNTED: re-derive each, against the revision the sentence names, now:"
51
+ printf '%s\n' "$quantified" | sed 's/^/ /'
52
+ fi
53
+ if [ -n "$absolute" ]; then
54
+ found=1
55
+ echo "ABSOLUTE: falsify each against the code, not against your memory of it:"
56
+ printf '%s\n' "$absolute" | sed 's/^/ /'
57
+ fi
58
+
59
+ if [ "$found" -eq 0 ]; then
60
+ echo "claim-check: no falsifiable claim found in $file"
61
+ exit 0
62
+ fi
63
+ echo
64
+ echo "claim-check: $file makes claims a maintainer can check. Re-measure each"
65
+ echo "one before sending. A sentence that names its revision can still be wrong"
66
+ echo "about it; that is the failure this exists for."
67
+ exit 1
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # file-pr.sh - the only way a PR gets filed: gh pr create behind two gates.
2
+ # file-pr.sh - the only way a PR gets filed: gh pr create behind three gates.
3
3
  #
4
4
  # Gate one is prior art. typer #1946 (closed 2026-08-31) duplicated an open PR
5
5
  # and swift-http-types #153 (closed 2026-09-04) reargued a closed issue, both
@@ -9,6 +9,10 @@
9
9
  # Gate two is the prose. Tool footers, session trailers and em or en dashes have
10
10
  # no place in somebody else's repository, so a body carrying one is refused.
11
11
  # Written 2026-09-07.
12
+ # Gate three is the adversarial review (review.sh). A different model attacked
13
+ # this exact head and this exact body file and said POST AS IS. An inline
14
+ # --body is refused, because a review cannot be bound to text that was never a
15
+ # file. Added 2026-09-12.
12
16
  #
13
17
  # Usage:
14
18
  # bash file-pr.sh owner/repo <gh pr create args...>
@@ -39,6 +43,10 @@ gates:
39
43
  exist and be under 24 hours old
40
44
  prose no tool footer, session trailer, co-author trailer, em dash or
41
45
  en dash in --body or --body-file
46
+ review run from the clone being filed; the report at
47
+ $HOUSEBROKEN_HOME/reviews/review-<owner>-<repo>-<head12>.md must
48
+ pass review.sh check for this head and the --body-file, and an
49
+ inline --body is refused
42
50
 
43
51
  environment:
44
52
  HOUSEBROKEN_HOME work directory, default $HOME/.housebroken
@@ -88,14 +96,18 @@ check_text() {
88
96
  }
89
97
 
90
98
  prev=""
99
+ bodyfile=""
100
+ inline_body=0
91
101
  for arg in "$@"; do
92
102
  case "$prev" in
93
103
  --body-file|-F)
94
104
  [ -f "$arg" ] || { echo "REFUSED: body file $arg does not exist" >&2; exit 2; }
95
105
  check_text "body file $arg" "$(cat "$arg")"
106
+ bodyfile="$arg"
96
107
  ;;
97
108
  --body|-b)
98
109
  check_text "--body" "$arg"
110
+ inline_body=1
99
111
  ;;
100
112
  esac
101
113
  case "$arg" in
@@ -103,12 +115,27 @@ for arg in "$@"; do
103
115
  f="${arg#--body-file=}"
104
116
  [ -f "$f" ] || { echo "REFUSED: body file $f does not exist" >&2; exit 2; }
105
117
  check_text "body file $f" "$(cat "$f")"
118
+ bodyfile="$f"
106
119
  ;;
107
- --body=*) check_text "--body" "${arg#--body=}" ;;
120
+ --body=*) check_text "--body" "${arg#--body=}"; inline_body=1 ;;
108
121
  esac
109
122
  prev="$arg"
110
123
  done
111
124
 
125
+ head=$(git rev-parse HEAD 2>/dev/null) || { echo "REFUSED: run file-pr.sh from inside the clone being filed" >&2; exit 2; }
126
+ if [ "$inline_body" -eq 1 ]; then
127
+ echo "REFUSED: pass the body as --body-file so the review can be bound to it" >&2
128
+ exit 2
129
+ fi
130
+ review="$home/reviews/review-${repo%%/*}-${repo##*/}-${head:0:12}.md"
131
+ if [ ! -f "$review" ]; then
132
+ echo "REFUSED: no adversarial review at $review - run review.sh brief and have a different model attack this head" >&2
133
+ exit 2
134
+ fi
135
+ review_args=(check "$review" --clone .)
136
+ [ -n "$bodyfile" ] && review_args+=(--text "$bodyfile")
137
+ bash "$(dirname "$0")/review.sh" "${review_args[@]}" >/dev/null || exit 2
138
+
112
139
  if [ "${FILE_PR_DRY:-0}" = "1" ]; then
113
140
  printf 'DRY RUN, not filed:\ngh pr create -R %s' "$repo"
114
141
  for arg in "$@"; do printf ' %q' "$arg"; done
@@ -0,0 +1,263 @@
1
+ #!/bin/bash
2
+ # review.sh - the adversarial review: a different model attacks the change
3
+ # before it leaves the machine, and the filing is held to exactly what it read.
4
+ #
5
+ # The gates before this one read code and count prose. None of them asks
6
+ # whether the idea of the patch was complete, or whether a sentence is true,
7
+ # and those are the defects that got furthest: a reply saying five new
8
+ # static_asserts fail on main when four did, a new test file committed 100755
9
+ # among 100644 siblings, a patch that removed behaviour the repository
10
+ # documents. A second session's review caught each of them (docs/lessons.md,
11
+ # 2026-09-10). Asked how a smaller model could catch what a larger one missed,
12
+ # the ledger answered that it was the seat, not the model: the author had
13
+ # decided the patch was good before writing the sentence. So the reviewer is
14
+ # never the model that wrote the change. It is one tier down by default, which
15
+ # costs less and still brings different blind spots, and never below Sonnet,
16
+ # because a review has to be able to check a standard citation or a CI matrix:
17
+ # Fable's work goes to Opus, Opus's to Sonnet, Sonnet's to Opus.
18
+ #
19
+ # A review that passed is not a proof. On 2026-09-12 a review passed a reply
20
+ # packet whose author then found, re-reading, that the two functions it cited
21
+ # as precedent compile only under MSVC. The author still re-reads every claim
22
+ # the reviewer did not re-derive.
23
+ #
24
+ # The report is bound to what it read. Its header records the head it attacked
25
+ # and the sha256 of every text, and check refuses when the branch or the text
26
+ # has moved since, or when the verdict is anything but POST AS IS: a fix the
27
+ # reviewer asked for is a new head, and a new head is reviewed again.
28
+ #
29
+ # Usage:
30
+ # bash review.sh reviewer MODEL
31
+ # bash review.sh brief --author MODEL --repo owner/repo [--reviewer MODEL]
32
+ # [--clone DIR [--base REF]] [--text FILE]...
33
+ # [--context FILE]...
34
+ # bash review.sh check REPORT [--clone DIR] [--text FILE]...
35
+ # bash review.sh --help
36
+ #
37
+ # reviewer prints the model that reviews work written by MODEL.
38
+ # brief prints on stdout the brief to hand a fresh session running the
39
+ # reviewer model, and on stderr who that is and where its report
40
+ # goes: $HOUSEBROKEN_HOME/reviews/review-<owner>-<repo>-<key>.md,
41
+ # keyed by the clone's head, or by the first text's sha256 when there
42
+ # is no clone.
43
+ # check exits 0 when REPORT may gate a filing or a reply, 1 when it may
44
+ # not, with the reason on stderr, and 2 on a usage error.
45
+ #
46
+ # Environment:
47
+ # HOUSEBROKEN_HOME work directory, default $HOME/.housebroken.
48
+ #
49
+ # Assumes git, and sha256sum or shasum.
50
+ set -u
51
+
52
+ usage() { awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0"; }
53
+ refuse() { echo "REFUSED: $1" >&2; exit 1; }
54
+ bad_usage() { echo "review.sh: $1" >&2; exit 2; }
55
+
56
+ home="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
57
+ here=$(cd "$(dirname "$0")" && pwd)
58
+
59
+ family() {
60
+ local m
61
+ m=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
62
+ case "$m" in
63
+ *fable*) echo fable ;;
64
+ *opus*) echo opus ;;
65
+ *sonnet*) echo sonnet ;;
66
+ *haiku*) echo haiku ;;
67
+ *) printf '%s\n' "$m" ;;
68
+ esac
69
+ }
70
+
71
+ # One tier down, never below sonnet. A family not listed has no default.
72
+ default_reviewer() {
73
+ case "$1" in
74
+ fable) echo opus ;;
75
+ opus) echo sonnet ;;
76
+ sonnet) echo opus ;;
77
+ haiku) echo sonnet ;;
78
+ esac
79
+ }
80
+
81
+ # Prints why REVIEWER may not review AUTHOR's work, or nothing when it may.
82
+ objection() {
83
+ if [ -z "$2" ]; then
84
+ echo "no reviewer model named"
85
+ elif [ "$2" = "$1" ]; then
86
+ echo "the reviewer is the model that wrote the change ($1)"
87
+ elif [ "$2" = haiku ]; then
88
+ echo "haiku is below the floor: a reviewer has to be able to check a standard citation or a CI matrix"
89
+ fi
90
+ }
91
+
92
+ sha256() {
93
+ if command -v sha256sum >/dev/null 2>&1; then
94
+ sha256sum "$1" | awk '{print $1}'
95
+ else
96
+ shasum -a 256 "$1" | awk '{print $1}'
97
+ fi
98
+ }
99
+
100
+ cmd_reviewer() {
101
+ [ "$#" -eq 1 ] || bad_usage "reviewer takes one model name"
102
+ local r
103
+ r=$(default_reviewer "$(family "$1")")
104
+ [ -n "$r" ] || bad_usage "no default reviewer for '$1'; name one with brief --reviewer: any model other than the author's, not below sonnet"
105
+ echo "$r"
106
+ }
107
+
108
+ cmd_brief() {
109
+ local author="" repo="" reviewer="" clone="" base="" head="" key="" mb="" t
110
+ local texts=() contexts=()
111
+ while [ "$#" -gt 0 ]; do
112
+ case "$1" in
113
+ --author) author="${2:-}"; shift ;;
114
+ --repo) repo="${2:-}"; shift ;;
115
+ --reviewer) reviewer="${2:-}"; shift ;;
116
+ --clone) clone="${2:-}"; shift ;;
117
+ --base) base="${2:-}"; shift ;;
118
+ --text) texts+=("${2:-}"); shift ;;
119
+ --context) contexts+=("${2:-}"); shift ;;
120
+ *) bad_usage "unknown argument: $1" ;;
121
+ esac
122
+ shift
123
+ done
124
+ [ -n "$author" ] || bad_usage "brief needs --author, the model that wrote the change"
125
+ case "$repo" in */*) : ;; *) bad_usage "brief needs --repo owner/repo" ;; esac
126
+ local ntext=${#texts[@]}
127
+ [ -n "$clone" ] || [ "$ntext" -gt 0 ] || bad_usage "brief needs --clone, --text, or both"
128
+ for t in ${texts[@]+"${texts[@]}"} ${contexts[@]+"${contexts[@]}"}; do
129
+ [ -f "$t" ] || bad_usage "$t does not exist"
130
+ done
131
+
132
+ local fa fr why
133
+ fa=$(family "$author")
134
+ if [ -n "$reviewer" ]; then fr=$(family "$reviewer"); else fr=$(default_reviewer "$fa"); fi
135
+ [ -n "$fr" ] || refuse "no default reviewer for '$author'; name one with --reviewer"
136
+ why=$(objection "$fa" "$fr")
137
+ [ -z "$why" ] || refuse "$why"
138
+ reviewer="${reviewer:-$fr}"
139
+
140
+ if [ -n "$clone" ]; then
141
+ head=$(git -C "$clone" rev-parse HEAD 2>/dev/null) || bad_usage "$clone is not a git clone"
142
+ if [ -z "$base" ]; then
143
+ for t in upstream/main upstream/master origin/main origin/master; do
144
+ if git -C "$clone" rev-parse --verify --quiet "$t" >/dev/null; then base="$t"; break; fi
145
+ done
146
+ fi
147
+ [ -n "$base" ] && mb=$(git -C "$clone" merge-base "$base" HEAD 2>/dev/null)
148
+ key="${head:0:12}"
149
+ else
150
+ key=$(sha256 "${texts[0]}" | cut -c1-12)
151
+ fi
152
+ local report="$home/reviews/review-${repo%%/*}-${repo##*/}-$key.md"
153
+ mkdir -p "$home/reviews"
154
+ echo "housebroken review: hand the brief below to a fresh session running $reviewer; its report goes to $report" >&2
155
+
156
+ printf 'ADVERSARIAL REVIEW BRIEF\n\n'
157
+ printf 'You are the reviewer. The change below was written by %s. You are %s, chosen because you did not write it. ' "$author" "$reviewer"
158
+ cat <<'EOF'
159
+ Find what is wrong with it before a maintainer does. Assume there is at least one defect and hunt for it. A finding needs evidence you produced yourself: a command and its output, a quoted line of code, a quoted document. An impression is not a finding.
160
+
161
+ Hard limits
162
+ - Nothing leaves this machine: no git push; no gh pr create, comment, review or merge; no gh api call with -X POST, PATCH, PUT or DELETE, or with -f or -F fields. Read-only gh calls are fine.
163
+ - Do not edit, commit in, or check out branches in the clone. To experiment, copy it and work in the copy. A copied build directory still points at the tree it was configured in, so reconfigure the build in the copy and make every arm's build log name the file that arm changed.
164
+ - Do not edit the text under review. Write only your report.
165
+
166
+ Under review
167
+ EOF
168
+ printf -- '- repository: %s\n' "$repo"
169
+ if [ -n "$clone" ]; then
170
+ printf -- '- clone: %s at %s, the branch exactly as it will be filed\n' "$clone" "$head"
171
+ if [ -n "$mb" ]; then
172
+ printf -- '- what the maintainer sees: git -C %s diff %s..%s\n' "$clone" "$mb" "$head"
173
+ git -C "$clone" diff --stat "$mb..HEAD" | sed 's/^/ /'
174
+ else
175
+ printf -- '- no upstream base found in the clone: ask which ref the branch is filed against\n'
176
+ fi
177
+ fi
178
+ for t in ${texts[@]+"${texts[@]}"}; do
179
+ printf -- '- text: %s (sha256 %s)\n' "$t" "$(sha256 "$t")"
180
+ done
181
+ for t in ${contexts[@]+"${contexts[@]}"}; do
182
+ printf -- '- context from the author, to attack rather than trust: %s\n' "$t"
183
+ done
184
+
185
+ printf '\nAttack at least\n'
186
+ printf '1. Every claim the text makes, re-derived now against the exact revision it names.\n'
187
+ for t in ${texts[@]+"${texts[@]}"}; do
188
+ printf ' %s:\n' "$t"
189
+ bash "$here/claim-check.sh" "$t" | awk 'NF == 0 {exit} {print " " $0}'
190
+ done
191
+ cat <<'EOF'
192
+ 2. The proof: the new test fails on the base and passes with the change. Swap file content with git show <base>:<path> rather than switching branches, compare hashes, and make sure the two arms differ. Ask what each step would print if it did nothing.
193
+ 3. The project's own CI, read from its workflow files: tests, lint, format, API and mutation checks, run on the change.
194
+ 4. Scope: every changed line is needed for what was asked, nothing is unrequested, no comment appears where the surrounding code has none, and documentation, README and CHANGELOG that describe the changed behaviour change with it.
195
+ 5. Prior art: nothing duplicates an open pull request or argues with a ruling in a closed issue.
196
+ 6. The text: it leads with the fact; no verdict on the reviewer, no thanks, apology or offer of help; a reply runs one to three sentences and a body stays under 120 words; no footer, trailer or typographic dash; every sentence is true as literally worded.
197
+ 7. Anything else that would embarrass the author in front of this maintainer.
198
+
199
+ Report
200
+ EOF
201
+ printf 'Write it to %s. It must start with exactly these lines, with the verdict filled in:\n\n' "$report"
202
+ printf 'review-of: %s\n' "$repo"
203
+ [ -n "$head" ] && printf 'head: %s\n' "$head"
204
+ for t in ${texts[@]+"${texts[@]}"}; do
205
+ printf 'text: sha256:%s %s\n' "$(sha256 "$t")" "$(basename "$t")"
206
+ done
207
+ printf 'author-model: %s\nreviewer-model: %s\n' "$author" "$reviewer"
208
+ printf 'verdict: POST AS IS | POST WITH CHANGES | DO NOT POST\n\n'
209
+ cat <<'EOF'
210
+ POST AS IS means nothing BLOCKING or SHOULD FIX remains. POST WITH CHANGES means the author fixes what you found and the fixed version comes back for another review. DO NOT POST means it should not go at all. After the header, number your findings, each with a severity (BLOCKING, SHOULD FIX or NIT), the evidence, and the exact fix, with replacement text in full for any wording. A report with no findings says what was checked and how.
211
+ EOF
212
+ }
213
+
214
+ cmd_check() {
215
+ local report="${1:-}" clone="" t
216
+ local texts=()
217
+ [ -n "$report" ] || bad_usage "check needs the report path"
218
+ shift
219
+ while [ "$#" -gt 0 ]; do
220
+ case "$1" in
221
+ --clone) clone="${2:-}"; shift ;;
222
+ --text) texts+=("${2:-}"); shift ;;
223
+ *) bad_usage "unknown argument: $1" ;;
224
+ esac
225
+ shift
226
+ done
227
+ [ -f "$report" ] || refuse "no review report at $report"
228
+
229
+ field() { sed -n "s/^$1: *//p" "$report" | head -1 | tr -d '\r' | sed 's/ *$//'; }
230
+ local author reviewer verdict head why now
231
+ author=$(field author-model)
232
+ reviewer=$(field reviewer-model)
233
+ verdict=$(field verdict | tr '[:lower:]' '[:upper:]')
234
+ head=$(field head)
235
+ [ -n "$author" ] || refuse "$report names no author-model"
236
+ why=$(objection "$(family "$author")" "$(family "$reviewer")")
237
+ [ -z "$why" ] || refuse "$why"
238
+ [ "$verdict" = "POST AS IS" ] ||
239
+ refuse "the verdict is '${verdict:-missing}' and only POST AS IS clears the gate: fix what the review found and review the new version"
240
+
241
+ if [ -n "$clone" ]; then
242
+ now=$(git -C "$clone" rev-parse HEAD 2>/dev/null) || bad_usage "$clone is not a git clone"
243
+ [ -n "$head" ] || refuse "$report records no head, so it cannot clear a branch"
244
+ if [ "${#head}" -lt 12 ] || [ "${now#"$head"}" = "$now" ]; then
245
+ refuse "the branch moved after the review: it attacked ${head:0:12}, the clone is at ${now:0:12}"
246
+ fi
247
+ fi
248
+ for t in ${texts[@]+"${texts[@]}"}; do
249
+ [ -f "$t" ] || bad_usage "$t does not exist"
250
+ grep -q "^text: sha256:$(sha256 "$t")" "$report" ||
251
+ refuse "$t is not the text that was reviewed: its sha256 is not in $report"
252
+ done
253
+ echo "review: POST AS IS by $reviewer on work by $author${head:+ at ${head:0:12}}"
254
+ }
255
+
256
+ case "${1:-}" in
257
+ --help|-h) usage; exit 0 ;;
258
+ reviewer) shift; cmd_reviewer "$@" ;;
259
+ brief) shift; cmd_brief "$@" ;;
260
+ check) shift; cmd_check "$@" ;;
261
+ "") usage >&2; exit 2 ;;
262
+ *) bad_usage "unknown subcommand: $1 (reviewer, brief or check)" ;;
263
+ esac
@@ -118,7 +118,11 @@ report it in one line and do not proceed to the next step.
118
118
  you reproduced on the fresh clone in this session, never a line carried
119
119
  from a journal. No tool footer, no session link, no
120
120
  co-author trailer, no typographic dashes. When a template asks whether AI
121
- was used, answer in one truthful sentence.
121
+ was used, answer in one truthful sentence. Run `housebroken claims` over
122
+ the body and re-measure every line it lists, now, against the exact
123
+ revision that sentence names. A claim that names its revision can still
124
+ be wrong about it, which is why the listing is the whole gate: the
125
+ re-derivation is yours to do.
122
126
 
123
127
  7. **Know the paperwork.**
124
128
  Before the first pull request to an organization, find its agreement: CLA,
@@ -136,15 +140,26 @@ report it in one line and do not proceed to the next step.
136
140
 
137
141
  9. **File through the gate.**
138
142
  `housebroken branch <clone>` first. It refuses a dirty working tree, a file
139
- whose mode disagrees with its own siblings, a committed file the repository
140
- ignores, and a branch that is not on top of its base, and it prints every
141
- absolute claim your added prose makes so you can falsify each one against
142
- the code. Then
143
+ whose mode disagrees with its own siblings, a file the branch adds that the
144
+ repository ignores, a commit carrying a tool trailer or a session link, a
145
+ sign-off that names a username, and a branch that is not on top of its
146
+ base, and it prints every absolute claim your added prose makes so you can
147
+ falsify each one against the code. Then the adversarial review:
148
+ `housebroken review brief --author <your model> --repo owner/repo --clone <clone> --text <body file>`
149
+ names the reviewer and prints its brief. The reviewer is never the model
150
+ that wrote the change: one tier down, Fable to Opus and Opus to Sonnet,
151
+ and never below Sonnet, so Sonnet's work goes to Opus. Hand the brief to a
152
+ fresh agent running that model; anything of yours it reads is for it to
153
+ attack, not to trust. It writes its report where the brief says, with its
154
+ own evidence behind every finding. Every fix it asks for is a new head,
155
+ and a new head is reviewed again. When it passes, re-read every claim it
156
+ did not re-derive itself: a passed review is not a proof. Then
143
157
  `housebroken file owner/repo --title ... --body-file ...`, the only way a
144
158
  pull request is opened. Never run the pull request creation command
145
159
  directly. The gate refuses when the prior-art printout for that repository
146
- is missing or older than a day, or when the body carries a footer, a
147
- trailer or a dash character. A refusal is a result; fix the cause.
160
+ is missing or older than a day, when the body carries a footer, a trailer
161
+ or a dash character, or when there is no POST AS IS review of the exact
162
+ head and body being filed. A refusal is a result; fix the cause.
148
163
 
149
164
  10. **Watch it land.**
150
165
  `housebroken verify owner/repo PR` re-derives from GitHub that the head is
@@ -157,7 +172,12 @@ report it in one line and do not proceed to the next step.
157
172
  on every open pull request. A list of who commented is not a read, and a
158
173
  conversion to draft is a review. Act on every human item, then run
159
174
  `housebroken inbox --ack`. Answer every maintainer comment the same day,
160
- in the user's voice. Rework what is asked for as a second commit so the reviewer sees
175
+ in the user's voice, and run `housebroken claims` over the reply before
176
+ it is posted: the only claim that has gone out wrong here was in a
177
+ reply, not a body. A reply that concedes a point, disagrees, or rides on
178
+ a code change goes through the adversarial review of step 9 first, with
179
+ the reply as its `--text`, and `housebroken review check <report> --text
180
+ <reply file>` must pass before it is posted. Rework what is asked for as a second commit so the reviewer sees
161
181
  the change. When the maintainer is
162
182
  right, concede in one sentence and let them close it. Never nudge a
163
183
  silent maintainer without the user's explicit word. After answering a
@@ -207,8 +227,9 @@ report it in one line and do not proceed to the next step.
207
227
  ## What you say to the user
208
228
 
209
229
  Before filing: the repository, the finding in one sentence, the prior-art
210
- result in one sentence, the red-then-green result, the agreement the
211
- organization wants, and the exact command you are about to run. After
230
+ result in one sentence, the red-then-green result, the review's verdict and
231
+ the model that gave it, the agreement the organization wants, and the exact
232
+ command you are about to run. After
212
233
  filing: the pull request URL and the verify result. At the end of the run:
213
234
  what went into the ledger, or that nothing did. On a refusal at any
214
235
  step: which step, and the one sentence that explains it.
@@ -219,4 +240,5 @@ Never open a pull request without the prior-art printout. Never argue with a
219
240
  ruling in a closed issue. Never file where the policy asks you not to. Never
220
241
  put a footer, a session link or a co-author trailer on anything that leaves
221
242
  the user's repositories. Never post a comment upstream that the user has not
222
- seen. Never claim a test ran that you did not run.
243
+ seen. Never file a change a different model has not attacked. Never claim a
244
+ test ran that you did not run.
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"
@@ -1 +0,0 @@
1
- __version__ = "0.2.1"
File without changes