housebroken-cli 0.7.0__tar.gz → 0.8.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 (30) hide show
  1. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/PKG-INFO +6 -4
  2. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/README.md +5 -3
  3. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/bin/housebroken +4 -0
  4. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/pyproject.toml +1 -1
  5. housebroken_cli-0.8.0/scripts/declare.sh +90 -0
  6. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/file-pr.sh +31 -2
  7. housebroken_cli-0.8.0/scripts/proof.sh +204 -0
  8. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/review.sh +11 -1
  9. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/skills/housebroken/SKILL.md +20 -3
  10. housebroken_cli-0.8.0/src/housebroken/__init__.py +1 -0
  11. housebroken_cli-0.7.0/src/housebroken/__init__.py +0 -1
  12. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/.gitignore +0 -0
  13. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/LICENSE +0 -0
  14. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/hatch_build.py +0 -0
  15. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/branch-check.sh +0 -0
  16. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/check-ai-policy.sh +0 -0
  17. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/claim-check.sh +0 -0
  18. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/comment-census.sh +0 -0
  19. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/comment-check.sh +0 -0
  20. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/diff-defaults.sh +0 -0
  21. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/distinct-outside.sh +0 -0
  22. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/file-issue.sh +0 -0
  23. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/fork-hygiene.sh +0 -0
  24. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/inbox.sh +0 -0
  25. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/notes.sh +0 -0
  26. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/pr-sweep.sh +0 -0
  27. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/prior-art.sh +0 -0
  28. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/ref-check.sh +0 -0
  29. {housebroken_cli-0.7.0 → housebroken_cli-0.8.0}/scripts/verify-filed-pr.sh +0 -0
  30. {housebroken_cli-0.7.0 → housebroken_cli-0.8.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.7.0
3
+ Version: 0.8.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
@@ -61,7 +61,7 @@ A pull request passes through the door in order, and stops at the first rule it
61
61
  A change that rejects an input the project tolerated, changes a default, or narrows what the project publishes is the maintainer's call. It is sent with the call stated in the body: what breaks for existing users, and an offer to narrow or close it. A finding whose only evidence is a measurement is a hold, not a pull request.
62
62
 
63
63
  **4. Prove it red first, and prove the proof.**
64
- On a fresh clone of the upstream default branch, the new test fails; with the patch, it passes. Every arm of the experiment prints evidence that its precondition held, files are swapped by content and checked by hash, and two arms that agree exactly have not tested the variable.
64
+ On a fresh clone of the upstream default branch, the new test fails; with the patch, it passes. Every arm of the experiment prints evidence that its precondition held, files are swapped by content and checked by hash, and two arms that agree exactly have not tested the variable. `proof.sh` runs the experiment itself: the test on the head, then on the head with every changed non-test file swapped back to its content at the merge base, then the head restored and checked by hash, and it stamps the head only when the base arm fails and the head arm passes. A change with nothing to show red says so, with the reason, and the filing prints it.
65
65
 
66
66
  **5. Run their CI, not yours.**
67
67
  The project's own test target on the fresh clone, and every gate its pull request workflow runs: lint, format, mutation, API check. A gate not run is a red check the maintainer sees first.
@@ -77,8 +77,8 @@ A memory-safety, remote-abort, injection or path-escape finding goes by the proj
77
77
 
78
78
  **9. File through the gate.**
79
79
  `branch-check.sh` refuses a dirty working tree, a file whose mode disagrees with its siblings, a file the repository's own `.gitignore` excludes, a working artifact, a commit carrying a tool trailer, a session link or a tool identity, a sign-off naming a username, a branch not on top of its base, any comment added to code, a warning suppression that does not name what it hides or that no build log shows firing, reflection added to a .NET project that declares AOT or trim support without an AOT publish log of it, a credential or a path under your own home, a lockfile, generated file or binary nobody named, an agent's working file, and a file rewritten for its line endings; it notes a base the upstream has moved past, a user path that may be a fixture and every absolute claim in added prose, and stamps the head it passed.
80
- `review.sh` names the adversarial reviewer: a different model from the one that wrote the change, one tier down, never below Sonnet. The reviewer gets a fresh context, hard read-only limits, and before it reads the diff it writes its own fix from the bug alone, so it is not anchored on ours; the report names that fix as a diff and the gate refuses one that does not, a POST AS IS over an open BLOCKING or SHOULD FIX finding, and a POST AS IS over items the reviewer skipped. Its attack list starts with every claim the prose makes and includes the form of the code: the reviewer writes the changed block the way the strongest engineer in that language would, in the repository's own idioms, and a smaller or clearer version is a finding; every finding carries evidence it produced itself, and its report is bound to the head and the text it read. A fix is a new head and gets a new review.
81
- `file-pr.sh` is the only way a pull request is opened. It refuses without a prior-art printout under a day old that searched every file the branch changes, a policy printout under a day old that reads neither BAN nor UNREAD, a body that discloses when the policy asks and runs under 120 words of its own beside the project's template, a branch stamp for the exact head whose base the upstream has not since changed under the branch's files, a POST AS IS review of that head and that body, fewer than three open pull requests of yours on the repository, and ten minutes since the last.
80
+ `review.sh` names the adversarial reviewer: a different model from the one that wrote the change, one tier down, never below Sonnet. Before it runs, the author writes a declaration (`declare.sh`): what the change was not tested against, and every assumption with what breaks if it is wrong and how it was checked. "Nothing" is refused as an answer. The review reads the declaration as one of its texts and attacks each line. The reviewer gets a fresh context, hard read-only limits, and before it reads the diff it writes its own fix from the bug alone, so it is not anchored on ours; the report names that fix as a diff and the gate refuses one that does not, a POST AS IS over an open BLOCKING or SHOULD FIX finding, and a POST AS IS over items the reviewer skipped. Its attack list starts with every claim the prose makes and includes the form of the code: the reviewer writes the changed block the way the strongest engineer in that language would, in the repository's own idioms, and a smaller or clearer version is a finding; every finding carries evidence it produced itself, and its report is bound to the head and the text it read. A fix is a new head and gets a new review.
81
+ `file-pr.sh` is the only way a pull request is opened. It refuses without a prior-art printout under a day old that searched every file the branch changes, a policy printout under a day old that reads neither BAN nor UNREAD, a body that discloses when the policy asks and runs under 120 words of its own beside the project's template, a branch stamp for the exact head whose base the upstream has not since changed under the branch's files, a proof stamp for that head, a declaration that passes, a POST AS IS review of that head, that body and that declaration, fewer than three open pull requests of yours on the repository, and ten minutes since the last.
82
82
  `file-issue.sh` is the only way an issue is opened. It refuses without the same prior-art and policy printouts, refuses the same prose, and refuses a body that mentions a fix, a patch or a pull request without a GitHub link to the branch or commit, so work that exists is attributable to whoever did it.
83
83
 
84
84
  **10. Watch it land.**
@@ -121,6 +121,8 @@ uv tool install git+https://github.com/lenamonj/housebroken
121
121
  | `scripts/comment-census.sh` | added code versus added comments, per branch |
122
122
  | `scripts/diff-defaults.sh` | default arguments and one-line wrappers added to keep call sites untouched |
123
123
  | `scripts/claim-check.sh` | every falsifiable claim in a body or a reply, listed to re-derive |
124
+ | `scripts/proof.sh` | the red-first experiment, run and stamped: red on the base, green on the head |
125
+ | `scripts/declare.sh` | what was not tested and what was assumed, for the review to attack |
124
126
  | `scripts/ref-check.sh` | every issue, link, test, file and commit a body names, looked up |
125
127
  | `scripts/branch-check.sh` | the branch's mechanical facts: clean tree, live base, modes, artifacts, secrets, generated files, trailers, sign-offs, comments, suppressions, absolute claims |
126
128
  | `scripts/review.sh` | the adversarial review: a different model attacks the change, and the filing is held to exactly what it read |
@@ -49,7 +49,7 @@ A pull request passes through the door in order, and stops at the first rule it
49
49
  A change that rejects an input the project tolerated, changes a default, or narrows what the project publishes is the maintainer's call. It is sent with the call stated in the body: what breaks for existing users, and an offer to narrow or close it. A finding whose only evidence is a measurement is a hold, not a pull request.
50
50
 
51
51
  **4. Prove it red first, and prove the proof.**
52
- On a fresh clone of the upstream default branch, the new test fails; with the patch, it passes. Every arm of the experiment prints evidence that its precondition held, files are swapped by content and checked by hash, and two arms that agree exactly have not tested the variable.
52
+ On a fresh clone of the upstream default branch, the new test fails; with the patch, it passes. Every arm of the experiment prints evidence that its precondition held, files are swapped by content and checked by hash, and two arms that agree exactly have not tested the variable. `proof.sh` runs the experiment itself: the test on the head, then on the head with every changed non-test file swapped back to its content at the merge base, then the head restored and checked by hash, and it stamps the head only when the base arm fails and the head arm passes. A change with nothing to show red says so, with the reason, and the filing prints it.
53
53
 
54
54
  **5. Run their CI, not yours.**
55
55
  The project's own test target on the fresh clone, and every gate its pull request workflow runs: lint, format, mutation, API check. A gate not run is a red check the maintainer sees first.
@@ -65,8 +65,8 @@ A memory-safety, remote-abort, injection or path-escape finding goes by the proj
65
65
 
66
66
  **9. File through the gate.**
67
67
  `branch-check.sh` refuses a dirty working tree, a file whose mode disagrees with its siblings, a file the repository's own `.gitignore` excludes, a working artifact, a commit carrying a tool trailer, a session link or a tool identity, a sign-off naming a username, a branch not on top of its base, any comment added to code, a warning suppression that does not name what it hides or that no build log shows firing, reflection added to a .NET project that declares AOT or trim support without an AOT publish log of it, a credential or a path under your own home, a lockfile, generated file or binary nobody named, an agent's working file, and a file rewritten for its line endings; it notes a base the upstream has moved past, a user path that may be a fixture and every absolute claim in added prose, and stamps the head it passed.
68
- `review.sh` names the adversarial reviewer: a different model from the one that wrote the change, one tier down, never below Sonnet. The reviewer gets a fresh context, hard read-only limits, and before it reads the diff it writes its own fix from the bug alone, so it is not anchored on ours; the report names that fix as a diff and the gate refuses one that does not, a POST AS IS over an open BLOCKING or SHOULD FIX finding, and a POST AS IS over items the reviewer skipped. Its attack list starts with every claim the prose makes and includes the form of the code: the reviewer writes the changed block the way the strongest engineer in that language would, in the repository's own idioms, and a smaller or clearer version is a finding; every finding carries evidence it produced itself, and its report is bound to the head and the text it read. A fix is a new head and gets a new review.
69
- `file-pr.sh` is the only way a pull request is opened. It refuses without a prior-art printout under a day old that searched every file the branch changes, a policy printout under a day old that reads neither BAN nor UNREAD, a body that discloses when the policy asks and runs under 120 words of its own beside the project's template, a branch stamp for the exact head whose base the upstream has not since changed under the branch's files, a POST AS IS review of that head and that body, fewer than three open pull requests of yours on the repository, and ten minutes since the last.
68
+ `review.sh` names the adversarial reviewer: a different model from the one that wrote the change, one tier down, never below Sonnet. Before it runs, the author writes a declaration (`declare.sh`): what the change was not tested against, and every assumption with what breaks if it is wrong and how it was checked. "Nothing" is refused as an answer. The review reads the declaration as one of its texts and attacks each line. The reviewer gets a fresh context, hard read-only limits, and before it reads the diff it writes its own fix from the bug alone, so it is not anchored on ours; the report names that fix as a diff and the gate refuses one that does not, a POST AS IS over an open BLOCKING or SHOULD FIX finding, and a POST AS IS over items the reviewer skipped. Its attack list starts with every claim the prose makes and includes the form of the code: the reviewer writes the changed block the way the strongest engineer in that language would, in the repository's own idioms, and a smaller or clearer version is a finding; every finding carries evidence it produced itself, and its report is bound to the head and the text it read. A fix is a new head and gets a new review.
69
+ `file-pr.sh` is the only way a pull request is opened. It refuses without a prior-art printout under a day old that searched every file the branch changes, a policy printout under a day old that reads neither BAN nor UNREAD, a body that discloses when the policy asks and runs under 120 words of its own beside the project's template, a branch stamp for the exact head whose base the upstream has not since changed under the branch's files, a proof stamp for that head, a declaration that passes, a POST AS IS review of that head, that body and that declaration, fewer than three open pull requests of yours on the repository, and ten minutes since the last.
70
70
  `file-issue.sh` is the only way an issue is opened. It refuses without the same prior-art and policy printouts, refuses the same prose, and refuses a body that mentions a fix, a patch or a pull request without a GitHub link to the branch or commit, so work that exists is attributable to whoever did it.
71
71
 
72
72
  **10. Watch it land.**
@@ -109,6 +109,8 @@ uv tool install git+https://github.com/lenamonj/housebroken
109
109
  | `scripts/comment-census.sh` | added code versus added comments, per branch |
110
110
  | `scripts/diff-defaults.sh` | default arguments and one-line wrappers added to keep call sites untouched |
111
111
  | `scripts/claim-check.sh` | every falsifiable claim in a body or a reply, listed to re-derive |
112
+ | `scripts/proof.sh` | the red-first experiment, run and stamped: red on the base, green on the head |
113
+ | `scripts/declare.sh` | what was not tested and what was assumed, for the review to attack |
112
114
  | `scripts/ref-check.sh` | every issue, link, test, file and commit a body names, looked up |
113
115
  | `scripts/branch-check.sh` | the branch's mechanical facts: clean tree, live base, modes, artifacts, secrets, generated files, trailers, sign-offs, comments, suppressions, absolute claims |
114
116
  | `scripts/review.sh` | the adversarial review: a different model attacks the change, and the filing is held to exactly what it read |
@@ -47,11 +47,13 @@ usage: housebroken <subcommand> [args...]
47
47
  1 policy read the repository's AI-contribution policy where it lives
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
+ 4 proof run the test on the base and the head, and stamp red then green
50
51
  6 census added code lines against added comment lines, per branch
51
52
  6 comments refuse a comment added to code nobody asked for
52
53
  6 claims every falsifiable claim in outbound text, listed to re-derive
53
54
  6 refs every issue, link, test, file and commit the text names, looked up
54
55
  9 branch the mechanical facts about the branch: tree, modes, artifacts, claims
56
+ 9 declare what was not tested and what was assumed, for the review to attack
55
57
  9 review a different model attacks the change before it is filed
56
58
  9 file the only way a pull request gets filed
57
59
  9 issue the only way an issue gets filed
@@ -98,6 +100,8 @@ case "$cmd" in
98
100
  comments) script=comment-check.sh ;;
