Skip to content

garden pr explain

A walkthrough is a proof-carrying reading guide you (the author) write for a PR. Instead of a reviewer re-deriving what your change does from the diff, you order the change into blocks, say what each block does, and point at proof it does that. Review becomes adjudicating evidence, not guessing intent.

Blocks form a tree — like a document outline. A block’s claim is established by its own proof plus the claims of its child blocks. The top-level blocks are the roots; together they prove the PR as a whole (the summary). So a high-level claim (“the merge queue is now crash-safe”) sits at the top, and the pieces that prove it (“state is persisted before the merge”, “restart resumes from that state”) nest underneath, each with their own proof, recursively.

You have the session, repo, and implementation context no reviewer has — so you are the best source for why this is correct. This is deliberately not a session dump: it’s the curated, minimal artifact you choose to stand behind.

Write one after every garden push (per revision).

garden pr explain <pr> # prints a YAML template from the diff
…edit it…
<edited> | garden pr explain apply <pr> -f - # write it (upsert)
garden pr explain show <pr> # read it back (block order)

There is no editor: fill the printed template and pipe it back with apply -f -.

The scaffold prints one block per changed file, but that is only a starting point. Re-block freely:

  • Split a file into several blocks when its hunks do different things.
  • Merge trivial blocks.
  • Drop boilerplate (imports, renames, generated code) — you don’t have to explain every hunk. What you leave out is itself a signal reviewers can ask about.
  • Nest blocks under a children: list to build the tree: put the pieces that prove a claim under that claim. A parent can have its own scope and proof too — it’s proven by both.
  • Order siblings the way a human should read them. Sibling order is the review path — it is never re-sorted.

The tree is fixed by your nesting; layout chooses how it flattens into the main reading pane (the sidebar outline is always top-down):

  • top-down (default) — each claim before its sub-proofs. Conclusion-first.
  • bottom-up — the building blocks before the claim they support. Foundation-first.
  • sandwich — the summary up top, then bottom-up construction, then a closing synthesis that ties it back to the PR.

Pick the one that best fits this change; there’s no universally right answer.

A big or mixed file almost always wants several blocks. Example — one file that does two unrelated things becomes two blocks:

blocks:
- id: b1
title: parse the keep/drop plan
scope:
- { path: cli/plan.go, lines: 40-90 } # just the parser
intent: turns the edited plan text back into a per-hunk keep/drop set.
attention: review
risk: low
proof:
- test: plan_test.go::TestParsePlan
- id: b2
title: reject a structurally-edited plan
scope:
- { path: cli/plan.go, lines: 95-120 } # the validation, same file
intent: errors if the agent rewrote the diff body instead of flipping
verbs, so a corrupt plan can't silently commit the wrong thing.
attention: scrutinize
risk: high
proof:
- test: plan_test.go::TestRejectsCorruption
- note: no test yet for a plan whose hunk headers were reordered.
summary: what this change does + the single most important thing to know
confidence: medium # low | medium | high — your own certainty
layout: top-down # top-down | bottom-up | sandwich
critical_path: [b2] # block ids — "5 minutes? read these"
blocks: # top-level roots — together they prove the PR
- id: b1 # unique across the whole tree
title: short human title
scope: # the code this block covers
- { path: pkg/x.go, lines: 40-80 } # whole file if lines omitted
- { path: pkg/y.go }
intent: what it does and why — the claim being proven
attention: review # skim | review | scrutinize
risk: low # low | medium | high (blast radius)
proof: [] # see below — a list, several allowed
unproven: what you did NOT verify — the honest gaps
children: # sub-blocks that prove this one (the tree)
- id: b1a
title: … # same shape, recursively

attention (how hard to look) and risk (how bad if wrong) are separate axes. unproven is the highest-value field: an honest gap list beats any prose.

summary, intent, unproven and note proofs are Markdown; bullet lists are ordinary content.

A block’s prose must be shorter than the code it points at.

A walkthrough that takes longer to read than the diff has no reason to exist. Reviewers abandon walls of text, and the artifact is worthless unread. These are budgets, not suggestions:

field budget
title ≤ 50 characters. A headline: no trailing punctuation, no leading article. Keep the identifier — ChunkDiskMapper.Truncate, poll_proceed() — and drop the prose around it.
intent one line, ≤ 20 words. If it has 2–3 separable points, use - bullets of ≤ 12 words instead, 3 maximum.
unproven bullets only, 2 maximum, ≤ 15 words each. More gaps than that? Keep the two that most change what a reviewer does.
summary ≤ 50 words, ending on the single most important thing to know.

intent must add what title doesn’t — the consequence, the constraint, the gotcha. If it only rephrases the title, leave it empty.

Empty beats filler. A blank unproven is honest; a padded one trains reviewers to skip the field.

  • Process narration — “I grepped”, “I checked”, “I verified”, “not verified:”, “I could not establish”. The field is called unproven; saying it again spends the reader’s only resource.
  • Restating what the code plainly shows. The diff is one panel away.
  • Hedging — “arguably”, “as far as I can tell”, “that reads deliberate but I cannot confirm it”. Either it is worth saying flat, or cut it.
  • Inline file:line citations. scope and proof already carry locations.
# 41 words — three sentences to say one thing
intent: >
Adds a second accumulator alongside `removed_selectors`.
`removed_selectors` keeps its existing meaning — "these make config
resolution fail" — and the new `removed_ignored_rules` collects the
selectors that are now downgraded to a warning. Splitting the sets rather
than adding a flag keeps the pre-existing error path byte-identical.
# 16 words
intent: A second set, so the pre-existing error path stays byte-identical
instead of growing a flag.
# 58 words of narration
unproven: >
Two things I can establish and neither is tested. (1) The message text
changes: this site stores `err.to_string()` raw, whereas
`Message::from_parse_error` prefixes `"SyntaxError: "` … (2) The only
tests touching `not_json.ipynb` live in `crates/ruff_notebook/…`
# 21 words
unproven: |
- Malformed `.ipynb` now renders without the `SyntaxError: ` prefix.
- Nothing exercises `from_source_error`'s rendering, before or after.

Quote any value containing : — YAML reads a bare colon-space as a mapping key, so caption: the gate — ErrInvalidSample: …`` is a parse error.

Each proof entry is exactly one kind:

proof:
- test: path/foo_test.go::TestName # a test that exercises it; CI runs it
- command: "go test ./internal/x/" # a command a reviewer can run
- attachment: ./demo.gif # a local screenshot/recording/data
caption: dark→light, persists on reload # file — uploaded on apply
- pointer: { path: pkg/x.go, lines: 40-80 } # any code, in the diff or not
caption: reuses the battle-tested path
- external: https://ci.example/run/123 # a CI run / deployed preview / issue
- note: reasoned only — see unproven # the weakest; pair with unproven

Garden does not verify proofs — CI runs the tests, the CLI handles attachments, and reviewers follow the pointer. Point at proof that already exists. To say a block is proven by another block, make it a child — that’s the tree.

You do not have to cover every file or hunk. Cover what a reviewer needs to understand and trust — the important logic, the risky parts, the load-bearing changes — and leave out routine boilerplate (imports, renames, formatting, generated code). Whatever no block covers is shown to the reviewer as an “unaccounted for” list, so nothing is hidden; an uncovered hunk is a signal that it didn’t need explaining, not a gap you must fill.

  • A walkthrough is written per revision and is frozen once the PR merges. A new push means a new revision; write a fresh one (you can reuse the previous revision’s as a starting point).
  • Any repo member can write or replace it; the author is recorded from your credentials.