GitHub Issues are the source of truth. Any agent with the gh CLI, authenticated
with repo scope against this repository, can run every operation below directly —
no local clone required.
Commands below assume export GH_REPO=brfid/readings, which lets gh drop the
repeated --repo flag.
- Labels are the queryable dimensions; everything else is a comment. Type and started-or-not are labels. Never store the same fact in two places.
- Store only what you can't derive. The issue already records when it was logged, when it closed, and how. Don't restate any of that.
- The body is the author, then optionally the link. One line always, a second line only when there is a URL. Nothing else, ever.
- No files. No per-reading files or folders, no saved copies. The issue is the whole record.
type:{book,paper,web}reading— you've started it
| Issue state | Close reason | Meaning |
|---|---|---|
| open | — | queued |
open + reading |
— | in progress |
| closed | completed |
finished |
| closed | not planned |
abandoned |
Closing touches no labels, so reading stays on a finished item as a record that you
started it. That's why the in-progress query is --label reading with no state flag —
gh issue list is open-only by default, and the closed ones drop out on their own.
A reread is a reopen. It needs no flag: the issue goes back to open, reading goes
back on, and the label-change events record that it happened twice.
Line 1 is the author's name, bare, always present:
Ursula K. Le Guin
No **Author(s):** prefix. Multiple authors are one comma-separated line; et al. is
fine. The title lives in the issue title.
Line 2 is a bare URL, and exists only when there is one:
Martin Fowler
https://martinfowler.com/bliki/ContinuousDelivery.html
No **Location:** prefix either. A field name earns its bytes only when there is a
second field to tell it apart from, and a URL tells itself apart — https:// is the
field name. That's also why nothing here can go blank: an item with no link has a
one-line body, not an empty field.
The link lives in the body rather than a comment because the body is what reaches people. A GitHub feed card is generated by the issue-opened event and carries the title plus the opening of the body; comments generate no card at all. Line 2 is inside the excerpt window on any item with a normal-length author line.
What counts as a link. A way to read the text, not a way to find out about it. Papers and web items: the canonical URL. Books: only a full text or a substantial selection — an author's posted chapter, an archive scan. Never a publisher, bookstore, Goodreads, or Wikipedia page. If no such link exists, there is no second line, and for most books there won't be.
Body text is searchable, so this is a real queryable path, and it means the link is to this domain rather than this domain got mentioned somewhere:
gh issue list --state all --search "martinfowler.com in:body"Everything that isn't the schema: where a physical copy is, reactions, quotes worth keeping, follow-ups. A comment exists only once you write one, so there is nothing blank to maintain.
A copy location that isn't a URL — Books app, a shelf, a library — is a comment. Only
URLs go on line 2, which is what keeps reading the body a matter of position and never
of pattern-matching.
for label in type:book type:paper type:web reading; do
gh label create "$label" --force
doneKeep the label list at exactly these four. Every open issue is assumed to be a reading item — that's what makes the queued query below a single term — so meta-issues about the tracker itself belong somewhere other than this repo.
# Add item — no link
gh issue create --title "TITLE" --label "type:TYPE" --body "NAME"
# Add item — with a link (a literal newline inside the quotes)
gh issue create --title "TITLE" --label "type:TYPE" --body "NAME
URL"
# Find an item's number — an exact title match wins, else the top hit
N=$(gh issue list --search "TITLE in:title" --json number,title \
--jq '(map(select(.title=="TITLE")) + .)[0].number')
# Start reading
gh issue edit $N --add-label reading
# Finish
gh issue close $N --reason completed
# Abandon
gh issue close $N --reason "not planned"
# Reread
gh issue reopen $N && gh issue edit $N --add-label reading
# Add a link to an item that already exists.
# --body replaces the whole body, so keep line 1 and append line 2.
gh issue edit $N --body "$(gh issue view $N --json body --jq '.body|split("\n")[0]')
URL"
# Comment (your commentary lives here, never in the body or a file)
gh issue comment $N --body "COMMENT"
# Timeline (the "when" — logged / started / closed)
gh api "/repos/brfid/readings/issues/$N/events" --jq '.[] | "\(.created_at) \(.event) \(.label.name // "")"'The exact-match preference in the lookup matters: GitHub ranks title search by relevance,
not by exactness, so a plain .[0] returns Grokking Continuous Delivery when you asked
for Continuous Delivery. Add --state all when the item may already be closed —
gh issue list is open-only by default.
Note that closing from the web UI can leave the reason unset; a closed issue with no reason has no determinable status. Close with the CLI, or set the reason afterwards.
# Status
gh issue list --label reading # in progress
gh issue list --search "is:open -label:reading" # queued
gh issue list --search "is:closed reason:completed" # finished
gh issue list --search 'is:closed reason:"not planned"' # abandoned
# Type
gh issue list --label type:paper --state all
# Search
gh issue list --search "keyword in:title"
gh issue list --search "keyword in:body" # author, or the link's domain
gh issue list --search "keyword in:comments" # your commentary
# Every item as a record: number, title, author, link (null when there isn't one)
gh issue list --state all --json number,title,body \
--jq '.[] | {n:.number, title, author:(.body|split("\n")[0]), url:(.body|split("\n")[1] // null)}'