99
101
  claims) script=claim-check.sh ;;
100
102
  refs) script=ref-check.sh ;;
103
+ proof) script=proof.sh ;;
104
+ declare) script=declare.sh ;;
101
105
  branch) script=branch-check.sh ;;
102
106
  review) script=review.sh ;;
103
107
  file) script=file-pr.sh ;;
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "housebroken-cli"
7
- version = "0.7.0"
7
+ version = "0.8.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"
@@ -0,0 +1,90 @@
1
+ #!/bin/bash
2
+ # declare.sh - the author says what was not tested and what was assumed.
3
+ #
4
+ # A review attacks what it is shown. The gaps an author knows about and does
5
+ # not mention are the ones a maintainer finds: the mode that was never run,
6
+ # the comparer that was assumed to be the default, the caller that was
7
+ # assumed not to exist. The declaration names them before the review, so
8
+ # the reviewer can attack each one and a gap left out becomes a statement
9
+ # that was false, not an oversight.
10
+ #
11
+ # The first run writes a declaration for the clone's head and prints its
12
+ # path; fill it in. Later runs check it:
13
+ # not-tested: what this change was not run against, at least one line;
14
+ # "none" is refused, because something always was not
15
+ # assumptions: one line each, "- <assumption> | if wrong: <what breaks,
16
+ # for whom> | checked by: <the command and its result, or
17
+ # unchecked>"
18
+ #
19
+ # Pass the declaration to the review as a --text, so the reviewer attacks it
20
+ # and the review is bound to it. file-pr.sh requires a passing declaration
21
+ # for the head, and that the review read it.
22
+ #
23
+ # Usage:
24
+ # bash declare.sh [CLONE]
25
+ # bash declare.sh --help
26
+ #
27
+ # Writes or reads $HOUSEBROKEN_HOME/declarations/<head>.md. Exits 0 when the
28
+ # declaration passes, 1 when it was just written or does not pass, 2 on a
29
+ # usage error.
30
+ set -u
31
+
32
+ usage() { awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0"; }
33
+
34
+ case "${1:-}" in
35
+ --help|-h) usage; exit 0 ;;
36
+ -*) echo "REFUSED: unknown option $1" >&2; exit 2 ;;
37
+ esac
38
+ clone="${1:-.}"
39
+ head=$(git -C "$clone" rev-parse HEAD 2>/dev/null) || { echo "REFUSED: $clone is not a git clone" >&2; exit 2; }
40
+ home="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
41
+ decl="$home/declarations/$head.md"
42
+
43
+ if [ ! -f "$decl" ]; then
44
+ mkdir -p "$home/declarations"
45
+ cat > "$decl" <<EOF
46
+ head: $head
47
+
48
+ not-tested:
49
+ -
50
+
51
+ assumptions:
52
+ - <assumption> | if wrong: <what breaks, for whom> | checked by: <command and result, or unchecked>
53
+ EOF
54
+ echo "declare: fill in $decl, then run this again"
55
+ exit 1
56
+ fi
57
+
58
+ bad=""
59
+ tr -d '\r' < "$decl" | grep -q "^head: $head\$" || bad="$bad
60
+ the head line is not $head"
61
+ section() { tr -d '\r' < "$decl" | awk -v s="$1:" '$0 == s {on = 1; next} /^[a-z-]+:/ {on = 0} on && /^- */ {sub(/^- */, ""); print}' | sed 's/[[:space:]]*$//' | grep -v '^$'; }
62
+ nt=$(section not-tested)
63
+ if [ -z "$nt" ]; then
64
+ bad="$bad
65
+ not-tested: names nothing; one line per item, each starting with '- '"
66
+ elif printf '%s\n' "$nt" | grep -qixE 'none\.?|n/a|nothing\.?'; then
67
+ bad="$bad
68
+ not-tested: says none; something always was not tested"
69
+ fi
70
+ as=$(section assumptions)
71
+ [ -n "$as" ] || bad="$bad
72
+ assumptions: lists none"
73
+ while IFS= read -r a; do
74
+ [ -n "$a" ] || continue
75
+ case "$a" in
76
+ *"<assumption>"*|*"<what breaks"*|*"<command and result"*) bad="$bad
77
+ an assumption is still the template line" ;;
78
+ *"| if wrong: "*"| checked by: "*) ;;
79
+ *) bad="$bad
80
+ an assumption lacks '| if wrong: ... | checked by: ...': $a" ;;
81
+ esac
82
+ done <<EOF
83
+ $as
84
+ EOF
85
+
86
+ if [ -n "$bad" ]; then
87
+ echo "REFUSED: $decl does not pass:$bad"
88
+ exit 1
89
+ fi
90
+ echo "declare: $decl passes ($(printf '%s\n' "$nt" | grep -c .) not tested, $(printf '%s\n' "$as" | grep -c .) assumptions)"
@@ -263,12 +263,41 @@ if [ "$since" -lt 600 ]; then
263
263
  echo "REFUSED: your last pull request on $repo was filed ${since}s ago - wait until ten minutes have passed" >&2
