Repository navigation
feat: draw each pull request as two lines, ruled apart and spread across the width - #13
Conversation
…oss the width The board drew each pull request on one line, with its branch and checkout hidden until f showed them. Each now takes two lines, as the first version of prs drew them. The first holds the state, the title, the diff, the age and the branch, spread across the width. The second holds the checks under the state, then the checkout, the number, the review, the newest Claude round and the app that opened it. A dotted rule sits between rows. The board keeps a margin of 12 columns a side, narrower under 150 columns, with rules under the titles and over the keys. The cursor's two lines are bold on the selection, and rows draw no chevron, because every row opens its document. prs list keeps its one-line table. gotermboard is pinned at the commit that adds this look. Agent-Name: claude-code-unattended Agent-Session: 69e19fe4-568c-41ef-9af1-316d086cd270 Agent-Run: 01a1b9f7-d1e5-73a0-a189-4f76a78eeb51 Worklog: 2568.1
There was a problem hiding this comment.
PR review — failure-paths
Used: Cloned and checked out PR 13 at 2ed553f. Read the diff for all 10 files and searched the added lines for exec, http, file-write and tmux calls. I did not build or run it.
Standards: No rule cited. No external effect was added, so I ran no fleet standards search.
Findings: 0 correctness · 0 design · 0 prose
No findings.
Findings as JSON
{
"used": "Cloned and checked out PR 13 at 2ed553f. Read the diff for all 10 files and searched the added lines for exec, http, file-write and tmux calls. I did not build or run it.",
"standards": "No rule cited. No external effect was added, so I ran no `fleet standards search`.",
"findings": []
}This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review — system-design
Used: Checked out PR 13 and listed its changed files: CLAUDE.md, README.md, cmd/prs/tui.go, cmd/prs/tui_test.go, internal/board/board.go, rows.go, sections.go and table.go. I did not build or run it.
Standards: I ran no fleet standards search. The change has no infrastructure, data or API surface to look up.
Findings: 0 correctness · 0 design · 0 prose
No findings.
Findings as JSON
{
"used": "Checked out PR 13 and listed its changed files: CLAUDE.md, README.md, cmd/prs/tui.go, cmd/prs/tui_test.go, internal/board/board.go, rows.go, sections.go and table.go. I did not build or run it.",
"standards": "I ran no `fleet standards search`. The change has no infrastructure, data or API surface to look up.",
"findings": []
}This review does not gate merge.
There was a problem hiding this comment.
PR review — fleet-patterns
Used: I cloned prs at PR 13's head (2ed553f), plus gotermboard at the pinned 374b01f and datapointchris/projects, the other gotermboard consumer. I read the diff and the library's LookBoard/Look/margin code. In a scratch Go program in my directory I ran prs's CutPrefix(path, home) rooting against home /home/ann on two paths, /home/ann/widget and /home/ann2/widget. It printed ~2/widget for the second.
Standards: cli-design.md § "A path a person reads writes the home directory as ~" covers the finding. code-quality.md § "Ask whatever owns a fact… never duplicate its logic at a second site" was checked. No rule covers the margin tapering or the two-line row, and only prs uses either.
Findings: 0 correctness · 1 design · 0 prose
Design and architecture
fleet-patterns.1 internal/board/rows.go:418 · datapointchris/projects:cli/data_source.go:239
Reach: Every checkout prs tui draws goes through whereCells. The paths come from the registry, and the TUI test now sets HOME so that ~/widget is what gets checked. A checkout in a sibling of home, such as /home/chris2/x under HOME=/home/chris, draws as ~2/x. Each repo keeps its own copy of this logic. Any later fix to one copy, such as the segment boundary or the path == home case, will not reach the others.
The new ~ helper in whereCells cuts home without the segment boundary that projects' homeAsTilde checks.
This PR moves ~-rooting into a new whereCells and makes the checkout always visible on the second line. Before, it only showed under f. The rooting is still strings.CutPrefix(where, home) followed by "~" + rest. projects solves the same problem in homeAsTilde (cli/data_source.go:239). That version returns ~ for home itself and cuts only at home + string(filepath.Separator). It also returns the path unchanged when home is empty.
I ran prs's logic with home /home/ann:
/home/ann/widgetgives~/widget./home/ann2/widgetgives~2/widget.
projects' version leaves the second path as it is. The two copies already disagree, and projects' version is correct.
cli-design.md § "A path a person reads writes the home directory as ~" covers this case. It requires the cut to fall at a path-segment boundary, and it names forge, syncer, safekeep and fleet as repos that each wrote a copy and disagreed. prs is now one more.
Fix: follow projects' homeAsTilde. Use the separator-bounded CutPrefix, handle path == home, and skip the cut when home is empty. A better fix moves one helper into the Go library prs and projects both import, gotermboard or its style package, and calls it from both repos. I could only search projects and gotermboard on this pod. The other four repos the rule names are unchecked, so this rests on one other repo and the rule.
Findings as JSON
{
"used": "I cloned prs at PR 13's head (2ed553f), plus gotermboard at the pinned 374b01f and datapointchris/projects, the other gotermboard consumer. I read the diff and the library's `LookBoard`/`Look`/margin code. In a scratch Go program in my directory I ran prs's `CutPrefix(path, home)` rooting against home `/home/ann` on two paths, `/home/ann/widget` and `/home/ann2/widget`. It printed `~2/widget` for the second.",
"standards": "cli-design.md § \"A path a person reads writes the home directory as ~\" covers the finding. code-quality.md § \"Ask whatever owns a fact… never duplicate its logic at a second site\" was checked. No rule covers the margin tapering or the two-line row, and only prs uses either.",
"findings": [
{
"number": "fleet-patterns.1",
"section": "design",
"sites": [
"internal/board/rows.go:418",
"datapointchris/projects:cli/data_source.go:239"
],
"reach": "Every checkout `prs tui` draws goes through `whereCells`. The paths come from the registry, and the TUI test now sets `HOME` so that `~/widget` is what gets checked. A checkout in a sibling of home, such as `/home/chris2/x` under `HOME=/home/chris`, draws as `~2/x`. Each repo keeps its own copy of this logic. Any later fix to one copy, such as the segment boundary or the `path == home` case, will not reach the others.",
"body": "**The new `~` helper in `whereCells` cuts home without the segment boundary that projects' `homeAsTilde` checks.**\n\nThis PR moves `~`-rooting into a new `whereCells` and makes the checkout always visible on the second line. Before, it only showed under `f`. The rooting is still `strings.CutPrefix(where, home)` followed by `\"~\" + rest`. projects solves the same problem in `homeAsTilde` (`cli/data_source.go:239`). That version returns `~` for home itself and cuts only at `home + string(filepath.Separator)`. It also returns the path unchanged when home is empty.\n\nI ran prs's logic with home `/home/ann`:\n\n- `/home/ann/widget` gives `~/widget`.\n- `/home/ann2/widget` gives `~2/widget`.\n\nprojects' version leaves the second path as it is. The two copies already disagree, and projects' version is correct.\n\ncli-design.md § \"A path a person reads writes the home directory as `~`\" covers this case. It requires the cut to fall at a path-segment boundary, and it names forge, syncer, safekeep and fleet as repos that each wrote a copy and disagreed. prs is now one more.\n\nFix: follow projects' `homeAsTilde`. Use the separator-bounded `CutPrefix`, handle `path == home`, and skip the cut when home is empty. A better fix moves one helper into the Go library prs and projects both import, gotermboard or its style package, and calls it from both repos. I could only search projects and gotermboard on this pod. The other four repos the rule names are unchecked, so this rests on one other repo and the rule."
}
]
}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 13 at 2ed553f. I read rows.go, table.go, board.go, sections.go, tui.go, README.md and CLAUDE.md at the head. go build ./... and go vet ./internal/board/ passed. go doc showed that gotermboard defines PanelFrom = 160 and the new Look fields. A search turned up no leftover uses of rowColumns, WrapLast, drawnColumns or identity(.
Standards: Two rules apply: code-quality.md § "Ask whatever owns a fact, the function or enum that computes it, never duplicate its logic at a second site", and documentation.md § "A doc points at the source instead of restating its content or constants, and the pointer is written with the source open".
Findings: 0 correctness · 2 design · 0 prose
Design and architecture
code-architecture.1 internal/board/rows.go:180 · internal/board/rows.go:187 · internal/board/rows.go:195 · internal/board/rows.go:373 · internal/board/rows.go:376 · internal/board/rows.go:379 · internal/board/rows.go:398 · internal/board/rows.go:402 · internal/board/rows.go:404 · internal/board/rows.go:407 · internal/board/rows.go:411 · internal/board/table.go:33
Reach: Two changes are coming. One is the next field on a pull request, such as the base branch this PR moved into the document. The other is a restyle of the number, stack or bot mark. Today each must edit boardColumns/secondLine and listColumns/titleLine together. This PR already did that for one field. The CLAUDE round moved out of a single rowColumns entry into both an optional listColumns column and an inline test in secondLine. CLAUDE.md:136-139 now tells the next author to put a new field in secondLine or boardColumns, and it says nothing about listColumns.
Two layouts format the number, stack and bot marks inline
This PR splits the single rowColumns into two layouts.
- Board:
boardColumnsplussecondLine. prs list:listColumnsplustitleLine.
Shared cell builders cover these fields: stateCell, checksCell, reviewCell, claudeCell, ageCell and diffCell. Three other fields have no builder. Each layout formats them inline, and the two copies already disagree:
- Number:
"%s #%d"at rows.go:195 and"%s #%d"at rows.go:402. - Stack parent:
" %s #%d"at rows.go:376 and" %s #%d"at rows.go:404. - Bot:
" "+botGlyph+" bot "at rows.go:379 andbotGlyph+" bot "at rows.go:411.
The two layouts also decide differently whether the round shows. listColumns uses optional: true at rows.go:187. secondLine tests claude.Text != "" at rows.go:407.
The layouts are meant to differ in order and placement. They are not meant to differ in how a field is formatted. code-quality.md § "Ask whatever owns a fact, the function or enum that computes it, never duplicate its logic at a second site" applies here.
Fix: Add numberCell(pr), stackCell(parent) and botCell(pr) beside checksCell and reviewCell. Call them from listColumns, titleLine and secondLine. Each layout then only chooses where a field sits, and spacing comes from the layout's gap. A new field becomes one builder plus one placement per layout. The formatting is written once.
code-architecture.2 README.md:149 · README.md:150 · README.md:151 · CLAUDE.md:195 · CLAUDE.md:196 · internal/board/rows.go:434 · internal/board/rows.go:435
Reach: Two changes would leave the docs wrong. One is a change to sideMargin or marginFrom in rows.go. The other is a gotermboard release that moves PanelFrom, and the PR body already says the pin moves to a new gotermboard release before merge. Either change makes README's "160" and "184" and CLAUDE.md's "184" wrong. Nothing in the build or tests ties these numbers back to their source.
The docs restate the panel threshold that gotermboard owns, plus a number derived from it
The side panel's threshold is gotermboard.PanelFrom = 160. gotermboard owns that constant. README.md:150 copies it as "160". The PR also writes the derived value "184" into README.md:151 and CLAUDE.md:196. 184 comes from PanelFrom + 2*sideMargin. The margin numbers "12" and "150" in README.md:149 copy sideMargin and marginFrom from rows.go:434-435.
The PR rests on a gotermboard pre-release pin. That makes PanelFrom the value most likely to move under these docs. documentation.md § "A doc points at the source instead of restating its content or constants" applies here.
Fix: Have CLAUDE.md name the computation: the panel shows from PanelFrom plus both of Look's sides. Drop the literal 184. If the README must keep a number, a test can check it. That test would assert gotermboard.PanelFrom + 2*sideMargin equals the value the README states.
Findings as JSON
{
"used": "I cloned the repo and checked out PR 13 at 2ed553f. I read rows.go, table.go, board.go, sections.go, tui.go, README.md and CLAUDE.md at the head. `go build ./...` and `go vet ./internal/board/` passed. `go doc` showed that gotermboard defines `PanelFrom = 160` and the new `Look` fields. A search turned up no leftover uses of `rowColumns`, `WrapLast`, `drawnColumns` or `identity(`.",
"standards": "Two rules apply: code-quality.md § \"Ask whatever owns a fact, the function or enum that computes it, never duplicate its logic at a second site\", and documentation.md § \"A doc points at the source instead of restating its content or constants, and the pointer is written with the source open\".",
"findings": [
{
"number": "code-architecture.1",
"section": "design",
"sites": [
"internal/board/rows.go:180",
"internal/board/rows.go:187",
"internal/board/rows.go:195",
"internal/board/rows.go:373",
"internal/board/rows.go:376",
"internal/board/rows.go:379",
"internal/board/rows.go:398",
"internal/board/rows.go:402",
"internal/board/rows.go:404",
"internal/board/rows.go:407",
"internal/board/rows.go:411",
"internal/board/table.go:33"
],
"reach": "Two changes are coming. One is the next field on a pull request, such as the base branch this PR moved into the document. The other is a restyle of the number, stack or bot mark. Today each must edit `boardColumns`/`secondLine` and `listColumns`/`titleLine` together. This PR already did that for one field. The CLAUDE round moved out of a single `rowColumns` entry into both an optional `listColumns` column and an inline test in `secondLine`. CLAUDE.md:136-139 now tells the next author to put a new field in `secondLine` or `boardColumns`, and it says nothing about `listColumns`.",
"body": "### Two layouts format the number, stack and bot marks inline\n\nThis PR splits the single `rowColumns` into two layouts.\n- **Board:** `boardColumns` plus `secondLine`.\n- **`prs list`:** `listColumns` plus `titleLine`.\n\nShared cell builders cover these fields: `stateCell`, `checksCell`, `reviewCell`, `claudeCell`, `ageCell` and `diffCell`. Three other fields have no builder. Each layout formats them inline, and the two copies already disagree:\n- **Number:** `\"%s #%d\"` at rows.go:195 and `\"%s #%d\"` at rows.go:402.\n- **Stack parent:** `\" %s #%d\"` at rows.go:376 and `\" %s #%d\"` at rows.go:404.\n- **Bot:** `\" \"+botGlyph+\" bot \"` at rows.go:379 and `botGlyph+\" bot \"` at rows.go:411.\n\nThe two layouts also decide differently whether the round shows. `listColumns` uses `optional: true` at rows.go:187. `secondLine` tests `claude.Text != \"\"` at rows.go:407.\n\nThe layouts are meant to differ in order and placement. They are not meant to differ in how a field is formatted. code-quality.md § \"Ask whatever owns a fact, the function or enum that computes it, never duplicate its logic at a second site\" applies here.\n\n**Fix:** Add `numberCell(pr)`, `stackCell(parent)` and `botCell(pr)` beside `checksCell` and `reviewCell`. Call them from `listColumns`, `titleLine` and `secondLine`. Each layout then only chooses where a field sits, and spacing comes from the layout's gap. A new field becomes one builder plus one placement per layout. The formatting is written once."
},
{
"number": "code-architecture.2",
"section": "design",
"sites": [
"README.md:149",
"README.md:150",
"README.md:151",
"CLAUDE.md:195",
"CLAUDE.md:196",
"internal/board/rows.go:434",
"internal/board/rows.go:435"
],
"reach": "Two changes would leave the docs wrong. One is a change to `sideMargin` or `marginFrom` in rows.go. The other is a gotermboard release that moves `PanelFrom`, and the PR body already says the pin moves to a new gotermboard release before merge. Either change makes README's \"160\" and \"184\" and CLAUDE.md's \"184\" wrong. Nothing in the build or tests ties these numbers back to their source.",
"body": "### The docs restate the panel threshold that gotermboard owns, plus a number derived from it\n\nThe side panel's threshold is `gotermboard.PanelFrom = 160`. gotermboard owns that constant. README.md:150 copies it as \"160\". The PR also writes the derived value \"184\" into README.md:151 and CLAUDE.md:196. 184 comes from `PanelFrom + 2*sideMargin`. The margin numbers \"12\" and \"150\" in README.md:149 copy `sideMargin` and `marginFrom` from rows.go:434-435.\n\nThe PR rests on a gotermboard pre-release pin. That makes `PanelFrom` the value most likely to move under these docs. documentation.md § \"A doc points at the source instead of restating its content or constants\" applies here.\n\n**Fix:** Have CLAUDE.md name the computation: the panel shows from `PanelFrom` plus both of `Look`'s sides. Drop the literal 184. If the README must keep a number, a test can check it. That test would assert `gotermboard.PanelFrom + 2*sideMargin` equals the value the README states."
}
]
}This review does not gate merge.
There was a problem hiding this comment.
PR review — stale-prose
Used: I cloned PR 13 at head 2ed553f and read every added or rewritten sentence in the diff, each beside its code. The files were README.md, CLAUDE.md, the help text in cmd/prs/tui.go, cmd/prs/tui_test.go, internal/board/rows.go and internal/board/table.go. I read the pinned gotermboard module for PanelFrom, LookBoard, the f key's Applies and the second line's truncation. I worked out Look's margin by hand from rows.go:442. I ran go test ./... with HOME set inside my directory, and every package passed.
Standards: pull-requests.md/Sentences the diff adds are checked for what goes stale, and the finding carries the rewrite; documentation.md/A count, version or measurement pinned in a comment or doc names what made it true and how it was measured, or goes stale as a constant; documentation.md/A comment states what this repos code enforces, never another repos deployment as fact
Findings: 0 correctness · 0 design · 2 prose
Prose
stale-prose.1 README.md:149 · README.md:150 · README.md:151 · CLAUDE.md:195 · CLAUDE.md:196 · CLAUDE.md:136 · CLAUDE.md:137 · CLAUDE.md:138
The README and CLAUDE.md copy out numbers and a field list that the code owns
This shape is a value the code holds. The sentences are true at the head. Each one goes wrong at the next change to a constant it copies.
README.md:149-151: "The board keeps a margin of 12 columns a side, and a terminal under 150 columns wide gives up a column a side for every two it lacks. The side panel wants 160 columns inside the margin, so it shows from 184."12copiessideMarginand150copiesmarginFrom(internal/board/rows.go:434-435).160copiesPanelFromin gotermboard'smodel.go:104. That constant lives in another repo, so this one is also will go stale. Moving the gotermboard pin can change it without touching this repo.184is derived from both (160 + 2×12).- "For every two it lacks" is already slightly off.
(marginFrom-width+1)/2rounds up, so at 149 columns the board already gives up a column a side (12-(150-149+1)/2 = 11).
CLAUDE.md:195-196: "PanelFromis measured inside the margin, so the side panel shows from 184 columns." The same 184 again.CLAUDE.md:136-138: "The first isboardColumns: state, title, diff, age and branch. The second issecondLine: checks under the state, then the checkout, the number, the review, the round and the bot." This repeats the slice inrows.go:166-174and the body ofsecondLine. Both already carry this list in their doc comments. A new column means editing CLAUDE.md too.
Rewrites:
- README.md:149-151 → "The board keeps a margin at each side, which a narrow terminal gives up first. The side panel's width threshold is measured inside the margin, so the panel needs a wider terminal than the margin alone suggests." A reader who wants the numbers finds
sideMarginandmarginFromininternal/board/rows.go. - CLAUDE.md:195-196 → "
PanelFromis measured inside the margin, so the side panel needsPanelFromplus both sides ofsideMargin." - CLAUDE.md:136-138 → "The first line is
boardColumnsand the second issecondLine." Keep the following sentence, which says where a new field goes. Its "before the bot" is the one ordering fact the code cannot show as a rule.
stale-prose.2 README.md:128
The README says every field has a glyph, but the title has none
This is false.
README.md:128: "A glyph marks each field, and a field's glyph is the same in every state."
The title is a field on the first line. titleCell (internal/board/rows.go:364-369) draws gotermboard.C(pr.Title, dim) or gotermboard.C(pr.Title, ""), with no glyph. Every other cell on both lines gets one: stateCell, checksCell, reviewCell, ageCell, diffCells, branchCell, whereCells, the number, the stack and the bot. The PR's own capture shows the title column with nothing in front of it.
Rewrite: "A glyph marks each field but the title, and a field's glyph is the same in every state."
Findings as JSON
{
"used": "I cloned PR 13 at head 2ed553f and read every added or rewritten sentence in the diff, each beside its code. The files were README.md, CLAUDE.md, the help text in cmd/prs/tui.go, cmd/prs/tui_test.go, internal/board/rows.go and internal/board/table.go. I read the pinned gotermboard module for `PanelFrom`, `LookBoard`, the `f` key's `Applies` and the second line's truncation. I worked out `Look`'s margin by hand from `rows.go:442`. I ran `go test ./...` with HOME set inside my directory, and every package passed.",
"standards": "pull-requests.md/Sentences the diff adds are checked for what goes stale, and the finding carries the rewrite; documentation.md/A count, version or measurement pinned in a comment or doc names what made it true and how it was measured, or goes stale as a constant; documentation.md/A comment states what this repos code enforces, never another repos deployment as fact",
"findings": [
{
"number": "stale-prose.1",
"section": "prose",
"sites": [
"README.md:149",
"README.md:150",
"README.md:151",
"CLAUDE.md:195",
"CLAUDE.md:196",
"CLAUDE.md:136",
"CLAUDE.md:137",
"CLAUDE.md:138"
],
"reach": "",
"body": "### The README and CLAUDE.md copy out numbers and a field list that the code owns\n\nThis shape is **a value the code holds**. The sentences are true at the head. Each one goes wrong at the next change to a constant it copies.\n\n- `README.md:149-151`: \"The board keeps a margin of 12 columns a side, and a terminal under 150 columns wide gives up a column a side for every two it lacks. The side panel wants 160 columns inside the margin, so it shows from 184.\"\n - `12` copies `sideMargin` and `150` copies `marginFrom` (`internal/board/rows.go:434-435`).\n - `160` copies `PanelFrom` in gotermboard's `model.go:104`. That constant lives in another repo, so this one is also **will go stale**. Moving the gotermboard pin can change it without touching this repo.\n - `184` is derived from both (160 + 2×12).\n - \"For every two it lacks\" is already slightly off. `(marginFrom-width+1)/2` rounds up, so at 149 columns the board already gives up a column a side (`12-(150-149+1)/2 = 11`).\n- `CLAUDE.md:195-196`: \"`PanelFrom` is measured inside the margin, so the side panel shows from 184 columns.\" The same 184 again.\n- `CLAUDE.md:136-138`: \"The first is `boardColumns`: state, title, diff, age and branch. The second is `secondLine`: checks under the state, then the checkout, the number, the review, the round and the bot.\" This repeats the slice in `rows.go:166-174` and the body of `secondLine`. Both already carry this list in their doc comments. A new column means editing CLAUDE.md too.\n\n**Rewrites:**\n\n- README.md:149-151 → \"**The board keeps a margin at each side, which a narrow terminal gives up first.** The side panel's width threshold is measured inside the margin, so the panel needs a wider terminal than the margin alone suggests.\" A reader who wants the numbers finds `sideMargin` and `marginFrom` in `internal/board/rows.go`.\n- CLAUDE.md:195-196 → \"`PanelFrom` is measured inside the margin, so the side panel needs `PanelFrom` plus both sides of `sideMargin`.\"\n- CLAUDE.md:136-138 → \"The first line is `boardColumns` and the second is `secondLine`.\" Keep the following sentence, which says where a new field goes. Its \"before the bot\" is the one ordering fact the code cannot show as a rule."
},
{
"number": "stale-prose.2",
"section": "prose",
"sites": [
"README.md:128"
],
"reach": "",
"body": "### The README says every field has a glyph, but the title has none\n\nThis is **false**.\n\n- `README.md:128`: \"A glyph marks each field, and a field's glyph is the same in every state.\"\n\nThe title is a field on the first line. `titleCell` (`internal/board/rows.go:364-369`) draws `gotermboard.C(pr.Title, dim)` or `gotermboard.C(pr.Title, \"\")`, with no glyph. Every other cell on both lines gets one: `stateCell`, `checksCell`, `reviewCell`, `ageCell`, `diffCells`, `branchCell`, `whereCells`, the number, the stack and the bot. The PR's own capture shows the title column with nothing in front of it.\n\n**Rewrite:** \"A glyph marks each field but the title, and a field's glyph is the same in every state.\""
}
]
}This review does not gate merge.
There was a problem hiding this comment.
PR review — contracts
Used: I cloned PR 13 at its head, ran go build ./... and go test ./... (all packages pass), and compared listColumns at the head with rowColumns on main: they are identical. I checked the prs tui flags, its --json snapshot path and list.schema.json. I read gotermboard's f key at the pinned pseudo-version. I searched the owner's repos with gh search code, then cloned dotfiles and read its prs tui callers in tmux.conf:317 and tmux-sessions:284.
Standards: cli-design.md § "Anything a table or screen shows is reachable through --json or a named verb, never only by parsing it" holds. The schema still carries every field the second line now draws: branch, base, path, stacked_on, bot and author. Searches: "human-readable output is not a contract; scripts read --json" and "pin dependency pseudo-version release before merge".
Findings: 0 correctness · 0 design · 0 prose
No findings.
Findings as JSON
{
"used": "I cloned PR 13 at its head, ran `go build ./...` and `go test ./...` (all packages pass), and compared `listColumns` at the head with `rowColumns` on main: they are identical. I checked the `prs tui` flags, its `--json` snapshot path and `list.schema.json`. I read gotermboard's `f` key at the pinned pseudo-version. I searched the owner's repos with `gh search code`, then cloned dotfiles and read its `prs tui` callers in `tmux.conf:317` and `tmux-sessions:284`.",
"standards": "cli-design.md § \"Anything a table or screen shows is reachable through --json or a named verb, never only by parsing it\" holds. The schema still carries every field the second line now draws: `branch`, `base`, `path`, `stacked_on`, `bot` and `author`. Searches: \"human-readable output is not a contract; scripts read --json\" and \"pin dependency pseudo-version release before merge\".",
"findings": []
}This review does not gate merge.
There was a problem hiding this comment.
PR review — useless-test
Used: I cloned the repo and checked out PR 13 at 2ed553f. I read every test the diff edits in cmd/prs/tui_test.go beside the changed rows.go, table.go, sections.go, board.go and tui.go. I searched the suite for tests of help text, of the f key and of the ~-rooted checkout. I ran go test ./... with HOME and the XDG paths pointed inside my directory, and every package passed.
Standards: I applied 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", and testing.md § "Never assert on rendered output — assert the value it was built from". No test the diff adds or edits takes a shape those rules say to delete.
Findings: 0 correctness · 0 design · 0 prose
No findings.
Findings as JSON
{
"used": "I cloned the repo and checked out PR 13 at 2ed553f. I read every test the diff edits in cmd/prs/tui_test.go beside the changed rows.go, table.go, sections.go, board.go and tui.go. I searched the suite for tests of help text, of the `f` key and of the `~`-rooted checkout. I ran `go test ./...` with HOME and the XDG paths pointed inside my directory, and every package passed.",
"standards": "I applied 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\", and testing.md § \"Never assert on rendered output — assert the value it was built from\". No test the diff adds or edits takes a shape those rules say to delete.",
"findings": []
}This review does not gate merge.
There was a problem hiding this comment.
PR review — G1
Used: Built the branch at 2ed553f and ran go vet, go test ./... and golangci-lint, all clean. Drove prs tui through tuidrive against the test stand-ins at 80, 100, 120, 140, 150, 160, 170, 184, 200, 240 and 300 columns, on this branch and on origin/main, with and without review rounds. Compared prs list, prs list --json and prs tui --json against main, and they match apart from timestamps and temp paths. Ran prs tui --once at its default width and with --pr at 170. Tried the margin fix in my clone, where the full suite passed, then restored the file.
Standards: documentation.md § "A doc points at the source instead of restating its content or constants, and the pointer is written with the source open"; documentation.md § "A count, version or measurement pinned in a comment or doc names what made it true and how it was measured, or goes stale as a constant"; I searched for a rule on pinning a dependency to an unreleased pseudo-version and found none.
Findings: 1 correctness · 0 design · 1 prose
Correctness
G1.1 internal/board/rows.go:441 · internal/board/rows.go:442 · cmd/prs/tui.go:26 · cmd/prs/tui.go:113
Reach: Any terminal from 160 to 183 columns wide running prs tui. Also every prs tui --once run with no COLUMNS set, because onceFallbackWidth is 160. The --once help also uses COLUMNS=160 as its own example.
A 160 to 183 column screen loses the document panel, and gains 24 blank columns.
On main, the side panel shows from 160 columns. On this branch, Look returns Sides: 12 from 150 columns up. gotermboard measures PanelFrom (160) against the width inside the margin. A 170-column terminal is 146 inside, so it gets no panel. The user must press enter and read the document full screen, which they did not need to do before.
What I ran:
prs tuiat 170 columns on the stand-ins drew no panel. Main draws one at 160.prs tui --oncewith no COLUMNS drew no│column and no document. Main's frame has the document beside the list.COLUMNS=170 prs tui --once --pr acme/widget#12put the document inline under the row instead of beside it.
The --once help says it exists "so an agent reads what a person reads". Its default frame now carries no document.
The body states the 184 threshold but gives no reason to trade the panel for blank margin. The reason it names is mechanical: the threshold is measured inside the margin.
Fix: have the margin give way to the panel. In Look, after sides is computed, add:
if width >= gotermboard.PanelFrom {
sides = min(sides, (width-gotermboard.PanelFrom)/2)
}I applied this in my clone. The panel then drew at 160 and at 170, and in --once's default frame. go test ./... still passed. From 184 up the margin stays at 12.
Prose
G1.2 README.md:149 · README.md:150 · README.md:151 · CLAUDE.md:195 · CLAUDE.md:196
The docs restate four layout numbers that the code and gotermboard own.
README.md says: "The board keeps a margin of 12 columns a side, and a terminal under 150 columns wide gives up a column a side for every two it lacks. The side panel wants 160 columns inside the margin, so it shows from 184."
CLAUDE.md says: "PanelFrom is measured inside the margin, so the side panel shows from 184 columns."
The four numbers have these owners:
- 12 is
sideMargininrows.go. - 150 is
marginFrominrows.go. - 160 is
gotermboard.PanelFrom, which lives in another repo. - 184 is 160 plus twice 12.
Changing any of the three constants leaves both docs wrong while they still read as correct. A bump of gotermboard can change PanelFrom without touching a line of prs. The fix proposed in the correctness finding above also makes 184 false. documentation.md § "A doc points at the source instead of restating its content or constants, and the pointer is written with the source open" covers this.
Rewrite for README.md: "The board keeps a margin at each side, narrower on a narrower screen. The side panel shows once the screen is wide enough for it inside that margin."
Rewrite for CLAUDE.md: "Look's margin is sideMargin from marginFrom columns. PanelFrom is measured inside the margin, so the panel's threshold is gotermboard.PanelFrom plus both of Look's sides."
Findings as JSON
{
"used": "Built the branch at 2ed553f and ran `go vet`, `go test ./...` and `golangci-lint`, all clean. Drove `prs tui` through tuidrive against the test stand-ins at 80, 100, 120, 140, 150, 160, 170, 184, 200, 240 and 300 columns, on this branch and on origin/main, with and without review rounds. Compared `prs list`, `prs list --json` and `prs tui --json` against main, and they match apart from timestamps and temp paths. Ran `prs tui --once` at its default width and with `--pr` at 170. Tried the margin fix in my clone, where the full suite passed, then restored the file.",
"standards": "documentation.md § \"A doc points at the source instead of restating its content or constants, and the pointer is written with the source open\"; documentation.md § \"A count, version or measurement pinned in a comment or doc names what made it true and how it was measured, or goes stale as a constant\"; I searched for a rule on pinning a dependency to an unreleased pseudo-version and found none.",
"findings": [
{
"number": "G1.1",
"section": "correctness",
"sites": [
"internal/board/rows.go:441",
"internal/board/rows.go:442",
"cmd/prs/tui.go:26",
"cmd/prs/tui.go:113"
],
"reach": "Any terminal from 160 to 183 columns wide running `prs tui`. Also every `prs tui --once` run with no COLUMNS set, because `onceFallbackWidth` is 160. The `--once` help also uses `COLUMNS=160` as its own example.",
"body": "**A 160 to 183 column screen loses the document panel, and gains 24 blank columns.**\n\nOn main, the side panel shows from 160 columns. On this branch, `Look` returns `Sides: 12` from 150 columns up. gotermboard measures `PanelFrom` (160) against the width inside the margin. A 170-column terminal is 146 inside, so it gets no panel. The user must press enter and read the document full screen, which they did not need to do before.\n\nWhat I ran:\n- `prs tui` at 170 columns on the stand-ins drew no panel. Main draws one at 160.\n- `prs tui --once` with no COLUMNS drew no `│` column and no document. Main's frame has the document beside the list.\n- `COLUMNS=170 prs tui --once --pr acme/widget#12` put the document inline under the row instead of beside it.\n\nThe `--once` help says it exists \"so an agent reads what a person reads\". Its default frame now carries no document.\n\nThe body states the 184 threshold but gives no reason to trade the panel for blank margin. The reason it names is mechanical: the threshold is measured inside the margin.\n\nFix: have the margin give way to the panel. In `Look`, after `sides` is computed, add:\n\n```go\nif width >= gotermboard.PanelFrom {\n\tsides = min(sides, (width-gotermboard.PanelFrom)/2)\n}\n```\n\nI applied this in my clone. The panel then drew at 160 and at 170, and in `--once`'s default frame. `go test ./...` still passed. From 184 up the margin stays at 12."
},
{
"number": "G1.2",
"section": "prose",
"sites": [
"README.md:149",
"README.md:150",
"README.md:151",
"CLAUDE.md:195",
"CLAUDE.md:196"
],
"reach": "",
"body": "**The docs restate four layout numbers that the code and gotermboard own.**\n\nREADME.md says: \"The board keeps a margin of 12 columns a side, and a terminal under 150 columns wide gives up a column a side for every two it lacks. The side panel wants 160 columns inside the margin, so it shows from 184.\"\n\nCLAUDE.md says: \"`PanelFrom` is measured inside the margin, so the side panel shows from 184 columns.\"\n\nThe four numbers have these owners:\n- 12 is `sideMargin` in `rows.go`.\n- 150 is `marginFrom` in `rows.go`.\n- 160 is `gotermboard.PanelFrom`, which lives in another repo.\n- 184 is 160 plus twice 12.\n\nChanging any of the three constants leaves both docs wrong while they still read as correct. A bump of gotermboard can change `PanelFrom` without touching a line of prs. The fix proposed in the correctness finding above also makes 184 false. `documentation.md` § \"A doc points at the source instead of restating its content or constants, and the pointer is written with the source open\" covers this.\n\nRewrite for README.md: \"The board keeps a margin at each side, narrower on a narrower screen. The side panel shows once the screen is wide enough for it inside that margin.\"\n\nRewrite for CLAUDE.md: \"`Look`'s margin is `sideMargin` from `marginFrom` columns. `PanelFrom` is measured inside the margin, so the panel's threshold is `gotermboard.PanelFrom` plus both of `Look`'s sides.\""
}
]
}This review does not gate merge.
There was a problem hiding this comment.
PR review — synthesis
Used: the 7 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 G1.2 · code-architecture.2 · stale-prose.1
The docs README.md and CLAUDE.md copy values that the code or gotermboard owns, and no check ties them to the source. The values are the margin 12, the margin start 150, the panel threshold 160 and the derived 184. A change to a constant leaves the prose wrong.
One defect, raised more than once
G1.2 · code-architecture.2 · stale-prose.1: README.md:149-151 and CLAUDE.md:195-196 restate the margin, panel threshold and derived 184 that the code and gotermboard own.
Shapes as JSON
{
"shapes": [
{
"number": "synthesis.1",
"section": "design",
"explains": [
"G1.2",
"code-architecture.2",
"stale-prose.1"
],
"body": "The docs README.md and CLAUDE.md copy values that the code or gotermboard owns, and no check ties them to the source. The values are the margin 12, the margin start 150, the panel threshold 160 and the derived 184. A change to a constant leaves the prose wrong."
}
],
"duplicates": [
{
"defect": "README.md:149-151 and CLAUDE.md:195-196 restate the margin, panel threshold and derived 184 that the code and gotermboard own.",
"findings": [
"G1.2",
"code-architecture.2",
"stale-prose.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": 13,
"head": "2ed553fab8180fa5fea630c8de96d85b73311fe4",
"experiment": "none",
"dispatch": "20261108T085315Z-pr-review",
"roster": "56080be48ea24f7450ff013d33a6bbbc2076f2c5",
"harness": "8d0504c41eb2e290861c83f258d503fc05ce3a79",
"machine": "queue",
"started": "2026-11-08T08:54:47Z",
"finished": "2026-11-08T09:03:46Z",
"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": "a0f85e3b82b4",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 459,
"tokens": {
"input": 64,
"cache_write": 78825,
"cache_read": 1805056,
"output": 21472
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 64,
"cache_write": 78825,
"cache_read": 1805056,
"output": 21472
}
},
"session": "01a1ba5e-7e6e-7667-aa72-25dd46e8b13a",
"worklog": "2568.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/13#pullrequestreview-5817292975",
"review_id": 5817292975,
"findings": {
"correctness": 1,
"design": 0,
"prose": 1
},
"numbers": [
"G1.1",
"G1.2"
],
"rules": {},
"head": "2ed553fab8180fa5fea630c8de96d85b73311fe4",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr13-round1-20261108T085315Z/records",
"started": "2026-11-08T08:55:37Z",
"finished": "2026-11-08T09:03:19Z",
"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": "3700d72236e3",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 104,
"tokens": {
"input": 20,
"cache_write": 29229,
"cache_read": 239505,
"output": 3509
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 20,
"cache_write": 29229,
"cache_read": 239505,
"output": 3509
}
},
"session": "01a1ba5f-32c7-70f2-8615-cc4e45399f90",
"worklog": "2568.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/13#pullrequestreview-5817280158",
"review_id": 5817280158,
"findings": {
"correctness": 0,
"design": 0,
"prose": 0
},
"numbers": [],
"rules": {},
"head": "2ed553fab8180fa5fea630c8de96d85b73311fe4",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr13-round1-20261108T085315Z/records",
"started": "2026-11-08T08:56:30Z",
"finished": "2026-11-08T08:58:20Z",
"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": "d9be86eb4324",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 121,
"tokens": {
"input": 22,
"cache_write": 38838,
"cache_read": 344325,
"output": 6046
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 22,
"cache_write": 38838,
"cache_read": 344325,
"output": 6046
}
},
"session": "01a1ba5e-7d2e-7e27-888b-e78c396712ba",
"worklog": "2568.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/13#pullrequestreview-5817278346",
"review_id": 5817278346,
"findings": {
"correctness": 0,
"design": 2,
"prose": 0
},
"numbers": [
"code-architecture.1",
"code-architecture.2"
],
"rules": {},
"head": "2ed553fab8180fa5fea630c8de96d85b73311fe4",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr13-round1-20261108T085315Z/records",
"started": "2026-11-08T08:55:32Z",
"finished": "2026-11-08T08:57:40Z",
"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": "eae343d1d8d5",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 90,
"tokens": {
"input": 24,
"cache_write": 20827,
"cache_read": 237759,
"output": 3709
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 24,
"cache_write": 20827,
"cache_read": 237759,
"output": 3709
}
},
"session": "01a1ba5e-a286-7eca-a841-bb2d51b434d5",
"worklog": "2568.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/13#pullrequestreview-5817277615",
"review_id": 5817277615,
"findings": {
"correctness": 0,
"design": 1,
"prose": 0
},
"numbers": [
"fleet-patterns.1"
],
"rules": {},
"head": "2ed553fab8180fa5fea630c8de96d85b73311fe4",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr13-round1-20261108T085315Z/records",
"started": "2026-11-08T08:55:50Z",
"finished": "2026-11-08T08:57:23Z",
"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": "c284e39c894a",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 14,
"tokens": {
"input": 6,
"cache_write": 3681,
"cache_read": 31225,
"output": 619
},
"tokens_from": "result",
"tokens_by_model": {
"claude-sonnet-5-5": {
"input": 6,
"cache_write": 3681,
"cache_read": 31225,
"output": 619
}
},
"session": "01a1ba5e-a0d3-74f6-9830-40202dd20a6b",
"worklog": "2568.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/13#pullrequestreview-5817273294",
"review_id": 5817273294,
"findings": {
"correctness": 0,
"design": 0,
"prose": 0
},
"numbers": [],
"rules": {},
"head": "2ed553fab8180fa5fea630c8de96d85b73311fe4",
"kept": false,
"records": "/home/chris/.cache/run-pr-reviewers/datapointchris-prs-pr13-round1-20261108T085315Z/records",
"started": "2026-11-08T08:55:30Z",
"finished": "2026-11-08T08:55:46Z",
"account": "live",
"api_key_source": "none",
"machine": "archlinux"
},
{
"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": "50506a05f936",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 174,
"tokens": {
"input": 24,
"cache_write": 37752,
"cache_read": 355380,
"output": 7591
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 24,
"cache_write": 37752,
"cache_read": 355380,
"output": 7591
}
},
"session": "01a1ba5d-b998-723b-97ee-5fda8d2277fa",
"worklog": "2568.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/13#pullrequestreview-5817278703",
"review_id": 5817278703,
"findings": {
"correctness": 0,
"design": 0,
"prose": 2
},
"numbers": [
"stale-prose.1",
"stale-prose.2"
],
"rules": {},
"head": "2ed553fab8180fa5fea630c8de96d85b73311fe4",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr13-round1-20261108T085315Z/records",
"started": "2026-11-08T08:54:47Z",
"finished": "2026-11-08T08:57:48Z",
"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": "56bdd640764b",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 25,
"tokens": {
"input": 10,
"cache_write": 4980,
"cache_read": 54191,
"output": 970
},
"tokens_from": "result",
"tokens_by_model": {
"claude-sonnet-5-5": {
"input": 10,
"cache_write": 4980,
"cache_read": 54191,
"output": 970
}
},
"session": "01a1ba5d-ccdc-7986-8ef9-fee1ae1774c7",
"worklog": "2568.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/13#pullrequestreview-5817272147",
"review_id": 5817272147,
"findings": {
"correctness": 0,
"design": 0,
"prose": 0
},
"numbers": [],
"rules": {},
"head": "2ed553fab8180fa5fea630c8de96d85b73311fe4",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr13-round1-20261108T085315Z/records",
"started": "2026-11-08T08:54:51Z",
"finished": "2026-11-08T08:55:20Z",
"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": "88b65626bceb",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 123,
"tokens": {
"input": 28,
"cache_write": 39351,
"cache_read": 446601,
"output": 4366
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 28,
"cache_write": 39351,
"cache_read": 446601,
"output": 4366
}
},
"session": "01a1ba5e-d90d-7a94-9f18-d7d01dfae4b2",
"worklog": "2568.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/13#pullrequestreview-5817279810",
"review_id": 5817279810,
"findings": {
"correctness": 0,
"design": 0,
"prose": 0
},
"numbers": [],
"rules": {},
"head": "2ed553fab8180fa5fea630c8de96d85b73311fe4",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr13-round1-20261108T085315Z/records",
"started": "2026-11-08T08:56:03Z",
"finished": "2026-11-08T08:58:12Z",
"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": "40dcb5a50df0",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 5,
"tokens": {
"input": 2,
"cache_write": 5939,
"cache_read": 1433,
"output": 285
},
"tokens_from": "result",
"tokens_by_model": {
"claude-sonnet-5-5": {
"input": 2,
"cache_write": 5939,
"cache_read": 1433,
"output": 285
}
},
"session": "01a1ba65-ec51-7962-8bfa-af0dd421e713",
"worklog": "2568.1",
"worklog_refused": "",
"claude_code": "2.1.319",
"review_url": "https://github.com/datapointchris/prs/pull/13#pullrequestreview-5817294110",
"review_id": 5817294110,
"findings": {
"correctness": 0,
"design": 1,
"prose": 0
},
"numbers": [
"synthesis.1"
],
"rules": {},
"head": "2ed553fab8180fa5fea630c8de96d85b73311fe4",
"kept": false,
"records": "/home/fleet/.cache/run-pr-reviewers/datapointchris-prs-pr13-round1-20261108T085315Z/records",
"started": "2026-11-08T09:03:38Z",
"finished": "2026-11-08T09:03:46Z",
"account": "live",
"api_key_source": "none",
"machine": "queue"
}
],
"rules": {
"proposed": 0,
"keys": [],
"unfiled": []
},
"kept": []
} |
…th segment gotermboard measures the side panel's threshold inside the margin, so a terminal from 160 to 183 columns wide lost the panel, and prs tui --once at its default width drew no document. The margin now narrows as the screen nears the panel's width plus both sides, so every screen wide enough for the panel with no margin keeps it. A checkout reads ~-rooted only where it sits inside the home directory at a path segment, so a sibling of home keeps its whole path. The number, the stacked parent and the bot each have one cell builder, which the board's second line and prs list's title line both place. prs list's number and bot take two spaces after their glyph, as every other field with words does. The README and CLAUDE.md say where the margin and panel come from rather than copying their numbers. gotermboard is pinned at the commit that keeps a look whole under tabs, on an empty frame and beside a wrapped last column. Agent-Name: claude-code-unattended Agent-Session: 69e19fe4-568c-41ef-9af1-316d086cd270 Agent-Run: 01a1ba5c-b70b-7c95-89fd-537c2603d8a5 Worklog: 2568.1
…ruled apart The board's look needs Row.Second, Group.BetweenRows and LookBoard, which gotermboard first tags in v0.35.0. The pin moves off the untagged commit it was built against. Agent-Name: claude-code Agent-Session: 60bee577-11ae-4c13-9517-75a25f6211a6 Worklog: 1786.1
…those with i (#14) 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 `d` opened 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`: `diffSinceReview` and `openDiff`. Check the order of the cases in `diffSinceReview`'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`: `HeadHolds` and `BaseMergedSince`. Check that each git exit is read as yes, no or failure, and that only a failure stops nvim. - `internal/pullrequests/detail.go`: `NewestReview` and `SinceReview`. 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 each `LEFT` thread and counts them. Check that against `cmd/prs/diff-threads.schema.json` and the nvim panel that places threads by side. - `cmd/prs/tui_test.go`: `TestARowCountsTheCommitsSinceItsReviewAndIDiffsOnlyThoseOrTheWholeDiffWithWhy`, and `withPushesOf`, which stages the cases on the stand-in GitHub. - `51efac4` is a separate fix to an existing test's wait, which raced under the parallel suite. ## How it was verified - `go test ./... -count=1` and `golangci-lint run`, green, then the pty suite twice more in a row, green. - The new test drives the built binary on a pty against the stand-in GitHub, git and nvim. It covers four rows: - Widget #12's head is two commits past mona's review. `i` runs `DiffviewOpen 9e8d7c6…..refs/remotes/origin/pr/12`, and its threads file reads `since_review` with one `LEFT` thread left out. - Gadget #5 has no review, so `i` opens the whole diff. - Widget #13's review read a commit the stand-in git does not have, so `i` opens the whole diff and names that commit. - Ledger #4 is on Bitbucket, where `i` fetches nothing and starts no nvim. - The test failed when the row's text was broken, and when the Bitbucket refusal was made to open the diff instead. - `HeadHolds` and `BaseMergedSince` ran 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's `not a git repository` line as an error. - The branch where nothing was pushed since the review was run once with a throwaway test, which saw the footer's line and no nvim start, then deleted. ## What changes - A GitHub row's second line reads `2 commits since review` or `1 commit since review` after its newest review and its Claude round. It reads `rebased since review` where the branch does not hold the reviewed commit. A row nobody reviewed, or whose head is the reviewed commit, reads nothing there. - `i` on a row fetches its head as `d` does and opens nvim's diff from the reviewed commit to the head. - `i` opens 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. - Where git cannot say whether the head holds the reviewed commit, `i` opens nothing and the footer names git's failure. - Where nothing was pushed since, the footer says so and nvim does not start. - `i` refuses on a Bitbucket row and on a recently closed one, because prs reads neither's reviews, and before a GitHub row's detail lands. - The threads file `i` writes reads `"diff": "since_review"`, names the reviewed commit as `base`, holds only the `RIGHT` threads, and counts the rest in `threads_left_out`. The one `d` writes reads `"diff": "whole"` and `"threads_left_out": 0`. The document file beside it still lists every thread. - A merge after `i` pins the head `i` fetched, as one after `d` does. - `i` is prs's own key now. A config whose window action binds `i` is refused, and `prs tui` does not start until the config moves it. ## Decisions, and what they rejected - **`i` for the key** — it stands for interdiff, the diff between two versions of one change. `D` deletes in other terminal tools, and `f`, `s`, `e`, `n`, `u`, `M` and `C` mean other things there. No common tool binds a letter to this diff, so none was copied. - **The fetched head decides, through git** — the row counts from the detail read, which can be older than the head `i` fetches. 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, and `git merge-base --is-ancestor` answers for one present. - **A base merge is found by where each side meets the base** — `git merge-base <reviewed> origin/<base>` and `git merge-base <head> origin/<base>` differ only where the base came into the branch after the review. - **A rebased branch opens the whole diff** — `git range-diff` would compare the two versions of the commits across a rebase, but the whole diff with the reason on the footer is the agreed behavior. - **The threads file drops `LEFT` threads for `i`** — a `LEFT` thread'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. `diff` says which left side `base` is, and `threads_left_out` says how many were dropped. - **Smallest change that would work** — `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 - It adds nothing to `prs list` or either `--json` output, because neither reads a pull request's detail. `prs tui --help` now says so. - It does not count commits for Bitbucket rows, which have no detail read. - A merge from the base counts as one commit since review on the row. `i` then names the merge as why it opened the whole diff. ## The review Round 1: #14 (comment). Every fix is in `3f2cb47`. - **G1.1** fixed — `i` on a recently closed row, or one no read lists, says prs reads none of its reviews and points at `d`. "Still being read" shows only while a read is under way. - **G1.2** fixed — `BaseMergedSince` compares where the reviewed commit and the head each meet `origin/<base>`, and `i` opens 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's `base merged since review` was not built: the detail read holds no commit parents, and the count of one pushed commit is true. - **G1.3** fixed — prose says "the diff keys" or "a diff key" rather than listing `d` and `i`, in CLAUDE.md, README, the threads schema and the code comments. - **stale-prose.1** fixed — README and `sinceReviewCell`'s comment say a blank means prs could not tell, or there is nothing to count. - **stale-prose.2** fixed — `keyTable`'s comment and CLAUDE.md drop the claims about what other tools bind. Each says only that `D` is left unbound because it deletes elsewhere. - **stale-prose.3** fixed — a review GitHub names no commit for opens the whole diff with its own reason, apart from the gone-commit one. - **useless-test.1** fixed — the `?` overlay checks and the `D` check are deleted, and so is "D binds nothing" in the test's comment. - **useless-test.2** fixed — the footer waits match the values each line carries: `bitbucket.*#4`, `diff of widget #12.*9e8d7c6`, `whole diff of gadget #5` and `whole diff of widget #13.*5ca1ab1`. The Bitbucket refusal is pinned by the fetch and nvim it does not run. - **failure-paths.1** fixed — same as synthesis.1. - **fleet-patterns.1** fixed — same as synthesis.1. - **synthesis.1** fixed — `HeadHolds` returns `(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. - **contracts.1** fixed — the threads file gains `diff` and `threads_left_out`, both required in the schema, so `base` reads one way for each kind of diff. - **contracts.2** fixed — README § "Keys of your own" says a window action on a key prs binds is refused and stops `prs tui`. It names `?` and `prs config show` as where the bound keys are read, rather than listing letters a release can add to. This body names `i` as 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. - **contracts.3** fixed — `prs tui --help` says `--once` and `--json` read 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
A pull request GitHub will not merge leaves prs with nothing to offer.
`m` sends the merge, gh refuses it, and the footer prints gh's reason.
The fix then happens somewhere else: a checkout, a browser tab or
another terminal, each found and opened by hand.
This opens a menu of fixes instead. `m` on a row whose merge state says
GitHub refuses it reads the pull request again. Where it is still
blocked, the menu opens rather than the merge question. A merge GitHub
refuses anyway opens the same menu under gh's words, and `f` opens it on
any row. Each choice is one key, and no choice exits prs.
| Blocker | Built-in choices |
| --- | --- |
| Conflicts (`CONFLICTING`, `DIRTY`) | `n` rebase onto the base and
resolve each stop in nvim · `x` close, after asking |
| Behind | `u` `gh pr update-branch --rebase` |
| Failing checks (`UNSTABLE`, or `BLOCKED` with a check failing) | `l`
read the failed log · `r` `gh run rerun --failed` · `m` merge anyway,
`UNSTABLE` only |
| Draft | `r` `gh pr ready` |
| Unknown, or blocked by something else | none; it says what GitHub
reads |
| Any row a rebase's worktree is left for | `p` push a finished rebase ·
`a` abort a stopped one, or remove the worktree |
`merge_fixes` in the config adds choices of your own per blocker kind.
Each runs in a tmux window opened behind the board, and the row reads
its `row_reads` while that window is open.
## What to look at
- `internal/board/rows.go`, `mergeMeanings` and `mergeStateOf`: one
table says what each merge state means, its word and the blocker the
menu offers fixes for. `blockerOf` in `internal/board/fix_menu.go` is a
lookup in it plus the failing-checks rule. A draft wins over a conflict,
and a conflict over everything else.
- `internal/pullrequests/rebase_worktree.go`, `Start`: the head branch
is fetched into `origin/<branch>`, and the worktree is added there,
detached. A registered worktree with a rebase stopped, or finished and
not pushed, is kept as it is. One on a branch is refused, and one with
neither must be clean before `checkout --detach`. A path that is not a
worktree is refused rather than replaced.
- `internal/pullrequests/rebase_worktree.go`, `Continue`: `git rebase
--continue` either stops at the next commit, finishes, or stops where
git cannot go on with nothing unmerged. That last one stays stopped,
with git's words.
- `internal/pullrequests/rebase_worktree.go`, `Push`: the lease is
`--force-with-lease=refs/heads/<branch>:<ORIG_HEAD>`, the head the
rebase started from. git's `--force-if-includes` reads the reflog of a
local branch named for the head, and a detached worktree pushes from
none. So git refused that push even where nobody else had pushed.
- `internal/pullrequests/pullrequest.go`, `runFor`: every client runs
with `GIT_EDITOR=true`. Without it, `rebase --continue` waits on an
editor in a session with no terminal.
- `internal/pullrequests/rebase_worktree.go`, `ConflictingFiles`: `git
merge-tree --write-tree --name-only` exits 1 both for a conflict and for
a ref it cannot read. The tree id on stdout's first line is what tells a
conflict apart.
- `internal/config/config.go`, `quotedPlaceholders`, `FillCommand` and
`insideQuotes`: a placeholder declared in `quotedPlaceholders`
(`{path}`, `{worktree}`, `{head}`, `{base}`, `{log}`) is quoted for the
shell, and a command that puts one inside quotes of its own is refused
at load.
- `internal/board/actions.go`, `mergeOf`, `merge` and `mergeWork`: every
merge key starts the merge `mergeOf` pins. A refused merge reads the
pull request again and opens the menu, except where the refusal was a
timeout or the host has no merge state.
- `internal/board/tmux_window_watch.go`: the tmux poll moved out of
`review_rounds.go`, and serves both review windows and fix windows.
## How it was verified
`go test -count=1 ./...` passed, with `cmd/prs` in 11.9s. Two tests
drive the built binary through tuidrive against the stand-in GitHub, gh,
git, tmux and nvim:
-
`TestARefusedMergeOpensTheFixMenuOnItsConflictsWhichRebaseInAWorktreeAndPush`
starts with a merge that reads ready and is refused by gh. The second
read finds it conflicting, and the test checks that the menu opens under
gh's words, naming both conflicting files. The test then walks the
rebase:
1. `n` fetches
`+refs/heads/cache-metrics:refs/remotes/origin/cache-metrics` and adds a
detached worktree there at `pr-13-rebase`. It runs `rebase --quiet
origin/add-cache` and opens nvim in the worktree.
2. With one path still unmerged, `c` opens `new-window -d` with the
worktree handed over, and the row reads `claude fixing`.
3. With the stop resolved, `f` and `n` reopen nvim, then run `rebase
--continue`. The stand-in rebase stops once more, and nvim opens again
at that stop, three nvim runs in all.
4. Once the rebase finishes, the push question lists the commit. Enter
pushes with `--force-with-lease=refs/heads/cache-metrics:<head>` and
removes the worktree.
5. Last, a rebase is left stopped while the pull request reads ready.
`f` offers `a`, which runs `rebase --abort` and removes the worktree.
-
`TestTheFixMenuUpdatesABranchReadiesADraftAndReadsOrRerunsFailingChecks`
covers `u` on a branch behind, which runs `update-branch --rebase`, and
`r` on a draft. Then it covers `l`, `r` and `c` on failing checks. Each
step checks the arguments the stand-in gh or tmux received.
Two assertions were proved able to fail:
- With `Fill` in place of `FillCommand`, the conflict test fails on the
missing `FIX_LOG=''`.
- With the next stop offering the menu rather than opening nvim, the
conflict test fails waiting for the second stop's unmerged path.
Three scratch checks ran once and were deleted:
- **The rebase, against real git.** The setup is a bare origin, and a
two-commit branch that conflicts with its base at each commit.
`RebaseWorktree` ran with `GIT_EDITOR=false` in the environment:
- `Start` stopped at the first commit, and `Continue` stopped at the
second.
- The next `Continue` finished, and `ORIG_HEAD` was the head the rebase
started from.
- A second clone then pushed to the branch, and `Push` was refused with
`stale info`. The rebase stayed in the worktree.
- `Abort` removed it. A new `Start` rebased the newest head, and `Push`
put it on origin over the other clone's commit.
- Last, a stopped rebase aborted cleanly.
- With `GIT_EDITOR=true` taken out of `runFor`, `Continue` failed on
`there was a problem with the editor 'false'`.
- **A fork.** A listing marking #13 cross-repository opens its menu with
the fork line and only `c` and `x`.
- **The quoting.** `insideQuotes` was checked on six commands.
`FillCommand` was checked on a branch named `a;b'c`, and on one named
`x;rm {number}`, which fills as one quoted word and is not filled again.
`gh api graphql` confirmed `isCrossRepository` on this pull request.
## What changes
- `m` on a blocked GitHub row opens the menu rather than asking to
merge, after one more read of the pull request. A ready row asks to
merge, and so does every Bitbucket row.
- `f` is a new key: the fix menu on the row under the cursor, after one
read of the pull request.
- A merge gh refuses opens the menu with gh's words at its top, where
the new read finds a blocker.
- Resolving a conflict adds a worktree at `<worktree_root>/<checkout's
directory>/pr-<n>-rebase`, detached at the head branch. `worktree_root`
defaults to `$XDG_DATA_HOME/prs/worktrees`. The worktree goes once its
rebase is pushed or aborted. Until then every menu on that row names it,
and offers `p` for a finished rebase and `a`.
- `n` moves `origin/<branch>` in the checkout, as the person's own `git
fetch` would.
- The push rewrites the head branch, with a lease on the head the rebase
started from, so it fails where anyone pushed since. A refused push
leaves the rebase in the worktree, and the footer names git's
refused-ref line.
- A conflict whose head branch is in a fork gets no `n`, and the menu
says why.
- `u` rebases the branch on GitHub, through `gh pr update-branch
--rebase`.
- Every client prs runs has `GIT_EDITOR=true`.
- A rerun goes on past a run that fails, and the footer names each run
that reran.
- `merge_fixes` and `worktree_root` are new config keys. `prs config
show` prints the worktree root and each fix, offered, refused, or not
offered for a missing program. `prs config path --help` documents both.
- A window action's command now fills through `FillCommand`. Its
placeholders are `{number}`, `{repo}` and `{slug}`, none quoted, so an
existing command fills unchanged.
- The failing log is kept at `$TMPDIR/prs-failing-log-<repo>-<n>.txt`,
which `{log}` names.
- gotermboard is pinned at the head of datapointchris/gotermboard#16,
which adds `Question.Choices` and `OpenWindowBehind`, until that
releases.
## Decisions, and what they rejected
- **A conflict and a branch behind both catch up by rebasing.**
*Rejected:* merging the base in, which the design first chose. The
design's owner settled on rebasing after the review, because a branch's
later catch-up rebases. A rebase over a merge of the base drops that
merge, and its conflicts must then be resolved a second time. The force
push is the rebase, and the lease keeps it from overwriting anyone's
commits.
- **The lease names its commit, `ORIG_HEAD`.** *Rejected:*
`--force-with-lease --force-if-includes`. From a detached worktree with
no local branch of the head's name, real git refused that push with
`remote ref updated since checkout` although nobody had pushed. A lease
on the head the rebase started from refuses exactly where someone pushed
since, and needs no local branch.
- **The worktree is detached at the head branch rather than on it.**
*Rejected:* `git worktree add <path> <branch>`. The branch is often
checked out in another worktree, where git refuses a second, and a local
branch can hold commits nobody pushed.
- **It is named `pr-<n>-rebase`.** A tool keeping its own worktree of a
pull request beside it more likely names it `pr-<n>`, and `Start` would
otherwise move that one off its branch.
- **Enter means yes on the push question.** The person just resolved the
rebase, and the lease refuses the push where it would overwrite someone
else's commit.
- **`m` reads the pull request again before choosing the menu over the
merge.** *Rejected:* deciding on the listing's merge state, which can be
minutes old. GitHub often reads `UNKNOWN` or `BEHIND` for a moment after
a push.
- **`UNSTABLE` with checks failing offers "merge anyway".** GitHub
merges an `UNSTABLE` pull request, because no failing check is required,
and prs merged those before this change. Only `BLOCKED` takes the merge
away.
- **Paths and branches are quoted, and a command may not quote them
itself.** *Rejected:* filling every value bare, which lets a branch
named `x;rm -rf ~` run its tail. *Rejected:* quoting every value, which
breaks `'/review-pr {number}'`, the shape existing commands take.
Quoting only what comes from outside prs, and refusing a quoted
placeholder, keeps both.
- **The worktree root is config and a default, with no environment
variable.** A tool implements the layers it needs, and a default may not
name a path outside the tool's own XDG directories. A machine that keeps
its worktrees elsewhere says so in `worktree_root`.
- **Fix windows open behind the board.** *Rejected:* `OpenWindow`, which
takes the terminal away from the board as soon as a fix starts.
- **Smallest change that would work**: a menu with only update-branch
and ready. A conflict is the commonest blocker on a stacked branch, and
the design asks for its files by name and for resolving it in nvim.
## Risk and rollback
A release ships the binary, and a revert removes the menu and the two
config keys. A config naming `merge_fixes` or `worktree_root` is refused
by any release before this one, because prs refuses unknown keys. So a
config change adding them waits for this release. A pushed rebase
rewrites the head branch, and GitHub keeps review comments on the old
commits as outdated. A revert leaves any worktree prs added under
`worktree_root`. `git worktree list` in the checkout names each one, and
`git worktree remove` takes it.
## The review
Round 1:
#17 (comment)
- `system-design.1` — fixed. The row fragment reads `isCrossRepository`,
carried as `HeadInFork` and kept out of `--json`, because Bitbucket's
listing does not say. A conflict whose head is in a fork gets no `n`,
and its menu says why. Configured fixes and `x` stay.
- `failure-paths.1` — fixed — same as `G1.1`.
- `failure-paths.2` — fixed. `rerunFailed` goes on past a run that
fails, and the footer names each run that reran, then each failure.
- `stale-prose.1` — fixed. The README points at `prs config path --help`
for the blocker kinds and at `prs config show` for the menu's keys. The
comment on `blocker` no longer counts its kinds.
- `stale-prose.2` — fixed — same as `G1.4`.
- `code-architecture.1` — fixed. Each `mergeMeanings` entry carries its
`blocker`, `checksBlock` and `mergesFailing`. `mergeStateOf` holds the
draft and conflict override for both `stateMeaning` and `blockerOf`, and
merge anyway reads `mergesFailing`.
- `code-architecture.2` — fixed. The quoted placeholders are a set of
their own, `quotedPlaceholders`, which the merge fix's set joins.
Declaring a placeholder in one set or the other decides whether it is
quoted, and `ShellQuotedPlaceholders()` names that set.
- `code-architecture.3` — rejected. The kinds are the config's
vocabulary: the words a file writes, refused at load and handed to
`{blocker}`. The board's kinds are defined from the config's constants,
so each word lives once. `unknown` and `held` are the board's alone,
because no fix is offered for them. With `code-architecture.1` fixed, a
new kind costs a constant, a `mergeMeanings` entry and a `fixesFor`
case, and the help text derives the list.
- `code-architecture.4` — fixed. `askFix` opens no menu offering one key
twice, and names both choices on the footer. So a built-in key
`menuKeys` misses shows, rather than hiding a choice.
- `code-architecture.5` — fixed. `mergeOf` does the provider check, the
refusal and the pin, and `askMerge`, merge anyway and the batch each
call it.
- `contracts.1` — rejected, for the variable. The help and README
example now read `~/.worktrees`. The standard the finding cites has a
tool implement the layers it needs, and forbids a default outside the
tool's own XDG directories. `WORKTREE_ROOT` belongs to another tool, and
prs reads nothing of one machine's setup. A machine whose worktrees live
elsewhere names the place in `worktree_root`, as
datapointchris/dotfiles#109 does.
- `fleet-patterns.1` — fixed. The design's owner settled on rebasing
after the round. `n` rebases the head onto the base in its worktree, and
`u` runs `gh pr update-branch --rebase`.
- `fleet-patterns.2` — fixed. The worktree is `pr-<n>-rebase`, and
`Start` leaves alone a worktree it finds on a branch. The default root
stays under prs's own data directory, for the reason `contracts.1`
gives.
- `fleet-patterns.3` — deferred. One quoter shared across tools, and
replacing the copies in other repos, is work outside this repo, filed
where those repos' work is tracked. prs's copy is the shape the others
should match.
- `G1.1` — fixed. A refused push leaves the finished rebase in the
worktree, and `p` pushes it again. `failureLine` makes the footer name
git's refused-ref line, such as `! [rejected] HEAD -> feat (stale
info)`, rather than `To <remote>`. Real git confirmed both, as "How it
was verified" says.
- `G1.2` — fixed. Every menu finds the worktree, whatever blocks the
pull request or where nothing does. `a` aborts its rebase and removes
it, and `p` pushes a finished one. `f` on a ready row reads the pull
request again to find one. The conflict test covers it.
- `G1.3` — fixed. The step presses `m`, waits for the merge question,
then presses enter.
- `G1.4` — fixed. "as before" is gone, and the key list points at `prs
config show`. The sentence on a waiting rebase now covers a refused
push.
- `synthesis.1` — fixed — same as `code-architecture.1`,
`code-architecture.2` and `code-architecture.4`. `code-architecture.3`
is rejected, as its line says.
- `synthesis.2` — fixed — same as `fleet-patterns.2`.
- `synthesis.3` — fixed — same as `G1.1`.
Session 0ed7f52a-bc57-4308-b45d-2889466add5f wrote this pull request.
The board drew each pull request on one line: its state, checks, review and round, its age and
diff, its repo and number, then its title wrapping under itself. Its branch and checkout sat
dim under the row, hidden until
fshowed them. The columns packed to the left, so a widescreen left most of its width empty, and nothing in the shape said which field was which.
The first version of prs drew each pull request as a two-line block, ruled apart from the
next, a glyph on each field, its columns spread across a screen with a margin. This puts that
look back. The keys, the groups, the state words, the side panel and the document are as they
were.
Session 69e19fe4-568c-41ef-9af1-316d086cd270 wrote this.
That is the board at 150 columns on the test fixtures, glyphs blanked because a browser has no
Nerd Font.
What to look at
internal/board/rows.go,boardColumnsandsecondLine: which field sits on which line.The first line is state, title, diff, age and branch. The second is checks under the state,
then the checkout, the number with any stacked parent, the review, the newest Claude round
and the app that opened it.
internal/board/rows.go,Look: the margin issideMarginfrommarginFromcolumns andnarrows below that. It also narrows from
gotermboard.PanelFromcolumns up, so the sidepanel, which gotermboard measures inside the margin, shows wherever it would with none.
internal/board/rows.go,numberCell,stackCellandbotCell: the board's second lineand
prs list's title line place the same cells.internal/board/table.go:prs listkeeps its one-line table throughlistColumns, andbuilds its own title line now that a board row carries no
WrapLast.cmd/prs/tui_test.go:HOMEis now the stand-in's directory, so a checkout draws as~/widgetthe way a real one reads, rather than as a long temporary path that cut thesecond line short.
How it was verified
go test ./...passes. The TUI tests drive the built binary on a pty against the stand-inGitHub and Bitbucket clients.
columns, and compared by eye with a capture of the first version at 150. No line passes the
screen's edge at any width, by two separate measurements of each frame. At 170 the panel
draws beside the list inside a 5-column margin.
prs list --jsonandprs tui --jsonstill validate againstcmd/prs/list.schema.json.What changes
headers, counts and folds.
right-aligned, so the signs stack.
The margin narrows on a narrow screen, and gives way to the side panel, which shows from
160 columns as before.
▸.Group headers keep theirs.
fhas nothing to showand leaves the footer and
?. The base a pull request merges into is in its document.~-rooted only inside the home directory at a path segment, so asibling of home keeps its whole path.
prs listkeeps its one-line table. Its number and bot marks take two spaces after theirglyph, as every other field with words does.
The pin moves to the release that pull request cuts before this one merges.
Decisions, and what they rejected
the checks on the second. Review, the round and the bot follow the number on the second line.
Rejected: stacking the state, the checks and the review in the status column, which makes
every row three lines where the first version's were two. Rejected: ordering the words by
which one blocks the merge, which would give a line a different meaning on each row.
↑ #12sits beside the number, because a cell takes one color and the title'scell no longer carries a line of marks after it.
~-rooted, or as owner/name withnot cloned herewhere none is.nothing apart. The first version had none.
panel and a narrower margin. Rejected: keeping 12 columns a side and losing the panel there,
which also left
prs tui --onceat its default 160 columns with no document.What this does not do
It adds no key and changes no key's behavior.
The review
Round 1: #13 (comment)
Looknarrows the margin fromgotermboard.PanelFromcolumns up to half ofwhat the screen has past it, so the width inside the margin never drops under the panel's
threshold.
TestATerminalWideEnoughForTheSidePanelKeepsItInsideTheMarginchecks every widthfrom
PanelFromto twice it, the terminals from 160 columns the finding names. A render at170 draws the panel inside a 5-column margin.
CLAUDE.md names
PanelFromand the margin rather than 184.numberCell,stackCellandbotCellsit besidechecksCellandreviewCell, and both layouts place them. The round stays an optionalcolumn in
prs listand a field the second line skips where empty: a table drops an emptycolumn, and a line of fields drops an empty field. CLAUDE.md says a field both show has one
builder.
boardColumnsandsecondLineand keeps where a new field goes.homeAsTildecuts athomeplus the separator, writes homeitself as
~, and leaves the path whole where home is unknown. Rejected: moving the helperinto a library prs and projects share. gotermboard and gotermstyle draw terminals and hold no
path helpers, and the rule asks for the segment boundary rather than one copy.