Repository navigation
feat: count the commits since a pull request's review, and diff only those with i - #14
Conversation
The cursor can start on #12, and every GitHub row's document holds mona's review. So the wait after g j j could pass before the keys landed, and the one-shot frame read then caught the cursor on the header mid-move. Under the parallel suite this failed one run in several. The test now waits for the header's own panel after g, then for #12's document beside a row of the table, rather than reading one frame. Agent-Name: claude-code-unattended Agent-Session: 3689c9b4-3208-4138-b764-71b0ed5b83a7 Agent-Run: 01a1bbb1-ce3f-7b4b-bac3-94b947201ee5 Worklog: 1330.1
…those with i After a review, a fixer pushes to the same branch, and the reader needs only what the push changed. GitHub records the commit each review was submitted against, and the detail read already holds it beside the branch's commits. - A row's second line reads "2 commits since review" where the head moved past the commit the newest review read, and "rebased since review" where the branch does not hold that commit. A row nobody reviewed reads nothing. - i fetches the head as d does, asks git whether the fetched head holds the reviewed commit, and opens nvim's diff from that commit to the head. Where it does not, or no review was submitted, it opens the whole diff and the footer says why once nvim exits. Where nothing was pushed since the review, it says so and opens nothing. Bitbucket's client reads no reviews, so i refuses there. D stays unbound, because it deletes. - The threads file i hands nvim names the reviewed commit as its base and holds only the RIGHT threads, because a LEFT thread's line counts in the base branch's copy, which this diff does not show. - d and i share openDiff, so the fetch, the socket, the window name and the merge's pinned head stay one path. Agent-Name: claude-code-unattended Agent-Session: 3689c9b4-3208-4138-b764-71b0ed5b83a7 Agent-Run: 01a1bbb1-ce3f-7b4b-bac3-94b947201ee5 Worklog: 1330.1
There was a problem hiding this comment.
PR review — failure-paths
Used: Read the PR diff and run/Run in pullrequest.go at head. Traced the effects: i's fetch, the git merge-base call and the nvim launch. Ran no failure, because the git merge-base failure path is traced from the code.
Standards: Searched "error collapsed to false reads as a real answer" and "a failed check reported as the negative answer". Cited cli-design.md "A partial or voided run is reported as partial, never as clean" and code-quality.md "A run that could not answer never replaces the last answer in a view of current state".
Findings: 1 correctness · 0 design · 0 prose
Correctness
failure-paths.1 internal/pullrequests/fetch.go:HeadHolds · internal/board/actions.go:diffSinceReview
Reach: Any failure of git merge-base --is-ancestor other than exit 1 reaches it. Examples: the 2-minute readTimeout kill, a git binary error, a locked or corrupt repo, a checkout that vanished. i then runs on a row whose review commit is on the branch. Traced from the code, not run.
HeadHolds returns err == nil, so every failure of the git call is reported as "the commit is not an ancestor". diffSinceReview turns that into wholeDiff(... "the commit its newest review read, X, is gone from the branch, as after a rebase").
Only exit 1 (not an ancestor) and exit 128 (unknown commit) mean the commit is gone. A timeout, a kill, a git error or a missing checkout did not answer the question. The user gets the whole diff and a footer that falsely says a rebase happened. They lose the since-review view and are told something untrue.
Fix: have HeadHolds return (bool, error). Treat only the exit codes 1 and 128 as false. On any other error, make i fail like a fetch failure, with m.Say of the git error, and open nothing. Alternatively, open the whole diff with a footer that says the check failed and why.
See cli-design.md § "A partial or voided run is reported as partial, never as clean".
Findings as JSON
{
"used": "Read the PR diff and run/Run in pullrequest.go at head. Traced the effects: i's fetch, the git merge-base call and the nvim launch. Ran no failure, because the git merge-base failure path is traced from the code.",
"standards": "Searched \"error collapsed to false reads as a real answer\" and \"a failed check reported as the negative answer\". Cited cli-design.md \"A partial or voided run is reported as partial, never as clean\" and code-quality.md \"A run that could not answer never replaces the last answer in a view of current state\".",
"findings": [
{
"number": "failure-paths.1",
"section": "correctness",
"sites": [
"internal/pullrequests/fetch.go:HeadHolds",
"internal/board/actions.go:diffSinceReview"
],
"reach": "Any failure of `git merge-base --is-ancestor` other than exit 1 reaches it. Examples: the 2-minute readTimeout kill, a git binary error, a locked or corrupt repo, a checkout that vanished. `i` then runs on a row whose review commit is on the branch. Traced from the code, not run.",
"body": "`HeadHolds` returns `err == nil`, so every failure of the git call is reported as \"the commit is not an ancestor\". `diffSinceReview` turns that into `wholeDiff(... \"the commit its newest review read, X, is gone from the branch, as after a rebase\")`.\n\nOnly exit 1 (not an ancestor) and exit 128 (unknown commit) mean the commit is gone. A timeout, a kill, a git error or a missing checkout did not answer the question. The user gets the whole diff and a footer that falsely says a rebase happened. They lose the since-review view and are told something untrue.\n\nFix: have `HeadHolds` return `(bool, error)`. Treat only the exit codes 1 and 128 as `false`. On any other error, make `i` fail like a fetch failure, with `m.Say` of the git error, and open nothing. Alternatively, open the whole diff with a footer that says the check failed and why.\n\nSee cli-design.md § \"A partial or voided run is reported as partial, never as clean\"."
}
]
}This review does not gate merge.
There was a problem hiding this comment.
PR review — code-architecture
Used: I cloned the repo and checked out PR 14's head (56edb72). I read the diff and then read actions.go, rows.go, diff_files.go, detail.go, fetch.go, provider.go and reviewrounds/rounds.go in the repo at the head. I used rg to find every other site that decides the newest review, the reviewed commit, whether a host's detail is read, the diff's left side and thread side. I ran no build or tests.
Standards: I checked the change against code-quality.md § "Ask whatever owns a fact, the function or enum that computes it, never duplicate its logic at a second site". The change follows it. Searches: "one owner per fact duplicated predicate" and "provider capability nil verb".
Findings: 0 correctness · 0 design · 0 prose
No findings.
Findings as JSON
{
"used": "I cloned the repo and checked out PR 14's head (56edb72). I read the diff and then read `actions.go`, `rows.go`, `diff_files.go`, `detail.go`, `fetch.go`, `provider.go` and `reviewrounds/rounds.go` in the repo at the head. I used rg to find every other site that decides the newest review, the reviewed commit, whether a host's detail is read, the diff's left side and thread `side`. I ran no build or tests.",
"standards": "I checked the change against `code-quality.md` § \"Ask whatever owns a fact, the function or enum that computes it, never duplicate its logic at a second site\". The change follows it. Searches: \"one owner per fact duplicated predicate\" and \"provider capability nil verb\".",
"findings": []
}This review does not gate merge.
There was a problem hiding this comment.
PR review — stale-prose
Used: I cloned PR 14 at head 56edb72 and read every added sentence beside its code. Those sentences sit in README.md, CLAUDE.md, the diff-threads schema, the tui help, and the doc comments in actions.go, rows.go, diff_files.go, detail.go, fetch.go and tui_test.go. I ran a throwaway Go test in internal/board, then deleted it. With 150 commits, a 2-commit read and an older reviewed commit, sinceReviewCell returned "". The footer sentence rendered with short("").
Standards: I applied pull-requests.md/Sentences the diff adds are checked for what goes stale, and the finding carries the rewrite. Searches surfaced two more: documentation.md/A comment states what this repos code enforces, never another repos deployment as fact, and help.md/Help text never carries a count, version, path or list of what is supported.
Findings: 0 correctness · 0 design · 3 prose
Prose
stale-prose.1 README.md:145 · internal/board/rows.go:440
Reach: A pull request with more than 100 commits whose newest review read a commit older than the last 100. The detail read is commits(last: 100) at internal/pullrequests/github_detail.go:34. SinceReview returns Known: false there (internal/pullrequests/detail.go:96), so the row shows nothing.
False: two lists of when the field reads nothing leave out a case.
The sentences:
README.md:145: "It readsrebased since reviewwhere the branch does not hold that commit, and nothing where no review was submitted or the head is the reviewed commit."internal/board/rows.go:440: "It is empty where the detail is not read, no review was submitted, or nothing was pushed since."
Both lists claim to be complete, and both miss one case. The field is also empty when the reviewed commit is older than the 100 commits the detail read holds. A throwaway test fed sinceReviewCell a detail with CommitCount: 150, two commits held, and a review at a commit outside them. It returned "". A user reading the README will take a blank field to mean nothing was pushed since the review. On a long branch that reading is wrong.
SinceReview's own Known doc at detail.go already states the cut correctly. The fix is to stop listing cases and say what a blank means.
Rewrite for README.md: "It reads rebased since review where the branch does not hold that commit. A blank means prs could not tell, or there is nothing to count."
Rewrite for rows.go: "It is empty wherever SinceReview is not Known or counts no commits." That rewrite points at the type that owns the cases instead of copying them.
stale-prose.2 internal/board/actions.go:50 · CLAUDE.md:102
Reach: Nothing in the repo checks these claims. Any terminal tool that binds a letter to an interdiff, or gives D another meaning, makes the sentences false. No diff to prs would show it.
Will go stale: two sentences state what other tools bind.
The sentences:
internal/board/actions.go:50: "i is the interdiff, the diff since review, which no common tool binds a letter to, and D stays unbound because it deletes."CLAUDE.md:102: "Dstays unbound, because it deletes in the TUIs prs sits among."
"No common tool binds a letter to" this diff is a fact about the outside world. It has no source and nothing keeps it current. "Stays unbound" also describes the change: D was never bound. The PR body already records the survey behind the choice, and that is where it belongs.
Rewrite for actions.go: "i is the interdiff, the diff since review. D is left unbound, because D deletes in other terminal tools."
Rewrite for CLAUDE.md: "D is left unbound, so a key that deletes elsewhere does nothing here." Both rewrites keep the reason for the decision and drop the survey claim.
stale-prose.3 internal/board/actions.go:214
Reach: A GitHub review whose commit comes back null. github_detail.go:234 then leaves CommitOID empty. diffSinceReview reaches the review.CommitOID == "" case, and short("") returns "a commit gone from the branch".
False: with an empty reviewed commit, the footer says "gone from the branch" twice.
The sentence at internal/board/actions.go:214 is: "the commit its newest review read, %s, is gone from the branch, as after a rebase", filled with at := short(review.CommitOID).
I ran it with short(""). It returned: "the commit its newest review read, a commit gone from the branch, is gone from the branch, as after a rebase". The sentence claims to name the commit, but here it repeats itself instead.
The fix is a separate sentence for the empty case, for example: "GitHub no longer names the commit its newest review read, as after a rebase." The existing sentence stays for the case where the commit is named.
Findings as JSON
{
"used": "I cloned PR 14 at head 56edb72 and read every added sentence beside its code. Those sentences sit in README.md, CLAUDE.md, the diff-threads schema, the tui help, and the doc comments in actions.go, rows.go, diff_files.go, detail.go, fetch.go and tui_test.go. I ran a throwaway Go test in internal/board, then deleted it. With 150 commits, a 2-commit read and an older reviewed commit, sinceReviewCell returned \"\". The footer sentence rendered with `short(\"\")`.",
"standards": "I applied pull-requests.md/Sentences the diff adds are checked for what goes stale, and the finding carries the rewrite. Searches surfaced two more: documentation.md/A comment states what this repos code enforces, never another repos deployment as fact, and help.md/Help text never carries a count, version, path or list of what is supported.",
"findings": [
{
"number": "stale-prose.1",
"section": "prose",
"sites": [
"README.md:145",
"internal/board/rows.go:440"
],
"reach": "A pull request with more than 100 commits whose newest review read a commit older than the last 100. The detail read is `commits(last: 100)` at internal/pullrequests/github_detail.go:34. `SinceReview` returns `Known: false` there (internal/pullrequests/detail.go:96), so the row shows nothing.",
"body": "**False: two lists of when the field reads nothing leave out a case.**\n\nThe sentences:\n- `README.md:145`: \"It reads `rebased since review` where the branch does not hold that commit, and nothing where no review was submitted or the head is the reviewed commit.\"\n- `internal/board/rows.go:440`: \"It is empty where the detail is not read, no review was submitted, or nothing was pushed since.\"\n\nBoth lists claim to be complete, and both miss one case. The field is also empty when the reviewed commit is older than the 100 commits the detail read holds. A throwaway test fed `sinceReviewCell` a detail with `CommitCount: 150`, two commits held, and a review at a commit outside them. It returned `\"\"`. A user reading the README will take a blank field to mean nothing was pushed since the review. On a long branch that reading is wrong.\n\n`SinceReview`'s own `Known` doc at detail.go already states the cut correctly. The fix is to stop listing cases and say what a blank means.\n\nRewrite for README.md: \"It reads `rebased since review` where the branch does not hold that commit. A blank means prs could not tell, or there is nothing to count.\"\n\nRewrite for rows.go: \"It is empty wherever SinceReview is not Known or counts no commits.\" That rewrite points at the type that owns the cases instead of copying them."
},
{
"number": "stale-prose.2",
"section": "prose",
"sites": [
"internal/board/actions.go:50",
"CLAUDE.md:102"
],
"reach": "Nothing in the repo checks these claims. Any terminal tool that binds a letter to an interdiff, or gives `D` another meaning, makes the sentences false. No diff to prs would show it.",
"body": "**Will go stale: two sentences state what other tools bind.**\n\nThe sentences:\n- `internal/board/actions.go:50`: \"i is the interdiff, the diff since review, which no common tool binds a letter to, and D stays unbound because it deletes.\"\n- `CLAUDE.md:102`: \"`D` stays unbound, because it deletes in the TUIs prs sits among.\"\n\n\"No common tool binds a letter to\" this diff is a fact about the outside world. It has no source and nothing keeps it current. \"Stays unbound\" also describes the change: `D` was never bound. The PR body already records the survey behind the choice, and that is where it belongs.\n\nRewrite for actions.go: \"i is the interdiff, the diff since review. D is left unbound, because D deletes in other terminal tools.\"\n\nRewrite for CLAUDE.md: \"`D` is left unbound, so a key that deletes elsewhere does nothing here.\" Both rewrites keep the reason for the decision and drop the survey claim."
},
{
"number": "stale-prose.3",
"section": "prose",
"sites": [
"internal/board/actions.go:214"
],
"reach": "A GitHub review whose `commit` comes back null. github_detail.go:234 then leaves `CommitOID` empty. `diffSinceReview` reaches the `review.CommitOID == \"\"` case, and `short(\"\")` returns \"a commit gone from the branch\".",
"body": "**False: with an empty reviewed commit, the footer says \"gone from the branch\" twice.**\n\nThe sentence at `internal/board/actions.go:214` is: `\"the commit its newest review read, %s, is gone from the branch, as after a rebase\"`, filled with `at := short(review.CommitOID)`.\n\nI ran it with `short(\"\")`. It returned: \"the commit its newest review read, a commit gone from the branch, is gone from the branch, as after a rebase\". The sentence claims to name the commit, but here it repeats itself instead.\n\nThe fix is a separate sentence for the empty case, for example: \"GitHub no longer names the commit its newest review read, as after a rebase.\" The existing sentence stays for the case where the commit is named."
}
]
}This review does not gate merge.
There was a problem hiding this comment.
PR review — contracts
Used: I built the PR head (56edb72) to ../bin/prs. I ran prs config show, then prs tui under a pty, with a config in my directory that binds a window action to i. Both refuse that key. I read the threads schema, the diff-file writer, the key table, Refused, the snapshot types and prs tui --help. I cloned datapointchris/dotfiles, which holds the only caller of the threads file, configs/common/.config/nvim/lua/core/pull-request-panel.lua, and the only prs config, configs/common/.config/prs/config.yml.tmpl. I ran gh search code --owner datapointchris for window_actions, prs-diff and PRS_DIFF_THREADS. They found nothing more.
Standards: Cited: cli-design.md § "Anything a table or screen shows is reachable through --json or a named verb, never only by parsing it" and cli-design.md § "One fact gets one answer on every command that reaches it". I searched for "field gains a second meaning", "file format version schema id change meaning", "new built-in key collides with user config key", "config that was valid becomes an error after upgrade" and "reserved names user-defined keybindings". None of those searches found a rule about a file field whose meaning depends on the mode, or about built-in keys taking over config keys.
Findings: 0 correctness · 3 design · 0 prose
Design and architecture
contracts.1 cmd/prs/diff-threads.schema.json:14 · cmd/prs/diff-threads.schema.json:17 · internal/board/diff_files.go:51 · internal/board/diff_files.go:59 · dotfiles:configs/common/.config/nvim/lua/core/pull-request-panel.lua:16
Reach: No caller breaks today. The only reader is dotfiles' pull-request-panel.lua, which reads title, threads_read and each thread's path, line, side, resolved, comments and comment_count. It never reads base. The cost lands on the next reader of base or of the threads count. One example is a Claude session split into the diff <repo> #<n> window that labels the diff, or counts open threads, from this file.
The threads file's base now has two meanings, and nothing in the file says which one applies.
Under d, base is origin/<base branch>, the diff is three-dot, and threads holds every thread. Under i, base is a bare commit SHA, the diff is two-dot from that commit, and threads silently drops each LEFT thread. The $id stays urn:prs:diff-threads:v1, and no field records which kind of diff was opened. A reader can tell the two apart only by checking whether base looks like a SHA. A reader also cannot tell that threads is filtered. threads_read: true with two threads could mean the PR has two threads, or it could mean three with one LEFT thread removed.
The PR says it rejected a field naming the left side, because readers would then handle two meanings of side. A field that names the kind of diff leaves side alone. Instead, base itself now carries the two meanings.
Fix: add a required field that states the kind of diff, such as "diff": "whole" | "since_review". Alternatively, keep base as origin/<base> in both cases and add since: <reviewed sha> for i. Either way, state in the schema that threads was filtered, for example with threads_left_out: <n>. The panel decodes leniently, so it keeps working with an added field.
contracts.2 internal/board/actions.go:56 · internal/board/actions.go:33 · internal/board/board.go:100 · cmd/prs/config.go:126 · README.md:257
Reach: No config found binds i. The only prs config is dotfiles' configs/common/.config/prs/config.yml.tmpl, which binds r and C. Any user config with key: i stops working on upgrade. The next built-in key will do the same to whichever letter it takes.
A config that binds i was valid before this PR. After it, prs tui refuses to start.
I ran the head with a config that holds window_actions: [{key: i, label: my interdiff, …}]. prs tui exited with:
Error: …/config.yml: window action "my interdiff": key "i" is prs's own, for view diff since review: …
prs config show printed key i my interdiff refused, key "i" is prs's own, ….
This behavior comes from Refused and is not new. This PR is the first to take a letter that was free for user keys. README § "Keys of your own" lists no keys prs reserves, and does not warn that a clash stops startup. A user cannot avoid a future clash.
Fix: list the letters prs owns, or reserves, in README § "Keys of your own". Alternatively, skip the clashing window action with a footer warning instead of failing startup. The PR body should also name i as a key users can no longer bind.
contracts.3 internal/board/rows.go:442 · internal/board/board.go:486 · cmd/prs/tui.go:108 · cmd/prs/list.schema.json:33
Reach: An agent that runs prs tui --json to read what a person reads, as the help text promises, cannot see rebased since review or 2 commits since review. It would have to scrape a frame. The PR body says no --json output gains the field.
The board shows the commit count since the newest review, but --json cannot reach it.
The new second-line field (2 commits since review, rebased since review) appears only in the rendered board. prs tui --help says --json prints "the snapshot it is drawn from, so an agent reads what a person reads". The snapshot's Row holds no field for it. That breaks the help text's promise and cli-design.md § "Anything a table or screen shows is reachable through --json or a named verb, never only by parsing it". The Claude-round cell has the same gap already. This PR adds a second field with that gap.
Fix: add since_review to the snapshot row and to cmd/prs/list.schema.json, for example {commits, rebased} or null where it is unknown, filled from the detail when it is held. Alternatively, narrow the help sentence to say which on-screen fields --json leaves out.
Findings as JSON
{
"used": "I built the PR head (56edb72) to `../bin/prs`. I ran `prs config show`, then `prs tui` under a pty, with a config in my directory that binds a window action to `i`. Both refuse that key. I read the threads schema, the diff-file writer, the key table, `Refused`, the snapshot types and `prs tui --help`. I cloned datapointchris/dotfiles, which holds the only caller of the threads file, `configs/common/.config/nvim/lua/core/pull-request-panel.lua`, and the only prs config, `configs/common/.config/prs/config.yml.tmpl`. I ran `gh search code --owner datapointchris` for `window_actions`, `prs-diff` and `PRS_DIFF_THREADS`. They found nothing more.",
"standards": "Cited: cli-design.md § \"Anything a table or screen shows is reachable through --json or a named verb, never only by parsing it\" and cli-design.md § \"One fact gets one answer on every command that reaches it\". I searched for \"field gains a second meaning\", \"file format version schema id change meaning\", \"new built-in key collides with user config key\", \"config that was valid becomes an error after upgrade\" and \"reserved names user-defined keybindings\". None of those searches found a rule about a file field whose meaning depends on the mode, or about built-in keys taking over config keys.",
"findings": [
{
"number": "contracts.1",
"section": "design",
"sites": [
"cmd/prs/diff-threads.schema.json:14",
"cmd/prs/diff-threads.schema.json:17",
"internal/board/diff_files.go:51",
"internal/board/diff_files.go:59",
"dotfiles:configs/common/.config/nvim/lua/core/pull-request-panel.lua:16"
],
"reach": "No caller breaks today. The only reader is dotfiles' `pull-request-panel.lua`, which reads `title`, `threads_read` and each thread's `path`, `line`, `side`, `resolved`, `comments` and `comment_count`. It never reads `base`. The cost lands on the next reader of `base` or of the threads count. One example is a Claude session split into the `diff <repo> #<n>` window that labels the diff, or counts open threads, from this file.",
"body": "**The threads file's `base` now has two meanings, and nothing in the file says which one applies.**\n\nUnder `d`, `base` is `origin/<base branch>`, the diff is three-dot, and `threads` holds every thread. Under `i`, `base` is a bare commit SHA, the diff is two-dot from that commit, and `threads` silently drops each `LEFT` thread. The `$id` stays `urn:prs:diff-threads:v1`, and no field records which kind of diff was opened. A reader can tell the two apart only by checking whether `base` looks like a SHA. A reader also cannot tell that `threads` is filtered. `threads_read: true` with two threads could mean the PR has two threads, or it could mean three with one `LEFT` thread removed.\n\nThe PR says it rejected a field naming the left side, because readers would then handle two meanings of `side`. A field that names the kind of diff leaves `side` alone. Instead, `base` itself now carries the two meanings.\n\n**Fix:** add a required field that states the kind of diff, such as `\"diff\": \"whole\" | \"since_review\"`. Alternatively, keep `base` as `origin/<base>` in both cases and add `since: <reviewed sha>` for `i`. Either way, state in the schema that `threads` was filtered, for example with `threads_left_out: <n>`. The panel decodes leniently, so it keeps working with an added field."
},
{
"number": "contracts.2",
"section": "design",
"sites": [
"internal/board/actions.go:56",
"internal/board/actions.go:33",
"internal/board/board.go:100",
"cmd/prs/config.go:126",
"README.md:257"
],
"reach": "No config found binds `i`. The only prs config is dotfiles' `configs/common/.config/prs/config.yml.tmpl`, which binds `r` and `C`. Any user config with `key: i` stops working on upgrade. The next built-in key will do the same to whichever letter it takes.",
"body": "**A config that binds `i` was valid before this PR. After it, `prs tui` refuses to start.**\n\nI ran the head with a config that holds `window_actions: [{key: i, label: my interdiff, …}]`. `prs tui` exited with:\n\n`Error: …/config.yml: window action \"my interdiff\": key \"i\" is prs's own, for view diff since review: …`\n\n`prs config show` printed `key i my interdiff refused, key \"i\" is prs's own, …`.\n\nThis behavior comes from `Refused` and is not new. This PR is the first to take a letter that was free for user keys. README § \"Keys of your own\" lists no keys prs reserves, and does not warn that a clash stops startup. A user cannot avoid a future clash.\n\n**Fix:** list the letters prs owns, or reserves, in README § \"Keys of your own\". Alternatively, skip the clashing window action with a footer warning instead of failing startup. The PR body should also name `i` as a key users can no longer bind."
},
{
"number": "contracts.3",
"section": "design",
"sites": [
"internal/board/rows.go:442",
"internal/board/board.go:486",
"cmd/prs/tui.go:108",
"cmd/prs/list.schema.json:33"
],
"reach": "An agent that runs `prs tui --json` to read what a person reads, as the help text promises, cannot see `rebased since review` or `2 commits since review`. It would have to scrape a frame. The PR body says no `--json` output gains the field.",
"body": "**The board shows the commit count since the newest review, but `--json` cannot reach it.**\n\nThe new second-line field (`2 commits since review`, `rebased since review`) appears only in the rendered board. `prs tui --help` says `--json` prints \"the snapshot it is drawn from, so an agent reads what a person reads\". The snapshot's `Row` holds no field for it. That breaks the help text's promise and cli-design.md § \"Anything a table or screen shows is reachable through --json or a named verb, never only by parsing it\". The Claude-round cell has the same gap already. This PR adds a second field with that gap.\n\n**Fix:** add `since_review` to the snapshot row and to `cmd/prs/list.schema.json`, for example `{commits, rebased}` or null where it is unknown, filled from the detail when it is held. Alternatively, narrow the help sentence to say which on-screen fields `--json` leaves out."
}
]
}This review does not gate merge.
There was a problem hiding this comment.
PR review — system-design
Used: Cloned and checked out PR 14. Read the diff and the fetch.go, detail.go and actions.go code it touches. I did not run the TUI.
Standards: I cited no rules. I searched no standards, because I found nothing that needed one.
Findings: 0 correctness · 0 design · 0 prose
No findings.
Findings as JSON
{
"used": "Cloned and checked out PR 14. Read the diff and the fetch.go, detail.go and actions.go code it touches. I did not run the TUI.",
"standards": "I cited no rules. I searched no standards, because I found nothing that needed one.",
"findings": []
}This review does not gate merge.
There was a problem hiding this comment.
PR review — fleet-patterns
Used: I cloned prs at PR 14's head (56edb72) and read the whole diff. I shallow-cloned five sibling repos: claude, dotfiles, bbkt, fleet and forge. I searched them with rg for every other "since review" count, interdiff, reviewed-commit read and merge-base --is-ancestor check. I read the dotfiles nvim panel that consumes the threads file, fleet's reviewrounds package and fleet's fix worktree.
Standards: Nearest rule: code-architecture.md/a-chokepoints-error-check-is-only-as-strict-as-the-command-it-wraps. It is near the finding but does not reach it. These searches found no rule: "git merge-base is-ancestor exit status distinguish no from failure", "commits since review interdiff", "diff since last review" and "subprocess exit code failure read as answer".
Findings: 0 correctness · 1 design · 0 prose
Design and architecture
fleet-patterns.1 internal/pullrequests/fetch.go:94 · internal/board/actions.go:213 · fleet:fix/worktree.go:328
Reach: i on any GitHub row with a submitted review calls HeadHolds after its fetch. A git merge-base that times out at readTimeout (2 minutes), runs in a broken checkout, or cannot find git reads as false. The footer then says the reviewed commit is "gone from the branch, as after a rebase". The next fix to this ancestry check has to be made in prs and in fleet separately, because the two copies already disagree on which exit status means "no".
prs reads every git failure as "not on the branch", and fleet does not. Both repos ask git whether a known commit is still in a branch's history.
- prs's
HeadHolds(internal/pullrequests/fetch.go:94) runsgit merge-base --is-ancestor <reviewed> <ref>and returnserr == nil. Every non-zero exit becomesfalse. That covers exit 1 (not an ancestor), exit 128 (unknown commit), thereadTimeoutkill and a missing git binary.diffSinceReview(internal/board/actions.go:213) turns thatfalseinto the whole diff, with the footer "is gone from the branch, as after a rebase". - fleet's
keepsStart(fleet:fix/worktree.go:328) runs the same command and returns(bool, error). Exit 0 is yes and exit 1 is no. Any other outcome is an error that names the commit: "ask whether HEAD descends from …".
fleet's shape is the better one. It keeps a failed question apart from a "no" answer, so a timeout never reads as a rebase. prs needs one difference from fleet. A commit git does not have at all is a real "no" for prs, because the stand-in test's rebasedAway case exits 128 with "Not a valid commit name". fleet would call that an error. That edge is already a disagreement between the two copies.
Fix: give HeadHolds fleet's signature, (bool, error). Read exit 1, and exit 128 with git's "Not a valid commit name", as false, nil. Read every other failure as an error, and have diffSinceReview report it in the footer the way openDiff reports a failed base fetch.
This is weak evidence. One other repo carries the pattern, and it is reported only because the two copies already disagree. No other repo counts commits since a review or opens an interdiff, so SinceReview and NewestReview have no sibling to match. Two sites that look related agree with this PR. The Claude review rounds post their reviews with commit_id set to the head (claude:src/builder/pr_review_round.py:1092), so GitHub's review commit and the round marker's head name the same commit. The dotfiles panel (dotfiles:configs/common/.config/nvim/lua/core/pull-request-panel.lua:107) places threads by side and never reads base, so dropping LEFT threads in prs leaves its contract intact.
Findings as JSON
{
"used": "I cloned prs at PR 14's head (56edb72) and read the whole diff. I shallow-cloned five sibling repos: claude, dotfiles, bbkt, fleet and forge. I searched them with rg for every other \"since review\" count, interdiff, reviewed-commit read and `merge-base --is-ancestor` check. I read the dotfiles nvim panel that consumes the threads file, fleet's reviewrounds package and fleet's fix worktree.",
"standards": "Nearest rule: code-architecture.md/a-chokepoints-error-check-is-only-as-strict-as-the-command-it-wraps. It is near the finding but does not reach it. These searches found no rule: \"git merge-base is-ancestor exit status distinguish no from failure\", \"commits since review interdiff\", \"diff since last review\" and \"subprocess exit code failure read as answer\".",
"findings": [
{
"number": "fleet-patterns.1",
"section": "design",
"sites": [
"internal/pullrequests/fetch.go:94",
"internal/board/actions.go:213",
"fleet:fix/worktree.go:328"
],
"reach": "`i` on any GitHub row with a submitted review calls `HeadHolds` after its fetch. A `git merge-base` that times out at `readTimeout` (2 minutes), runs in a broken checkout, or cannot find git reads as `false`. The footer then says the reviewed commit is \"gone from the branch, as after a rebase\". The next fix to this ancestry check has to be made in prs and in fleet separately, because the two copies already disagree on which exit status means \"no\".",
"body": "**prs reads every git failure as \"not on the branch\", and fleet does not.** Both repos ask git whether a known commit is still in a branch's history.\n\n- prs's `HeadHolds` (`internal/pullrequests/fetch.go:94`) runs `git merge-base --is-ancestor <reviewed> <ref>` and returns `err == nil`. Every non-zero exit becomes `false`. That covers exit 1 (not an ancestor), exit 128 (unknown commit), the `readTimeout` kill and a missing git binary. `diffSinceReview` (`internal/board/actions.go:213`) turns that `false` into the whole diff, with the footer \"is gone from the branch, as after a rebase\".\n- fleet's `keepsStart` (`fleet:fix/worktree.go:328`) runs the same command and returns `(bool, error)`. Exit 0 is yes and exit 1 is no. Any other outcome is an error that names the commit: \"ask whether HEAD descends from …\".\n\nfleet's shape is the better one. It keeps a failed question apart from a \"no\" answer, so a timeout never reads as a rebase. prs needs one difference from fleet. A commit git does not have at all is a real \"no\" for prs, because the stand-in test's `rebasedAway` case exits 128 with \"Not a valid commit name\". fleet would call that an error. That edge is already a disagreement between the two copies.\n\n**Fix**: give `HeadHolds` fleet's signature, `(bool, error)`. Read exit 1, and exit 128 with git's \"Not a valid commit name\", as `false, nil`. Read every other failure as an error, and have `diffSinceReview` report it in the footer the way `openDiff` reports a failed base fetch.\n\nThis is weak evidence. One other repo carries the pattern, and it is reported only because the two copies already disagree. No other repo counts commits since a review or opens an interdiff, so `SinceReview` and `NewestReview` have no sibling to match. Two sites that look related agree with this PR. The Claude review rounds post their reviews with `commit_id` set to the head (`claude:src/builder/pr_review_round.py:1092`), so GitHub's review commit and the round marker's head name the same commit. The dotfiles panel (`dotfiles:configs/common/.config/nvim/lua/core/pull-request-panel.lua:107`) places threads by `side` and never reads `base`, so dropping LEFT threads in prs leaves its contract intact."
}
]
}This review does not gate merge.
There was a problem hiding this comment.
PR review — useless-test
Used: Checked out PR 14 in a clone in my directory and read every changed test beside actions.go, rows.go, diff_files.go, detail.go and fetch.go. Ran go vet ./... and go test ./internal/..., both green. The pty tests in cmd/prs did not run here. A temp dir inside my directory makes the stand-in's gh.sock path too long to bind, and a relative TMPDIR makes them time out on the first wait.
Standards: testing.md § "Test behavior the user cares about, not what this commit happened to change"; pull-requests.md § "Tests the diff adds are checked for whether they test the program or the change"; testing.md § "Never assert on rendered output — assert the value it was built from".
Findings: 0 correctness · 2 design · 0 prose
Design and architecture
useless-test.1 cmd/prs/tui_test.go:832 · cmd/prs/tui_test.go:833 · cmd/prs/tui_test.go:834 · cmd/prs/tui_test.go:785
Reach: Every rewording of the i Label in keyTable (internal/board/actions.go) fails it, and so does any later change that binds D. It reaches no behavior a user gets from the key.
The ? help-screen checks assert a typed label and a decision, and should be deleted.
In TestARowCountsTheCommitsSinceItsReviewAndIDiffsOnlyThoseOrTheWholeDiffWithWhy, the block at lines 832–836 does three things:
- It presses
?. - It waits for
i {17}view diff since review. - It fails if any help line starts with
D.
Delete all three, and the sentence "D binds nothing" in the test's doc comment (line 785).
- The
iline is help text.view diff since review: …is a literalLabelinkeyTable(internal/board/actions.go). gotermboard prints it unchanged. The check asserts that the literal equals itself, and it fails on the next rewording. testing.md § "Never assert on rendered output — assert the value it was built from" names help text as rendered output, never the contract. The binding is already pinned: theipresses at lines 797, 799, 822 and 824 reach the fetch,merge-base --is-ancestorandDiffviewOpenassertions. - The
Dcheck asserts that the diff was made.Dis not bound onorigin/main. Main'skeyTablebindsd, the config's window keys,o,y,c,x,spaceandm. The check pins this PR's decision to leaveDunbound, and no user can reach that decision. It also matches a column width in the help screen (D {17}). See testing.md § "Test behavior the user cares about, not what this commit happened to change", the shape "It asserts the diff was made".
No replacement is needed. The i presses already pin the binding, and an unbound key has no behavior to test.
useless-test.2 cmd/prs/tui_test.go:798 · cmd/prs/tui_test.go:800 · cmd/prs/tui_test.go:821 · cmd/prs/tui_test.go:823
Reach: Any rewording of the footer sentences in diffSinceReview and wholeDiff (internal/board/actions.go) fails these waits with nothing wrong. Line 798 is the only check that i refuses on a Bitbucket row.
Four footer waits pin whole sentences the code types, where the values inside them carry the behavior.
In TestARowCountsTheCommitsSinceItsReviewAndIDiffsOnlyThoseOrTheWholeDiffWithWhy, four waits match a full sentence from diffSinceReview or wholeDiff:
- Line 798:
prs reads no reviews from bitbucket, so #4 has no reviewed commit to diff from; d opens the whole diff - Line 800:
closed the diff of widget #12 since its newest review, at 9e8d7c6 - Line 821:
closed the whole diff of gadget #5. It was whole, because no review was submitted on it - Line 823:
closed the whole diff of widget #13. It was whole, because the commit its newest review read, 5ca1ab1, is gone from the branch, as after a rebase
Delete each full-sentence match. The waits are still needed to know nvim has exited, so cut each one down to the interpolated values the code computed. testing.md § "Never assert on rendered output — assert the value it was built from" allows that exception.
- Lines 800, 821 and 823. The
s.ranchecks at 801–806 and 826–830 already pin which diff each row opened:DiffviewOpen 9e8d7c6…..refs/remotes/origin/pr/12for #12, and the three-dot whole diff for #5 and #13. Wait onWaitForPattern("widget #12.*9e8d7c6"),WaitFor("gadget #5")andWaitForPattern("widget #13.*5ca1ab1"). The short reviewed commit is the computed value that reaches the text, and the patterns still require it. - Line 798. This sentence is the only thing pinning a real behavior:
ion a Bitbucket row starts nothing. Keep that behavior and pin it by what ran. Wait onWaitForPattern("bitbucket.*#4"), which matchespr.Providerandpr.Numberas interpolated. Then assert thats.ran(t, "pull/4/head")ands.ran(t, "nvim", "cwd="+s.ledger)are both false. A refusal that still fetched or opened nvim would fail that. The current wording check would not.
Findings as JSON
{
"used": "Checked out PR 14 in a clone in my directory and read every changed test beside actions.go, rows.go, diff_files.go, detail.go and fetch.go. Ran `go vet ./...` and `go test ./internal/...`, both green. The pty tests in cmd/prs did not run here. A temp dir inside my directory makes the stand-in's gh.sock path too long to bind, and a relative TMPDIR makes them time out on the first wait.",
"standards": "testing.md § \"Test behavior the user cares about, not what this commit happened to change\"; pull-requests.md § \"Tests the diff adds are checked for whether they test the program or the change\"; testing.md § \"Never assert on rendered output — assert the value it was built from\".",
"findings": [
{
"number": "useless-test.1",
"section": "design",
"sites": [
"cmd/prs/tui_test.go:832",
"cmd/prs/tui_test.go:833",
"cmd/prs/tui_test.go:834",
"cmd/prs/tui_test.go:785"
],
"reach": "Every rewording of the `i` Label in keyTable (internal/board/actions.go) fails it, and so does any later change that binds `D`. It reaches no behavior a user gets from the key.",
"body": "**The `?` help-screen checks assert a typed label and a decision, and should be deleted.**\n\nIn `TestARowCountsTheCommitsSinceItsReviewAndIDiffsOnlyThoseOrTheWholeDiffWithWhy`, the block at lines 832–836 does three things:\n\n- It presses `?`.\n- It waits for `i {17}view diff since review`.\n- It fails if any help line starts with `D`.\n\nDelete all three, and the sentence \"D binds nothing\" in the test's doc comment (line 785).\n\n- **The `i` line is help text.** `view diff since review: …` is a literal `Label` in `keyTable` (internal/board/actions.go). gotermboard prints it unchanged. The check asserts that the literal equals itself, and it fails on the next rewording. testing.md § \"Never assert on rendered output — assert the value it was built from\" names help text as rendered output, never the contract. The binding is already pinned: the `i` presses at lines 797, 799, 822 and 824 reach the fetch, `merge-base --is-ancestor` and `DiffviewOpen` assertions.\n- **The `D` check asserts that the diff was made.** `D` is not bound on `origin/main`. Main's `keyTable` binds `d`, the config's window keys, `o`, `y`, `c`, `x`, `space` and `m`. The check pins this PR's decision to leave `D` unbound, and no user can reach that decision. It also matches a column width in the help screen (`D {17}`). See testing.md § \"Test behavior the user cares about, not what this commit happened to change\", the shape \"It asserts the diff was made\".\n\nNo replacement is needed. The `i` presses already pin the binding, and an unbound key has no behavior to test."
},
{
"number": "useless-test.2",
"section": "design",
"sites": [
"cmd/prs/tui_test.go:798",
"cmd/prs/tui_test.go:800",
"cmd/prs/tui_test.go:821",
"cmd/prs/tui_test.go:823"
],
"reach": "Any rewording of the footer sentences in diffSinceReview and wholeDiff (internal/board/actions.go) fails these waits with nothing wrong. Line 798 is the only check that `i` refuses on a Bitbucket row.",
"body": "**Four footer waits pin whole sentences the code types, where the values inside them carry the behavior.**\n\nIn `TestARowCountsTheCommitsSinceItsReviewAndIDiffsOnlyThoseOrTheWholeDiffWithWhy`, four waits match a full sentence from `diffSinceReview` or `wholeDiff`:\n\n- Line 798: `prs reads no reviews from bitbucket, so #4 has no reviewed commit to diff from; d opens the whole diff`\n- Line 800: `closed the diff of widget #12 since its newest review, at 9e8d7c6`\n- Line 821: `closed the whole diff of gadget #5. It was whole, because no review was submitted on it`\n- Line 823: `closed the whole diff of widget #13. It was whole, because the commit its newest review read, 5ca1ab1, is gone from the branch, as after a rebase`\n\nDelete each full-sentence match. The waits are still needed to know nvim has exited, so cut each one down to the interpolated values the code computed. testing.md § \"Never assert on rendered output — assert the value it was built from\" allows that exception.\n\n- **Lines 800, 821 and 823.** The `s.ran` checks at 801–806 and 826–830 already pin which diff each row opened: `DiffviewOpen 9e8d7c6…..refs/remotes/origin/pr/12` for #12, and the three-dot whole diff for #5 and #13. Wait on `WaitForPattern(\"widget #12.*9e8d7c6\")`, `WaitFor(\"gadget #5\")` and `WaitForPattern(\"widget #13.*5ca1ab1\")`. The short reviewed commit is the computed value that reaches the text, and the patterns still require it.\n- **Line 798.** This sentence is the only thing pinning a real behavior: `i` on a Bitbucket row starts nothing. Keep that behavior and pin it by what ran. Wait on `WaitForPattern(\"bitbucket.*#4\")`, which matches `pr.Provider` and `pr.Number` as interpolated. Then assert that `s.ran(t, \"pull/4/head\")` and `s.ran(t, \"nvim\", \"cwd=\"+s.ledger)` are both false. A refusal that still fetched or opened nvim would fail that. The current wording check would not."
}
]
}This review does not gate merge.
There was a problem hiding this comment.
PR review — G1
Used: Built the branch and ran go vet and go test ./... -count=1, which were green. Drove the built prs tui in tmux, read-only against real GitHub, with HOME and the XDG dirs set inside my directory and a registry naming a clone of prs. Pressed i on merged #13. Read the GitHub GraphQL reviews of prs #10, #11 and #13 for their review authors and reviewed commits.
Standards: Cited help.md § "Help text never carries a count, version, path or list of what is supported, which the next release makes stale" and pull-requests.md § "Sentences the diff adds are checked for what goes stale, and the finding carries the rewrite". Searches for a rule on a message that promises a wait that never ends found none.
Findings: 2 correctness · 0 design · 1 prose
Correctness
G1.1 internal/board/actions.go:201 · internal/board/actions.go:202 · internal/board/rows.go:609 · internal/board/sections.go:50
Reach: A user moves the cursor into the recently closed group and presses i. The board draws that group under the open rows on every start, and d works on those rows.
i on a merged or closed row says its reviews are still being read, and they never will be.
readsDetail returns false for every recently closed row, so the board never reads that row's detail. diffSinceReview then reaches its !detail.Held case. That case tells the user to wait.
I ran the built board against real GitHub, moved to merged prs #13 and pressed i. The footer said #13's reviews are still being read; i diffs from the commit the newest one read once they land. The side panel beside it said It is merged, so its review, checks, files and comments are not read. Pressing i again more than a minute later gave the same sentence. A user who waits gets nothing.
Fix: in diffSinceReview, check readsDetail(pr) before the !detail.Held case, or branch on detail.Reading, which detailOf already computes. Where the detail is not read, say so: #13 is merged, so prs reads none of its reviews; d opens the whole diff. Keep the "still being read" sentence for a row with Reading set.
G1.2 internal/board/actions.go:211 · internal/board/actions.go:217 · internal/pullrequests/fetch.go:90 · internal/pullrequests/detail.go:93
Reach: unproven — it needs a branch that had its base merged in after its review, for example through GitHub's "Update branch" button or a merge from main pushed by a bot or a person. The fleet's own git hook blocks merges into branches, so the fleet's PRs would not reach it. prs also reads any repo a registry or owners names, and those repos may allow such merges.
After a merge from the base, i shows the base's changes as if the author pushed them.
The chooser asks only whether the head still holds the reviewed commit. A merge of origin/main into the branch keeps that commit as an ancestor, so HeadHolds returns true. i then opens the two-dot RangeFrom diff, <reviewed>..refs/remotes/origin/pr/N. That diff holds every file the merge brought from main. The user meant to read only the author's fixes, and gets main's changes mixed in under the label "since its newest review".
The row is wrong the same way. GitHub's PR commit list leaves out commits that are already on the base, so the row reads 1 commit since review for the merge commit. The diff i opens behind that row can carry many unrelated files.
I tried to stage this in a scratch repo, and the session's hook refused git merge. The two-dot result is plain git behaviour: git diff A..B compares the trees at A and B, whatever B merged.
Fix: after HeadHolds, compare git merge-base <reviewed> origin/<base> with git merge-base <head> origin/<base>. Where they differ, the base moved into the branch. Open the whole diff with the reason its base was merged in since its newest review, as the rebase case already does. The row can say base merged since review when the commit list holds a merge whose second parent is not on the branch.
Prose
G1.3 CLAUDE.md:12 · CLAUDE.md:18 · CLAUDE.md:24 · CLAUDE.md:94 · CLAUDE.md:111 · README.md:74 · README.md:228 · cmd/prs/diff-threads.schema.json:4 · cmd/prs/diff-threads.schema.json:5 · cmd/prs/diff-threads.schema.json:15 · internal/board/board.go:63 · internal/board/actions.go:375
Twelve sentences list the diff keys by letter, so adding one more diff key means editing all twelve.
The diff edits sentences such as the one \d` or `i` fetched, review threads `d` and `i` write for nvim, `d`, `i` and the diff share, the nvim d or i startsanddiffed is the head commit d or i last fetched. Each one lists the current callers of openDiff`. Every earlier edit added one more letter, and the next diff key will add another, for example a range-diff key for the rebased case that the body considers. help.md § "Help text never carries a count, version, path or list of what is supported, which the next release makes stale" covers the schema descriptions and the README. pull-requests.md § "Sentences the diff adds are checked for what goes stale" names the list of callers as the shape to avoid.
The diff already shows the fix once. pinFor's footer reason changed from d fetched it to its diff in nvim fetched it.
Rewrite: name what the keys share rather than the letters. Examples: the head a diff in nvim last fetched, the review threads a diff in nvim is handed, what the diff keys and the Diff section share, and the nvim a diff key starts. Keep the letters only in the README paragraph and the help text that introduce each key.
Findings as JSON
{
"used": "Built the branch and ran `go vet` and `go test ./... -count=1`, which were green. Drove the built `prs tui` in tmux, read-only against real GitHub, with HOME and the XDG dirs set inside my directory and a registry naming a clone of prs. Pressed `i` on merged #13. Read the GitHub GraphQL reviews of prs #10, #11 and #13 for their review authors and reviewed commits.",
"standards": "Cited help.md § \"Help text never carries a count, version, path or list of what is supported, which the next release makes stale\" and pull-requests.md § \"Sentences the diff adds are checked for what goes stale, and the finding carries the rewrite\". Searches for a rule on a message that promises a wait that never ends found none.",
"findings": [
{
"number": "G1.1",
"section": "correctness",
"sites": [
"internal/board/actions.go:201",
"internal/board/actions.go:202",
"internal/board/rows.go:609",
"internal/board/sections.go:50"
],
"reach": "A user moves the cursor into the `recently closed` group and presses `i`. The board draws that group under the open rows on every start, and `d` works on those rows.",
"body": "**`i` on a merged or closed row says its reviews are still being read, and they never will be.**\n\n`readsDetail` returns false for every `recently closed` row, so the board never reads that row's detail. `diffSinceReview` then reaches its `!detail.Held` case. That case tells the user to wait.\n\nI ran the built board against real GitHub, moved to merged prs #13 and pressed `i`. The footer said `#13's reviews are still being read; i diffs from the commit the newest one read once they land`. The side panel beside it said `It is merged, so its review, checks, files and comments are not read`. Pressing `i` again more than a minute later gave the same sentence. A user who waits gets nothing.\n\nFix: in `diffSinceReview`, check `readsDetail(pr)` before the `!detail.Held` case, or branch on `detail.Reading`, which `detailOf` already computes. Where the detail is not read, say so: `#13 is merged, so prs reads none of its reviews; d opens the whole diff`. Keep the \"still being read\" sentence for a row with `Reading` set."
},
{
"number": "G1.2",
"section": "correctness",
"sites": [
"internal/board/actions.go:211",
"internal/board/actions.go:217",
"internal/pullrequests/fetch.go:90",
"internal/pullrequests/detail.go:93"
],
"reach": "unproven — it needs a branch that had its base merged in after its review, for example through GitHub's \"Update branch\" button or a merge from main pushed by a bot or a person. The fleet's own git hook blocks merges into branches, so the fleet's PRs would not reach it. prs also reads any repo a registry or `owners` names, and those repos may allow such merges.",
"body": "**After a merge from the base, `i` shows the base's changes as if the author pushed them.**\n\nThe chooser asks only whether the head still holds the reviewed commit. A merge of `origin/main` into the branch keeps that commit as an ancestor, so `HeadHolds` returns true. `i` then opens the two-dot `RangeFrom` diff, `<reviewed>..refs/remotes/origin/pr/N`. That diff holds every file the merge brought from main. The user meant to read only the author's fixes, and gets main's changes mixed in under the label \"since its newest review\".\n\nThe row is wrong the same way. GitHub's PR commit list leaves out commits that are already on the base, so the row reads `1 commit since review` for the merge commit. The diff `i` opens behind that row can carry many unrelated files.\n\nI tried to stage this in a scratch repo, and the session's hook refused `git merge`. The two-dot result is plain git behaviour: `git diff A..B` compares the trees at A and B, whatever B merged.\n\nFix: after `HeadHolds`, compare `git merge-base <reviewed> origin/<base>` with `git merge-base <head> origin/<base>`. Where they differ, the base moved into the branch. Open the whole diff with the reason `its base was merged in since its newest review`, as the rebase case already does. The row can say `base merged since review` when the commit list holds a merge whose second parent is not on the branch."
},
{
"number": "G1.3",
"section": "prose",
"sites": [
"CLAUDE.md:12",
"CLAUDE.md:18",
"CLAUDE.md:24",
"CLAUDE.md:94",
"CLAUDE.md:111",
"README.md:74",
"README.md:228",
"cmd/prs/diff-threads.schema.json:4",
"cmd/prs/diff-threads.schema.json:5",
"cmd/prs/diff-threads.schema.json:15",
"internal/board/board.go:63",
"internal/board/actions.go:375"
],
"reach": "",
"body": "**Twelve sentences list the diff keys by letter, so adding one more diff key means editing all twelve.**\n\nThe diff edits sentences such as `the one \\`d\\` or \\`i\\` fetched`, `review threads \\`d\\` and \\`i\\` write for nvim`, `\\`d\\`, \\`i\\` and the diff share`, `the nvim d or i starts` and `diffed is the head commit d or i last fetched`. Each one lists the current callers of `openDiff`. Every earlier edit added one more letter, and the next diff key will add another, for example a range-diff key for the rebased case that the body considers. help.md § \"Help text never carries a count, version, path or list of what is supported, which the next release makes stale\" covers the schema descriptions and the README. pull-requests.md § \"Sentences the diff adds are checked for what goes stale\" names the list of callers as the shape to avoid.\n\nThe diff already shows the fix once. `pinFor`'s footer reason changed from `d fetched it` to `its diff in nvim fetched it`.\n\nRewrite: name what the keys share rather than the letters. Examples: `the head a diff in nvim last fetched`, `the review threads a diff in nvim is handed`, `what the diff keys and the Diff section share`, and `the nvim a diff key starts`. Keep the letters only in the README paragraph and the help text that introduce each key."
}
]
}This review does not gate merge.
There was a problem hiding this comment.
PR review — synthesis
Used: the 13 findings of round 1, from 8 review(s), and nothing else
Standards: none, because this step reads only the findings
Findings: 0 correctness · 1 design · 0 prose
Design and architecture
synthesis.1 explains fleet-patterns.1 · failure-paths.1
HeadHolds in internal/pullrequests/fetch.go returns err == nil. Every git failure, such as a timeout, a missing binary or a broken checkout, reads as "not an ancestor". diffSinceReview then tells the user the branch was rebased. Both findings ask for a (bool, error) signature.
One defect, raised more than once
fleet-patterns.1 · failure-paths.1: HeadHolds in internal/pullrequests/fetch.go returns err == nil, so any git failure reads as a rebase in the diffSinceReview footer.
Shapes as JSON
{
"shapes": [
{
"number": "synthesis.1",
"section": "design",
"explains": [
"fleet-patterns.1",
"failure-paths.1"
],
"body": "`HeadHolds` in internal/pullrequests/fetch.go returns `err == nil`. Every git failure, such as a timeout, a missing binary or a broken checkout, reads as \"not an ancestor\". `diffSinceReview` then tells the user the branch was rebased. Both findings ask for a `(bool, error)` signature."
}
],
"duplicates": [
{
"defect": "`HeadHolds` in internal/pullrequests/fetch.go returns `err == nil`, so any git failure reads as a rebase in the `diffSinceReview` footer.",
"findings": [
"fleet-patterns.1",
"failure-paths.1"
]
}
]
}This review does not gate merge.
PR review round 1Ran: G1, useless-test, code-architecture, fleet-patterns, system-design, stale-prose, failure-paths, contracts, synthesis
Round record{
"grammar": 6,
"round": 1,
"repo": "datapointchris/prs",
"pr": 14,
"head": "56edb72eeb0222b530976fcba5269bc9d182b7f8",
"experiment": "none",
"dispatch": "20261108T152737Z-pr-review",
"roster": "9fa7d955aca301b4f2edd5447426a0d838639046",
"harness": "6c387842ffdad77075f35f64a0c672d7ca178c86",
"machine": "queue",
"started": "2026-11-08T15:28:04Z",
"finished": "2026-11-08T15:35:18Z",
"reviewers": [
{
"label": "G1",
"agent": "pr-reviewer",
"model": "opus",
"effort": "high",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/general.md",
"prompt_sha": "00271016dd7d",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 338,
"tokens": {
"input": 50,
"cache_write": 61476,
"cache_read": 1109901,
"output": 15157
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 50,
"cache_write": 61476,
"cache_read": 1109901,
"output": 15157
}
},
"session": "01a1bbc5-c6ce-7a63-a77c-7d6109993b17",
"worklog": "1330.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/14#pullrequestreview-5818469670",
"review_id": 5818469670,
"findings": {
"correctness": 2,
"design": 0,
"prose": 1
},
"numbers": [
"G1.1",
"G1.2",
"G1.3"
],
"rules": {},
"head": "56edb72eeb0222b530976fcba5269bc9d182b7f8",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr14-round1-20261108T152737Z/records",
"started": "2026-11-08T15:28:05Z",
"finished": "2026-11-08T15:33:50Z",
"account": "live",
"api_key_source": "none",
"machine": "queue"
},
{
"label": "useless-test",
"agent": "pr-useless-test-reviewer",
"model": "opus",
"effort": "high",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/useless-test.md",
"prompt_sha": "f0b52bbf8a48",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 198,
"tokens": {
"input": 26,
"cache_write": 39486,
"cache_read": 440565,
"output": 9376
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 26,
"cache_write": 39486,
"cache_read": 440565,
"output": 9376
}
},
"session": "01a1bbc5-ca50-7ddf-8dcd-fd5e5f56cadd",
"worklog": "1330.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/14#pullrequestreview-5818461806",
"review_id": 5818461806,
"findings": {
"correctness": 0,
"design": 2,
"prose": 0
},
"numbers": [
"useless-test.1",
"useless-test.2"
],
"rules": {},
"head": "56edb72eeb0222b530976fcba5269bc9d182b7f8",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr14-round1-20261108T152737Z/records",
"started": "2026-11-08T15:28:04Z",
"finished": "2026-11-08T15:31:27Z",
"account": "live",
"api_key_source": "none",
"machine": "queue"
},
{
"label": "code-architecture",
"agent": "pr-code-architecture-reviewer",
"model": "opus",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/persona.md",
"prompt_sha": "283f4014efc3",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 57,
"tokens": {
"input": 18,
"cache_write": 28146,
"cache_read": 192750,
"output": 3582
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 18,
"cache_write": 28146,
"cache_read": 192750,
"output": 3582
}
},
"session": "01a1bbc5-d48b-7d5d-b7d7-50096af65f19",
"worklog": "1330.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/14#pullrequestreview-5818455560",
"review_id": 5818455560,
"findings": {
"correctness": 0,
"design": 0,
"prose": 0
},
"numbers": [],
"rules": {},
"head": "56edb72eeb0222b530976fcba5269bc9d182b7f8",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr14-round1-20261108T152737Z/records",
"started": "2026-11-08T15:28:27Z",
"finished": "2026-11-08T15:29:30Z",
"account": "live",
"api_key_source": "none",
"machine": "queue"
},
{
"label": "fleet-patterns",
"agent": "pr-fleet-patterns-reviewer",
"model": "opus",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/persona.md",
"prompt_sha": "efa99c642e6d",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 153,
"tokens": {
"input": 52,
"cache_write": 48953,
"cache_read": 930177,
"output": 9358
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 52,
"cache_write": 48953,
"cache_read": 930177,
"output": 9358
}
},
"session": "01a1bbc6-6020-79ab-a82f-cab91a7e6a26",
"worklog": "1330.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/14#pullrequestreview-5818461734",
"review_id": 5818461734,
"findings": {
"correctness": 0,
"design": 1,
"prose": 0
},
"numbers": [
"fleet-patterns.1"
],
"rules": {},
"head": "56edb72eeb0222b530976fcba5269bc9d182b7f8",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr14-round1-20261108T152737Z/records",
"started": "2026-11-08T15:28:49Z",
"finished": "2026-11-08T15:31:26Z",
"account": "live",
"api_key_source": "none",
"machine": "queue"
},
{
"label": "system-design",
"agent": "pr-system-design-reviewer",
"model": "sonnet",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/persona.md",
"prompt_sha": "bc2eb8615f41",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 26,
"tokens": {
"input": 8,
"cache_write": 11989,
"cache_read": 49913,
"output": 905
},
"tokens_from": "result",
"tokens_by_model": {
"claude-sonnet-5-5": {
"input": 8,
"cache_write": 11989,
"cache_read": 49913,
"output": 905
}
},
"session": "01a1bbc7-5e7e-7bb4-9f47-e128bbc46110",
"worklog": "1330.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/14#pullrequestreview-5818460741",
"review_id": 5818460741,
"findings": {
"correctness": 0,
"design": 0,
"prose": 0
},
"numbers": [],
"rules": {},
"head": "56edb72eeb0222b530976fcba5269bc9d182b7f8",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr14-round1-20261108T152737Z/records",
"started": "2026-11-08T15:30:37Z",
"finished": "2026-11-08T15:31:07Z",
"account": "live",
"api_key_source": "none",
"machine": "queue"
},
{
"label": "stale-prose",
"agent": "pr-stale-prose-reviewer",
"model": "opus",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/persona.md",
"prompt_sha": "4359b874a436",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 103,
"tokens": {
"input": 18,
"cache_write": 26594,
"cache_read": 209972,
"output": 6377
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 18,
"cache_write": 26594,
"cache_read": 209972,
"output": 6377
}
},
"session": "01a1bbc5-c803-7dca-bf66-78e436d3d830",
"worklog": "1330.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/14#pullrequestreview-5818456698",
"review_id": 5818456698,
"findings": {
"correctness": 0,
"design": 0,
"prose": 3
},
"numbers": [
"stale-prose.1",
"stale-prose.2",
"stale-prose.3"
],
"rules": {},
"head": "56edb72eeb0222b530976fcba5269bc9d182b7f8",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr14-round1-20261108T152737Z/records",
"started": "2026-11-08T15:28:06Z",
"finished": "2026-11-08T15:29:53Z",
"account": "live",
"api_key_source": "none",
"machine": "queue"
},
{
"label": "failure-paths",
"agent": "pr-failure-paths-reviewer",
"model": "sonnet",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/persona.md",
"prompt_sha": "4239ed2be477",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 33,
"tokens": {
"input": 10,
"cache_write": 15418,
"cache_read": 74243,
"output": 1652
},
"tokens_from": "result",
"tokens_by_model": {
"claude-sonnet-5-5": {
"input": 10,
"cache_write": 15418,
"cache_read": 74243,
"output": 1652
}
},
"session": "01a1bbc5-d1f1-7d73-8a13-50e93eb7143d",
"worklog": "1330.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/14#pullrequestreview-5818454142",
"review_id": 5818454142,
"findings": {
"correctness": 1,
"design": 0,
"prose": 0
},
"numbers": [
"failure-paths.1"
],
"rules": {},
"head": "56edb72eeb0222b530976fcba5269bc9d182b7f8",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr14-round1-20261108T152737Z/records",
"started": "2026-11-08T15:28:27Z",
"finished": "2026-11-08T15:29:04Z",
"account": "live",
"api_key_source": "none",
"machine": "queue"
},
{
"label": "contracts",
"agent": "pr-contracts-reviewer",
"model": "opus",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/persona.md",
"prompt_sha": "3cf737a7c111",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 155,
"tokens": {
"input": 48,
"cache_write": 40029,
"cache_read": 800982,
"output": 9266
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 48,
"cache_write": 40029,
"cache_read": 800982,
"output": 9266
}
},
"session": "01a1bbc5-d7b4-7fe0-a20d-6a02b6a693ce",
"worklog": "1330.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/14#pullrequestreview-5818460698",
"review_id": 5818460698,
"findings": {
"correctness": 0,
"design": 3,
"prose": 0
},
"numbers": [
"contracts.1",
"contracts.2",
"contracts.3"
],
"rules": {},
"head": "56edb72eeb0222b530976fcba5269bc9d182b7f8",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr14-round1-20261108T152737Z/records",
"started": "2026-11-08T15:28:27Z",
"finished": "2026-11-08T15:31:07Z",
"account": "live",
"api_key_source": "none",
"machine": "queue"
},
{
"label": "synthesis",
"agent": null,
"model": "sonnet",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/synthesis.md",
"prompt_sha": "8832c4b84e09",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 16,
"tokens": {
"input": 2,
"cache_write": 9527,
"cache_read": 1499,
"output": 311
},
"tokens_from": "result",
"tokens_by_model": {
"claude-sonnet-5-5": {
"input": 2,
"cache_write": 9527,
"cache_read": 1499,
"output": 311
}
},
"session": "01a1bbcb-a758-768c-8368-996fa40573be",
"worklog": "1330.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/14#pullrequestreview-5818474644",
"review_id": 5818474644,
"findings": {
"correctness": 0,
"design": 1,
"prose": 0
},
"numbers": [
"synthesis.1"
],
"rules": {},
"head": "56edb72eeb0222b530976fcba5269bc9d182b7f8",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr14-round1-20261108T152737Z/records",
"started": "2026-11-08T15:34:51Z",
"finished": "2026-11-08T15:35:18Z",
"account": "live",
"api_key_source": "none",
"machine": "queue"
}
],
"rules": {
"proposed": 0,
"keys": [],
"unfiled": []
},
"kept": []
} |
…s one i asks git where the reviewed commit and the fetched head each meet origin/<base>. Where the two differ, the base was merged into the branch since the review, and a two-dot diff from there would show the base's changes as the branch's, so i opens the whole diff and says why. HeadHolds tells a head that does not hold the reviewed commit from git failing to say. A rev-parse first finds a commit missing from the checkout, and merge-base --is-ancestor answers for one present. Any other exit opens nothing, and the footer names git's own line, rather than claiming a rebase. i on a recently closed row, or one no read lists, says prs reads none of its reviews, rather than that they are still being read. The threads file gains diff, naming whole or since_review, and threads_left_out, counting the LEFT threads a since-review diff leaves out, so base reads one way for each. The README says a window action on a key prs binds is refused and stops prs tui, and names ? and prs config show as where the bound keys are read. prs tui's help says --once and --json read no pull request's detail. Prose says the diff keys rather than listing their letters. The since-review test waits on the values each footer line carries, and pins i's refusal on a Bitbucket row by the fetch and nvim it does not run. Agent-Name: claude-code-unattended Agent-Session: 3689c9b4-3208-4138-b764-71b0ed5b83a7 Agent-Run: 01a1bbc5-b8aa-703f-afe7-8113cced5d40 Worklog: 1330.1
After a review, a fixer pushes its fixes to the same branch, and whoever reads the pull request next needs only what that push changed. The board gave no sign that a reviewed branch had moved, and
dopened only the whole diff. GitHub records the commit each review was submitted against, and the detail read already fetches it beside the branch's commits, so this costs no read of its own.This adds a field to each row's second line that counts the commits since the newest review, and a key,
i, that opens only those commits in nvim's diff.What to look at
internal/board/actions.go:diffSinceReviewandopenDiff. Check the order of the cases indiffSinceReview's chooser, which runs after the fetch. The fetched head is the truth, so a branch force-pushed after the detail read still opens the whole diff.internal/pullrequests/fetch.go:HeadHoldsandBaseMergedSince. Check that each git exit is read as yes, no or failure, and that only a failure stops nvim.internal/pullrequests/detail.go:NewestReviewandSinceReview. Check the case where the reviewed commit is missing from a commit list the read cut at 100, which reads as unknown rather than rebased.internal/board/diff_files.go: the since-review threads file leaves out eachLEFTthread and counts them. Check that againstcmd/prs/diff-threads.schema.jsonand the nvim panel that places threads by side.cmd/prs/tui_test.go:TestARowCountsTheCommitsSinceItsReviewAndIDiffsOnlyThoseOrTheWholeDiffWithWhy, andwithPushesOf, which stages the cases on the stand-in GitHub.51efac4is a separate fix to an existing test's wait, which raced under the parallel suite.How it was verified
go test ./... -count=1andgolangci-lint run, green, then the pty suite twice more in a row, green.irunsDiffviewOpen 9e8d7c6…..refs/remotes/origin/pr/12, and its threads file readssince_reviewwith oneLEFTthread left out.iopens the whole diff.iopens the whole diff and names that commit.ifetches nothing and starts no nvim.HeadHoldsandBaseMergedSinceran once against a real git repository in a throwaway test, then deleted. It covered six branch shapes: linear with the base moved, base merged in, base merged in and moved again, reviewed after a merge, rebased, and a commit the checkout lacks. Outside a repository, both returned git'snot a git repositoryline as an error.What changes
2 commits since reviewor1 commit since reviewafter its newest review and its Claude round. It readsrebased since reviewwhere the branch does not hold the reviewed commit. A row nobody reviewed, or whose head is the reviewed commit, reads nothing there.ion a row fetches its head asddoes and opens nvim's diff from the reviewed commit to the head.iopens the whole diff where no review was submitted, where the head does not hold the reviewed commit, or where the base was merged into the branch since the review. The footer says why once nvim exits.iopens nothing and the footer names git's failure.irefuses on a Bitbucket row and on a recently closed one, because prs reads neither's reviews, and before a GitHub row's detail lands.iwrites reads"diff": "since_review", names the reviewed commit asbase, holds only theRIGHTthreads, and counts the rest inthreads_left_out. The onedwrites reads"diff": "whole"and"threads_left_out": 0. The document file beside it still lists every thread.ipins the headifetched, as one afterddoes.iis prs's own key now. A config whose window action bindsiis refused, andprs tuidoes not start until the config moves it.Decisions, and what they rejected
ifor the key — it stands for interdiff, the diff between two versions of one change.Ddeletes in other terminal tools, andf,s,e,n,u,MandCmean other things there. No common tool binds a letter to this diff, so none was copied.ifetches. Asking the detail alone would diff from a commit a force-push since the read took off the branch.git rev-parse --verify --quiet <commit>^{commit}finds a commit missing from the checkout, andgit merge-base --is-ancestoranswers for one present.git merge-base <reviewed> origin/<base>andgit merge-base <head> origin/<base>differ only where the base came into the branch after the review.git range-diffwould compare the two versions of the commits across a rebase, but the whole diff with the reason on the footer is the agreed behavior.LEFTthreads fori— aLEFTthread's line counts in the base branch's copy, and the left pane of this diff is the reviewed commit's copy, so an nvim config placing it there would put it on the wrong line.diffsays which left sidebaseis, andthreads_left_outsays how many were dropped.DiffviewOpen <reviewed>..<head>with no check that the head holds the reviewed commit. On a rebased branch git either refuses the unknown commit or diffs two unrelated trees, so it is ruled out.What this does not do
prs listor either--jsonoutput, because neither reads a pull request's detail.prs tui --helpnow says so.ithen names the merge as why it opened the whole diff.The review
Round 1: #14 (comment). Every fix is in
3f2cb47.ion a recently closed row, or one no read lists, says prs reads none of its reviews and points atd. "Still being read" shows only while a read is under way.BaseMergedSincecompares where the reviewed commit and the head each meetorigin/<base>, andiopens the whole diff where they differ. Its reach is unproven, so it has no committed test. A throwaway test against real git covered it. The row'sbase merged since reviewwas not built: the detail read holds no commit parents, and the count of one pushed commit is true.dandi, in CLAUDE.md, README, the threads schema and the code comments.sinceReviewCell's comment say a blank means prs could not tell, or there is nothing to count.keyTable's comment and CLAUDE.md drop the claims about what other tools bind. Each says only thatDis left unbound because it deletes elsewhere.?overlay checks and theDcheck are deleted, and so is "D binds nothing" in the test's comment.bitbucket.*#4,diff of widget #12.*9e8d7c6,whole diff of gadget #5andwhole diff of widget #13.*5ca1ab1. The Bitbucket refusal is pinned by the fetch and nvim it does not run.HeadHoldsreturns(bool, error). A missing commit and--is-ancestor's exit 1 read as no. Any other exit opens nothing and names git's line on the footer.diffandthreads_left_out, both required in the schema, sobasereads one way for each kind of diff.prs tui. It names?andprs config showas where the bound keys are read, rather than listing letters a release can add to. This body namesias a key a config can no longer bind. Rejected: skipping a clashing window action with a footer warning, because the action would vanish where the refusal names it.prs tui --helpsays--onceand--jsonread no pull request's detail, and names the review round, the commits since review and the document sections that leave out.Session: 3689c9b4-3208-4138-b764-71b0ed5b83a7