264
264
  exit 2
265
265
  fi
266
+ # The experiment the body describes was run by proof.sh, not recounted, and
267
+ # against the base branch-check measured; the author's declaration of what
268
+ # was not tested passes and names a proof that was waived; and the review
269
+ # read the declaration. Proof and declaration come before the review because
270
+ # the review is bound to the declaration.
271
+ proof="$home/proofs/$head"
272
+ if [ ! -f "$proof" ]; then
273
+ echo "REFUSED: no proof for head ${head:0:12} - run proof.sh --test 'COMMAND', or --none 'REASON' when nothing can fail first" >&2
274
+ exit 2
275
+ fi
276
+ decl="$home/declarations/$head.md"
277
+ if ! declared=$(bash "$(dirname "$0")/declare.sh" . 2>&1); then
278
+ echo "$declared" >&2
279
+ exit 2
280
+ fi
281
+ if grep -q '^proof: none' "$proof"; then
282
+ reason=$(sed -n 's/^reason: //p' "$proof" | tr -d '\r')
283
+ if ! grep -qF -- "$reason" "$decl"; then
284
+ echo "REFUSED: the proof for ${head:0:12} is none ($reason) and the declaration does not say so - add it under not-tested, so the reviewer attacks it" >&2
285
+ exit 2
286
+ fi
287
+ echo "note: no red-green proof for ${head:0:12}: $reason" >&2
288
+ else
289
+ proved=$(sed -n 's/^base: //p' "$proof" | tr -d '\r')
290
+ if [ "$proved" != "$stamped" ]; then
291
+ echo "REFUSED: the proof for ${head:0:12} ran against base ${proved:0:12}, branch-check measured ${stamped:0:12} - rerun proof.sh --base $checked" >&2
292
+ exit 2
293
+ fi
294
+ fi
266
295
  review="$home/reviews/review-${repo%%/*}-${repo##*/}-${head:0:12}.md"
