What this session did
read-thru started the session as a folder of files inside a personal workspace. It ends it as a published MIT repo and a Claude Code skill, make-read-thru, that an agent invokes to turn code into a guide a reviewer can vouch for.
Three moves got it there. First it graduated: renamed readthrough โ read-thru, de-coupled from the workspace, pushed to its own GitHub repo, wired back in as a submodule. Then it was repackaged as a skill โ the 411-line gen.py split into a documented package, a one-command CLI added, the authoring API lightened, and bundled references and golden examples written. Finally it moved to live where a skill belongs: .claude/skills/make-read-thru, the repo and the skill one and the same directory.
This guide walks the code that resulted. Read it top to bottom and you should be able to vouch for how the skill works โ and it was built with the skill itself.
The public surface
An author touches one thing: the helpers re-exported from read_thru. The package is split along clean seams โ a model, a markdown subset, code rendering, page assembly, a CLI โ but the surface stays small and flat.
Five modules behind one import; the DSL an author actually uses.
read_thru/__init__.py ยท 39 lines
โฏ 22 lines hidden (lines 1โ22)
A Section, with its ceremony made optional
The old Section demanded an id, an act, a number, and a technique on every entry. That fit a 50-section epic; it was dead weight for a four-section PR. Now only title and blocks are required. The id defaults to a slug of the title, so anchors stay stable across rebuilds, and the rest default to empty.
A deterministic slug, then every structural field optional.
read_thru/model.py ยท 61 lines
โฏ 19 lines hidden (lines 1โ19)
โฏ 23 lines hidden (lines 39โ61)
A small, safe markdown subset โ and a portability fix
Authored prose is never trusted as HTML; everything outside inline-code spans is escaped exactly once. The subset is deliberately tiny. The list-marker patterns below also carry a real fix from this session: they used to live inside the f-strings that build each <li>, and a backslash inside an f-string expression is a syntax error before Python 3.12. Widening support to 3.10 surfaced it.
Markers compiled once, out of the f-strings โ valid back to 3.10.
read_thru/markdown.py ยท 135 lines
โฏ 21 lines hidden (lines 1โ21)
โฏ 40 lines hidden (lines 24โ63)
โฏ 62 lines hidden (lines 74โ135)
Verbatim source, foldable
This is the heart. Each file is highlighted by Pygments, then split so that every source line becomes its own row โ and no <span> ever crosses a line boundary, which is what makes per-line folding and deep-linking possible.
Tokens that span newlines are split; one HTML string per line.
read_thru/code.py ยท 166 lines
โฏ 33 lines hidden (lines 1โ33)
โฏ 114 lines hidden (lines 53โ166)
Rows that an author folds are never dropped โ they collapse into a <details> stub that still holds every hidden line. That is the contract the build later checks: present, just folded.
Folded runs grouped into a labelled stub; the lines stay in the DOM.
read_thru/code.py ยท 166 lines
โฏ 127 lines hidden (lines 1โ127)
โฏ 21 lines hidden (lines 146โ166)
The table of contents adapts
The sidebar reads the sections it's given. If any one sets an act, the entries group under act headings the way the froot guide does. If none do โ the normal case for a PR or a snippet โ the list is flat, with no empty group chrome.
One branch on whether any section declares an act.
read_thru/page.py ยท 122 lines
โฏ 25 lines hidden (lines 1โ25)
โฏ 76 lines hidden (lines 47โ122)
The completeness contract
A whole-repo guide makes a promise: every file, in full. build keeps it honest. Pass the list of files that must appear and it reports any that no code() call rendered, plus the total number of code rows emitted.
Write the file, then check every declared file actually rendered.
read_thru/page.py ยท 122 lines
โฏ 109 lines hidden (lines 1โ109)
One command
Before, every guide needed a hand-written build.py that set three module globals and called build(). Now the CLI does the wiring. The one subtlety it gets right: SECTIONS is built when the content module is imported, and each code() reads a file off SOURCE_ROOT as it runs โ so the source root must be set before the import, not after.
Resolve paths, then import the content, then render.
read_thru/cli.py ยท 144 lines
โฏ 87 lines hidden (lines 1โ87)
โฏ 30 lines hidden (lines 115โ144)
The agent contract
What makes this a skill and not just a library is the frontmatter below. The description is the whole triggering mechanism โ it tells Claude when to reach for the skill, phrased to fire on "explain this PR" even when no one says the word "read-thru".
Name + a deliberately pushy description: the skill's entire trigger.
SKILL.md ยท 98 lines
โฏ 86 lines hidden (lines 13โ98)
Everything heavier lives one level down, loaded only when needed: the methodology in references/process.md, the DSL in references/dsl.md, the sizing tiers in references/depth-and-scope.md, and three golden examples to calibrate against. The skill body stays short on purpose.
What makes it a skill
The engine was always generic. Turning it into a skill was less about code than about contract: a tight SKILL.md that says when to use it and how, references that hold the depth, goldens that show the bar, and an interface an agent can drive in two steps โ write a content module, run one command.
One thing was deliberately removed. An earlier draft taught the skill how to deploy a guide to a personal static site and link it from a PR. That's a workspace concern, not read-thru's job, so it now lives only in the workspace's own instructions. The skill produces one self-contained HTML file and stops there. What you do with it next is up to you โ including, for instance, reading this.