A lot of real engineering reasoning gets written down in two places: pull request reviews and ticket comment threads. "Why not just use a queue here?" "Because ordering matters for refunds." That exchange is the most useful sentence in the whole change, and it disappears the moment the PR is merged.
You can search for it later, in theory. In practice you don't remember which PR it was, or the repo got migrated, or the ticket tracker changed. This post is about keeping the useful parts somewhere you control, without turning code review into a documentation chore.
I'll use GitHub and Linear in the examples and Obsidian for the notes, but the idea carries over to GitLab, Jira, or a plain folder of Markdown.
What's worth keeping
Most reviews produce nothing worth saving, and that's fine. The things worth keeping fall into a few buckets:
- Decisions: "we chose X over Y because Z". Often buried in a reply to a review comment.
-
Gotchas: behavior that surprised someone. "Postgres
NULLis ignored byCOUNT(column)but not byCOUNT(*)." - Patterns: "this is the third PR where we've needed a retry wrapper." A repeat is a hint that something should exist.
- Context from tickets: the business reason for a change, which almost never makes it into the code or the commit message.
Capture right after the review
The cheapest moment to capture is right after you submit (or respond to) a review, while the details are in your head. Ask one question: did a decision or a surprise happen here? If not, move on. If yes, spend three to five minutes on a short note.
An illustrative example:
---
type: pr
repo: web
pr: 482
ticket: ENG-217
date: 2026-09-18
tags: [dependencies]
---
# Replace lodash.groupBy with a local helper
**Decision:** Use a 10-line `groupBy` in `lib/collections.ts` instead of importing lodash.
**Why:** It was the only lodash function left in the client bundle. The local version is typed the way we use it.
**Trade-off:** One more function we own and test.
**Link:** https://github.com/acme/web/pull/482
Short, dated, linked. The link matters most: the note is an index into the original discussion, not a replacement for it.
Tickets: the reasoning is in the comments
Ticket descriptions tend to say what. The why, and the changes of direction, are in the comment thread: a PM explaining why the scope changed, a lead pushing back on an approach. When you close a ticket that had a real discussion, summarize it in two or three lines:
# ENG-217 Faster search on the orders page
**Problem:** Support staff wait 5+ seconds searching orders by email.
**What changed during the ticket:** Started as "add an index"; turned out the query was doing a case-insensitive scan. Switched to a lowercased generated column.
**Related:** [[PR web#482]], [[PR api#1190]]
That second line is the part nobody will reconstruct later.
Pulling the week's activity with scripts
Capturing in the moment is best, but a weekly sweep catches what you missed. The GitHub CLI can list the PRs you reviewed recently:
# PRs in the current repo you reviewed, closed in the last 7 days
since=$(date -d '7 days ago' +%F) # macOS: date -v-7d +%F
gh pr list --state closed --limit 50 \
--search "reviewed-by:@me closed:>=$since" \
--json number,title,url \
--jq '.[] | "- [#\(.number)](\(.url)) \(.title)"' >> "notes/reviews/week-$(date +%F).md"
gh pr list has no --reviewed-by flag, but its --search option accepts GitHub's search qualifiers, and reviewed-by:@me is one of them.
To read the discussion on a specific PR:
# Description, conversation comments and review summaries
gh pr view 482 --json title,body,comments,reviews
# Inline review comments (the line-by-line ones) come from a different endpoint
gh api repos/{owner}/{repo}/pulls/482/comments --jq '.[] | "\(.path):\(.line) \(.user.login): \(.body)"'
gh api fills in {owner} and {repo} from the repo you're in. The inline comments are where most of the interesting back-and-forth happens, and gh pr view doesn't include them.
Exporting Linear tickets
Linear has a GraphQL API at https://api.linear.app/graphql. With a personal API key (Settings > API), a small script can turn recently completed issues into notes:
import json, os, pathlib, urllib.request
QUERY = """
query($since: DateTimeOrDuration!) {
issues(first: 50, filter: { completedAt: { gte: $since } }) {
nodes {
identifier title url description estimate
comments { nodes { body createdAt user { name } } }
}
}
}
"""
def fetch(since="-P7D"): # ISO 8601 duration: last 7 days
req = urllib.request.Request(
"https://api.linear.app/graphql",
data=json.dumps({"query": QUERY, "variables": {"since": since}}).encode(),
headers={"Content-Type": "application/json",
"Authorization": os.environ["LINEAR_API_KEY"]},
)
with urllib.request.urlopen(req) as r:
return json.load(r)["data"]["issues"]["nodes"]
out = pathlib.Path("notes/tickets")
out.mkdir(parents=True, exist_ok=True)
for i in fetch():
lines = [
"---", f"type: ticket", f"ticket: {i['identifier']}",
f"estimate: {i['estimate']}", "---", "",
f"# {i['identifier']} {i['title']}", "", i["url"], "",
"## Description", i["description"] or "", "", "## Discussion",
]
for c in i["comments"]["nodes"]:
who = c["user"]["name"] if c["user"] else "unknown"
lines += [f"- **{who}** ({c['createdAt'][:10]}): {c['body']}"]
lines += ["", "## Summary", "_Two lines: what changed and why._"]
(out / f"{i['identifier']}.md").write_text("\n".join(lines))
Two notes on this:
- Linear's
estimateis in points on whatever scale your team configured, not hours. Don't do "estimate vs actual hours" math on it. - The script dumps raw comments. The valuable part is the
## Summaryyou write by hand afterwards; a vault full of unread exports isn't a second brain, it's a backup.
Keep the API key in an environment variable, never in the vault.
Linking it together
Once PR notes and ticket notes exist, wiki links connect them. A ticket note links to its PRs, a PR note links to its ticket and any decision record, and backlinks give you the reverse direction for free. When you later wonder why the orders search uses a generated column, you land on the ticket, which links to both PRs and the reasoning.
If you use Dataview, a simple dashboard helps during the weekly sweep:
```dataview
TABLE repo, pr, ticket, date
FROM "reviews"
WHERE date >= date(today) - dur(7 days)
SORT date DESC
```
Keeping it from going stale
Advice from an old PR can be wrong now: a dependency got upgraded, the team changed its mind, the service was rewritten. Two cheap habits help:
- Mark superseded notes instead of deleting them. Add a line at the top: "Superseded by [[new note]]: we moved from Redux to Zustand in 2026." The old reasoning stays findable and nobody follows it by accident.
-
Add a
reviewed:date to notes you rely on. When you use a note and it still holds, bump the date. Notes nobody has touched in a year are the first to question.
The weekly sweep
Once a week (Friday works), do a quick pass:
- Run the
ghcommand above and skim the list for reviews where something was decided that you didn't capture. - Look at tickets you closed this week and write the two-line summary for any with a real discussion.
- Scan the week's notes for repeats: the same gotcha twice is a candidate for a lint rule, a helper or a README section.
Expect around half an hour, less on quiet weeks. The in-the-moment captures do most of the work; the sweep catches what slipped through.
Start small
You don't need the scripts on day one. Start with the habit: after any review where a decision or surprise came up, write five lines and paste the link. After a month you'll have a small, searchable record of why your codebase looks the way it does, which is more than most teams have.
Dev Second Brain, my Obsidian vault for developers, includes a PR review template if you want somewhere to start.
More templates and a free Dataview starter pack are at forge.engelailabs.com.
Top comments (0)