267
296
  if [ ! -f "$review" ]; then
268
- echo "REFUSED: no adversarial review at $review - run review.sh brief and have a different model attack this head" >&2
297
+ echo "REFUSED: no adversarial review at $review - run review.sh brief with --text $decl and have a different model attack this head" >&2
269
298
  exit 2
270
299
  fi
271
- bash "$(dirname "$0")/review.sh" check "$review" --clone . --text "$bodyfile" >/dev/null || exit 2
300
+ bash "$(dirname "$0")/review.sh" check "$review" --clone . --text "$bodyfile" --text "$decl" >/dev/null || exit 2
272
301
 
273
302
  if [ "${FILE_PR_DRY:-0}" = "1" ]; then
274
303
  printf 'DRY RUN, not filed:\ngh pr create -R %s' "$repo"
@@ -0,0 +1,204 @@
1
+ #!/bin/bash
2
+ # proof.sh - run the red-first experiment and stamp what it showed.
3
+ #
4
+ # "The new test fails on main and passes with the patch" is the claim every
5
+ # pull request rests on, and the ledger has five runs that reported it while
6
+ # seeing nothing: a worktree whose checkout did nothing, a refused checkout
7
+ # that left the patch in place, a soft reset, a filtered count, a test that
8
+ # never compiled. This runs the experiment itself. On the clone's head it
9
+ # runs the test command; then it swaps every changed file that is not a test
10
+ # back to its content at the merge base, by content and checked by hash, so
11
+ # the tests stay and the fix goes; runs the command again; and restores the
12
+ # head from the commit, checked by hash. The stamp is written only when the
13
+ # base arm fails, the head arm passes, and the two arms ran different code.
14
+ #
15
+ # A change with nothing to prove red, a documentation fix or a refactor
16
+ # whose behaviour the existing suite already pins, says so with --none and a
17
+ # reason; the stamp records the reason and the filing prints it.
18
+ #
19
+ # Usage:
20
+ # bash proof.sh [CLONE] --test 'COMMAND' [--base REF] [--keep PATH]...
21
+ # [--fix PATH]... [--base-file PATH=FILE]...
22
+ # bash proof.sh [CLONE] --none 'REASON'
23
+ # bash proof.sh --help
24
+ #
25
+ # CLONE defaults to the current directory. REF defaults to the base the
26
+ # branch-check stamp for the head names, then to the first of upstream/main,
27
+ # upstream/master, origin/main and origin/master that resolves. COMMAND runs
28
+ # with bash -c from the clone's top; name the new test in it so the run is
29
+ # that test, not the whole suite. A changed file is taken for a test when its
30
+ # path says test or spec; --keep PATH keeps another changed file at the head
31
+ # in the base arm, such as a fixture the test reads; --fix PATH takes a file
32
+ # away although its path says test, such as src/testing/util.go. When one
33
+ # file carries both the test and the fix, a Rust module with its #[cfg(test)]
34
+ # block, --base-file PATH=FILE puts FILE's content, the base plus the test,
35
+ # at PATH in the base arm.
36
+ #
37
+ # Writes $HOUSEBROKEN_HOME/proofs/<head>, which file-pr.sh requires.
38
+ # Exits 0 when the stamp is written, 1 when the experiment does not show the
39
+ # fix, 2 on a usage error or when the clone cannot be swapped safely.
40
+ set -u
41
+
42
+ usage() { awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0"; }
43
+
44
+ home="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
45
+ clone="."; cmd=""; none=""; base=""
46
+ keeps=(); forces=(); basefiles=()
47
+ while [ "$#" -gt 0 ]; do
48
+ case "$1" in
49
+ --help|-h) usage; exit 0 ;;
50
+ --test) cmd="${2:-}"; [ -n "$cmd" ] || { echo "REFUSED: --test needs the command" >&2; exit 2; }; shift ;;
51
+ --none) none="${2:-}"; [ -n "$none" ] || { echo "REFUSED: --none needs the reason" >&2; exit 2; }; shift ;;
52
+ --base) base="${2:-}"; [ -n "$base" ] || { echo "REFUSED: --base needs a ref" >&2; exit 2; }; shift ;;
53
+ --keep) [ -n "${2:-}" ] || { echo "REFUSED: --keep needs a path" >&2; exit 2; }; keeps+=("$2"); shift ;;
54
+ --fix) [ -n "${2:-}" ] || { echo "REFUSED: --fix needs a path" >&2; exit 2; }; forces+=("$2"); shift ;;
55
+ --base-file)
56
+ case "${2:-}" in ?*=?*) ;; *) echo "REFUSED: --base-file needs PATH=FILE" >&2; exit 2 ;; esac
57
+ [ -f "${2#*=}" ] || { echo "REFUSED: --base-file: ${2#*=} does not exist" >&2; exit 2; }
58
+ basefiles+=("$2"); shift ;;
59
+ -*) echo "REFUSED: unknown option $1" >&2; exit 2 ;;
60
+ *) clone="$1" ;;
61
+ esac
62
+ shift
63
+ done
64
+ [ -n "$cmd" ] || [ -n "$none" ] || { echo "REFUSED: pass --test 'COMMAND', or --none 'REASON'" >&2; exit 2; }
65
+ [ -z "$cmd" ] || [ -z "$none" ] || { echo "REFUSED: --test and --none are exclusive" >&2; exit 2; }
66
+ if ! top=$(git -C "$clone" rev-parse --show-toplevel 2>/dev/null) || ! cd "$top"; then
67
+ echo "REFUSED: $clone is not a git clone" >&2
68
+ exit 2
69
+ fi
70
+ head=$(git rev-parse HEAD)
71
+ mkdir -p "$home/proofs"
72
+ stamp="$home/proofs/$head"
73
+ when=$(date -u +%Y-%m-%dT%H:%M:%SZ)
74
+
75
+ if [ -n "$none" ]; then
76
+ printf 'head: %s\nproof: none\nreason: %s\nchecked: %s\n' "$head" "$none" "$when" > "$stamp"
77
+ echo "proof: none recorded for ${head:0:12}: $none"
78
+ exit 0
79
+ fi
80
+
81
+ [ -z "$(git status --porcelain)" ] || { echo "REFUSED: the working tree is dirty, so the arms would not be the commits; after a proof run that was killed, git checkout -- . puts the head back" >&2; exit 2; }
82
+ [ -n "$base" ] || base=$(sed -n 's/^base-ref: //p' "$home/branch-checks/$head" 2>/dev/null | tr -d '\r')
83
+ if [ -z "$base" ]; then
84
+ for candidate in upstream/main upstream/master origin/main origin/master; do
85
+ if git rev-parse --verify -q "$candidate" >/dev/null 2>&1; then base="$candidate"; break; fi
86
+ done
87
+ fi
88
+ [ -n "$base" ] || { echo "REFUSED: no --base, no branch-check stamp for ${head:0:12} to read it from, and no upstream or origin main or master" >&2; exit 2; }
89
+ mb=$(git merge-base "$base" HEAD 2>/dev/null) || { echo "REFUSED: $base and HEAD share no history" >&2; exit 2; }
90
+
91
+ listed() { local n="$1" k; shift; for k in "$@"; do [ "$n" = "$k" ] && return 0; done; return 1; }
92
+ base_file_for() { local e; for e in ${basefiles[@]+"${basefiles[@]}"}; do case "$e" in "$1="*) printf '%s' "${e#*=}"; return 0 ;; esac; done; return 1; }
93
+ mode_in() { git ls-tree "$1" -- "$2" | cut -c1-6; }
94
+ # Renames are read as the delete and the add they are, so the base arm gets
95
+ # the old path back; quotePath off, so a non-ASCII path is the path.
96
+ changed=$(git -c core.quotePath=false diff --name-only --no-renames "$mb" HEAD)
97
+ for p in ${keeps[@]+"${keeps[@]}"} ${forces[@]+"${forces[@]}"} ${basefiles[@]+"${basefiles[@]%%=*}"}; do
98
+ printf '%s\n' "$changed" | grep -qxF -- "$p" || { echo "REFUSED: $p is not a file this branch changes (paths are as git diff --name-only prints them)" >&2; exit 2; }
99
+ done
100
+ fixes=(); kept=()
101
+ while IFS= read -r f; do
102
+ [ -n "$f" ] || continue
103
+ if listed "$f" ${keeps[@]+"${keeps[@]}"}; then kept+=("$f"); continue; fi
104
+ if ! listed "$f" ${forces[@]+"${forces[@]}"}; then
105
+ case "$f" in *test*|*Test*|*spec*) kept+=("$f"); continue ;; esac
106
+ fi
107
+ for t in HEAD "$mb"; do
108
+ case "$(mode_in "$t" "$f")" in
109
+ 100644|100755|"") ;;
110
+ *) echo "REFUSED: $f is not a regular file in $t (a symlink, a submodule or a directory); the experiment swaps regular files only" >&2; exit 2 ;;
111
+ esac
112
+ done
113
+ fixes+=("$f")
114
+ done <<EOF
115
+ $changed
116
+ EOF
117
+ if [ "${#fixes[@]}" -eq 0 ]; then
118
+ echo "REFUSED: every changed file is a test or kept (${kept[*]}); there is no fix to take away - --fix PATH takes one away although its path says test" >&2
119
+ exit 2
120
+ fi
121
+ echo "proof: taking away ${#fixes[@]} file(s): ${fixes[*]}${kept[0]:+; keeping ${#kept[@]} at the head: ${kept[*]}}"
122
+
123
+ hash_of() { if [ -f "$1" ]; then git hash-object -- "$1"; else echo absent; fi; }
124
+ hashes() { local f; for f in "${fixes[@]}"; do printf '%s %s\n' "$(hash_of "$f")" "$f"; done; }
125
+ run_arm() { # label -> rc in arm_rc, output tail in arm_out
126
+ arm_out=$(bash -c "$cmd" 2>&1); arm_rc=$?
127
+ arm_out=$(printf '%s\n' "$arm_out" | tail -15)
128
+ echo "== $1 arm: exit $arm_rc"
129
+ printf '%s\n' "$arm_out" | sed 's/^/ /'
130
+ }
131
+
132
+ head_hashes=$(hashes)
133
+ run_arm head
134
+ head_rc=$arm_rc; head_out=$arm_out
135
+
136
+ # The base arm: each fix file by its content at the merge base, through the
137
+ # clone's own filters so line endings are what a checkout gives, or gone
138
+ # when the branch added it. The restore below runs whatever happens in
139
+ # between. It restores from the commit, not the index: the tree was clean,
140
+ # so the two agree, and the commit brings the mode and the line endings back
141
+ # with the content, which a plain write does not.
142
+ restore() {
143
+ local f
144
+ for f in "${fixes[@]}"; do
145
+ if git cat-file -e "HEAD:./$f" 2>/dev/null; then
146
+ git checkout -q HEAD -- "$f"
147
+ else
148
+ rm -f -- "$f"
149
+ fi
150
+ done
151
+ }
152
+ trap restore EXIT
153
+ for f in "${fixes[@]}"; do
154
+ if bf=$(base_file_for "$f"); then
155
+ cat "$bf" > "$f"
156
+ elif git cat-file -e "$mb:./$f" 2>/dev/null; then
157
+ mkdir -p "$(dirname "$f")" && git cat-file --filters "$mb:./$f" > "$f"
158
+ [ "$(mode_in "$mb" "$f")" != 100755 ] || chmod +x "$f"
159
+ else
160
+ rm -f -- "$f"
161
+ fi
162
+ done
163
+ base_hashes=$(hashes)
164
+ run_arm base
165
+ base_rc=$arm_rc; base_out=$arm_out
166
+ restore
167
+ trap - EXIT
168
+
169
+ if [ "$(hashes)" != "$head_hashes" ]; then
170
+ echo "REFUSED: the head did not come back byte for byte; check the clone before anything else:" >&2
171
+ diff <(printf '%s\n' "$head_hashes") <(hashes) | sed 's/^/ /' >&2
172
+ exit 2
173
+ fi
174
+ dirt=$(git status --porcelain --untracked-files=no)
175
+ if [ -n "$dirt" ]; then
176
+ echo "REFUSED: the test command changed tracked files; restore them with git checkout -- <path>, then make the command leave the tree alone or commit what it generates:" >&2
177
+ printf '%s\n' "$dirt" | sed 's/^/ /' >&2
178
+ exit 2
179
+ fi
180
+ left=$(git status --porcelain | sed -n 's/^?? //p')
181
+ [ -z "$left" ] || echo "note: the test command left untracked files, which branch-check refuses: $(printf '%s' "$left" | tr '\n' ' ')"
182
+
183
+ failed=""
184
+ [ "$base_hashes" != "$head_hashes" ] || failed="the two arms ran the same files, so they did not test the fix"
185
+ case "$base_rc" in
186
+ 126|127) failed="${failed:+$failed; }the base arm's command did not run (exit $base_rc: $(printf '%s' "$base_out" | head -1)); a test named in the command has to exist in both arms" ;;
187
+ 0) failed="${failed:+$failed; }the test passes without the fix, so it does not show the bug${kept[0]:+ (or the fix is in a kept file: ${kept[*]})}" ;;
188
+ esac
189
+ [ "$head_rc" -eq 0 ] || failed="${failed:+$failed; }the test fails with the fix"
190
+ if [ -n "$failed" ]; then
191
+ echo "proof: not shown - $failed"
192
+ exit 1
193
+ fi
194
+
195
+ {
196
+ printf 'head: %s\nbase: %s\nproof: red-green\ncommand: %s\nchecked: %s\n' "$head" "$mb" "$cmd" "$when"
197
+ printf 'base-exit: %s\nhead-exit: %s\n' "$base_rc" "$head_rc"
198
+ printf '%s\n' "$head_hashes" | sed 's/^/head-file: /'
199
+ printf '%s\n' "$base_hashes" | sed 's/^/base-file: /'
200
+ for f in ${kept[@]+"${kept[@]}"}; do printf 'kept-file: %s\n' "$f"; done
201
+ for e in ${basefiles[@]+"${basefiles[@]}"}; do printf 'base-from: %s\n' "$e"; done
202
+ printf 'base-output:\n%s\nhead-output:\n%s\n' "$(printf '%s\n' "$base_out" | sed 's/^/ /')" "$(printf '%s\n' "$head_out" | sed 's/^/ /')"
203
+ } > "$stamp"
204
+ echo "proof: red on the base, green on the head, for ${head:0:12}; stamped at $stamp"
@@ -193,12 +193,22 @@ EOF
193
193
  done
