housebroken-cli 0.4.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.4.0 → housebroken_cli-0.5.0}/PKG-INFO +9 -3
  2. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/README.md +8 -2
  3. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/bin/housebroken +2 -0
  4. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/pyproject.toml +1 -1
  5. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/scripts/branch-check.sh +34 -5
  6. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/scripts/file-pr.sh +29 -2
  7. housebroken_cli-0.5.0/scripts/review.sh +263 -0
  8. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/skills/housebroken/SKILL.md +26 -10
  9. housebroken_cli-0.5.0/src/housebroken/__init__.py +1 -0
  10. housebroken_cli-0.4.0/src/housebroken/__init__.py +0 -1
  11. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/.gitignore +0 -0
  12. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/LICENSE +0 -0
  13. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/hatch_build.py +0 -0
  14. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/scripts/check-ai-policy.sh +0 -0
  15. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/scripts/claim-check.sh +0 -0
  16. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/scripts/comment-census.sh +0 -0
  17. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/scripts/diff-defaults.sh +0 -0
  18. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/scripts/distinct-outside.sh +0 -0
  19. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/scripts/fork-hygiene.sh +0 -0
  20. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/scripts/inbox.sh +0 -0
  21. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/scripts/notes.sh +0 -0
  22. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/scripts/pr-sweep.sh +0 -0
  23. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/scripts/prior-art.sh +0 -0
  24. {housebroken_cli-0.4.0 → housebroken_cli-0.5.0}/scripts/verify-filed-pr.sh +0 -0
  25. {housebroken_cli-0.4.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.4.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.
@@ -147,7 +152,8 @@ uv tool install git+https://github.com/lenamonj/housebroken
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
154
  | `scripts/claim-check.sh` | every falsifiable claim in a body or a reply, listed to re-derive |
150
- | `scripts/branch-check.sh` | the branch's mechanical facts: clean tree, file modes, artifacts, absolute claims |
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 |
151
157
  | `scripts/file-pr.sh` | the only way a pull request gets filed |
152
158
  | `scripts/verify-filed-pr.sh` | the filed pull request is what was meant, and CI settled |
153
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.
@@ -135,7 +140,8 @@ uv tool install git+https://github.com/lenamonj/housebroken
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
142
  | `scripts/claim-check.sh` | every falsifiable claim in a body or a reply, listed to re-derive |
138
- | `scripts/branch-check.sh` | the branch's mechanical facts: clean tree, file modes, artifacts, absolute claims |
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 |
139
145
  | `scripts/file-pr.sh` | the only way a pull request gets filed |
140
146
  | `scripts/verify-filed-pr.sh` | the filed pull request is what was meant, and CI settled |
141
147
  | `scripts/pr-sweep.sh` | every open pull request where the ball is in your court |
@@ -50,6 +50,7 @@ usage: housebroken <subcommand> [args...]
50
50
  6 census added code lines against added comment lines, per branch
51
51
  6 claims every falsifiable claim in outbound text, listed to re-derive
52
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
53
54
  9 file the only way a pull request gets filed
54
55
  10 verify the filed pull request is what was meant, and CI settled
55
56
  10 sweep every open pull request where the ball is in your court
@@ -93,6 +94,7 @@ case "$cmd" in
93
94
  census) script=comment-census.sh ;;
94
95
  claims) script=claim-check.sh ;;
95
96
  branch) script=branch-check.sh ;;
97
+ review) script=review.sh ;;
96
98
  file) script=file-pr.sh ;;
97
99
  verify) script=verify-filed-pr.sh ;;
98
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.4.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
@@ -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
@@ -140,15 +140,26 @@ report it in one line and do not proceed to the next step.
140
140
 
141
141
  9. **File through the gate.**
142
142
  `housebroken branch <clone>` first. It refuses a dirty working tree, a file
143
- whose mode disagrees with its own siblings, a committed file the repository
144
- ignores, and a branch that is not on top of its base, and it prints every
145
- absolute claim your added prose makes so you can falsify each one against
146
- 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
147
157
  `housebroken file owner/repo --title ... --body-file ...`, the only way a
148
158
  pull request is opened. Never run the pull request creation command
149
159
  directly. The gate refuses when the prior-art printout for that repository
150
- is missing or older than a day, or when the body carries a footer, a
151
- 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.
152
163
 
153
164
  10. **Watch it land.**
154
165
  `housebroken verify owner/repo PR` re-derives from GitHub that the head is
@@ -163,7 +174,10 @@ report it in one line and do not proceed to the next step.
163
174
  `housebroken inbox --ack`. Answer every maintainer comment the same day,
164
175
  in the user's voice, and run `housebroken claims` over the reply before
165
176
  it is posted: the only claim that has gone out wrong here was in a
166
- reply, not a body. Rework what is asked for as a second commit so the reviewer sees
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
167
181
  the change. When the maintainer is
168
182
  right, concede in one sentence and let them close it. Never nudge a
169
183
  silent maintainer without the user's explicit word. After answering a
@@ -213,8 +227,9 @@ report it in one line and do not proceed to the next step.
213
227
  ## What you say to the user
214
228
 
215
229
  Before filing: the repository, the finding in one sentence, the prior-art
216
- result in one sentence, the red-then-green result, the agreement the
217
- 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
218
233
  filing: the pull request URL and the verify result. At the end of the run:
219
234
  what went into the ledger, or that nothing did. On a refusal at any
220
235
  step: which step, and the one sentence that explains it.
@@ -225,4 +240,5 @@ Never open a pull request without the prior-art printout. Never argue with a
225
240
  ruling in a closed issue. Never file where the policy asks you not to. Never
226
241
  put a footer, a session link or a co-author trailer on anything that leaves
227
242
  the user's repositories. Never post a comment upstream that the user has not
228
- 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