194
194
  cat <<'EOF'
195
195
  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.
196
+ EOF
197
+ if [ -n "$clone" ]; then
198
+ if [ -f "$home/proofs/$head" ]; then
199
+ printf ' The author'"'"'s run, stamped by proof.sh; rerun it. A base arm that never ran the test (nothing collected, a file missing, a build that did not happen), or a swapped file the test needed for a reason other than the fix, is a finding:\n'
200
+ sed 's/^/ /' "$home/proofs/$head"
201
+ else
202
+ printf ' No proof stamp exists for this head: the author has not run proof.sh. Say so.\n'
203
+ fi
204
+ fi
205
+ cat <<'EOF'
196
206
  3. The project's own CI, read from its workflow files: tests, lint, format, API and mutation checks, run on the change.
197
207
  4. The form: write the changed block the way the best engineer in this language would, in this repository's own idioms and at the language version its manifest names, then measure the diff against it. Fewer moving parts, no state the loop already carries, no extra pass, no extra allocation, no counter beside a cursor that could be bounded itself. When yours is smaller or clearer, the finding is SHOULD FIX with your version in full; when the diff is already that form, say which alternatives you tried and why each is worse. A maintainer who asks for a rework is a review round you could have spent on the merge.
198
208
  5. 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.
199
209
  6. Prior art: nothing duplicates an open pull request or argues with a ruling in a closed issue.
200
210
  7. 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.
201
- 8. Anything else that would embarrass the author in front of this maintainer.
211
+ 8. Anything else that would embarrass the author in front of this maintainer. When a text is the author's declaration (not-tested and assumptions), rerun every "checked by", and test the assumptions marked unchecked yourself: a false assumption is a finding, and so is one the change rests on that the declaration leaves out. A "checked by" that cannot be rerun as written, because it names no command or no result, is a finding too.
202
212
  EOF
203
213
  if [ -n "$clone" ]; then
204
214
  cat <<'EOF'
@@ -110,6 +110,17 @@ report it in one line and do not proceed to the next step.
110
110
  invariant instead of resetting it, and its grid has a row for every
111
111
  order the two are called in, the same value twice and null included. A
112
112
  reviewer lists the empty cells otherwise.
113
+ Then run it through the gate: `housebroken proof <clone> --test '<the
114
+ command that runs the new test>'` runs the test on the head and again with
115
+ every changed file that is not a test swapped back to the base, restores
116
+ the head by hash, and stamps it only when the base arm fails and the head
117
+ arm passes. `--keep <path>` keeps a fixture the test reads; `--fix <path>`
118
+ takes a file away although its path says test; `--base-file
119
+ <path>=<file>` gives the base arm a file that carries both the test and
120
+ the fix, a Rust module with its `#[cfg(test)]` block, as the base plus
121
+ the test. A change with
122
+ nothing that can fail first, documentation or a pinned refactor, records
123
+ `--none '<reason>'`. The filing requires the stamp.
113
124
 
114
125
  5. **Run their CI, not yours.**
115
126
  Run the project's own test target on the fresh clone, and every gate its
@@ -182,7 +193,12 @@ report it in one line and do not proceed to the next step.
182
193
  makes so you can falsify each one against the code, and it stamps the
183
194
  head it passed, which the filing requires. Then the adversarial review:
184
195
  `housebroken review brief --author <your model> --repo owner/repo --clone <clone> --text <body file>`
185
- names the reviewer and prints its brief. The reviewer is never the model
196
+ names the reviewer and prints its brief; first run `housebroken declare
197
+ <clone>`, fill in what the change was not tested against and every
198
+ assumption it rests on ("- <assumption> | if wrong: <what breaks> |
199
+ checked by: <command and result, or unchecked>"), and pass that file to
200
+ the brief as a second `--text`, so the reviewer attacks it and the review
201
+ is bound to it. The reviewer is never the model
186
202
  that wrote the change: one tier down, Fable to Opus and Opus to Sonnet,
187
203
  and never below Sonnet, so Sonnet's work goes to Opus. Hand the brief to a
188
204
  fresh agent running that model; anything of yours it reads is for it to
@@ -216,8 +232,9 @@ report it in one line and do not proceed to the next step.
216
232
  has not stamped the exact head against the branch it is filed into
217
233
  (`--base` to both when the project takes pull requests on a branch other
218
234
  than its default), when the upstream has since changed a file the branch
219
- changes, when there is no POST AS IS review of the exact head and body
220
- being filed, when you already have three open pull requests on the
235
+ changes, when there is no proof stamp for the head, when the declaration
236
+ does not pass, when there is no POST AS IS review of the exact head, body
237
+ and declaration being filed, when you already have three open pull requests on the
221
238
  repository, and when your last one there is under ten minutes old.
222
239
  A refusal is a result; fix the cause.
223
240
 
@@ -0,0 +1 @@
1
+ __version__ = "0.8.0"
@@ -1 +0,0 @@
1
- __version__ = "0.7.0"
File without changes