The scripts I should have written first
On this page
On 29 October I built this site in one day with Claude. Every piece of work got its own thread: the about page, the paper cursor, the projects page, the Khmer and Spanish translations. By the evening, 33 pull requests had been merged.
At the end of the day I asked Claude to read back through every thread and look for chores that kept coming back. There were a lot of them, and almost none of them were written down anywhere a later thread could reuse.
The same chores in every thread
This site has a rule that makes it look like a page from my notebook: every line of text sits on a ruled line, 32px apart. Before the pull request that set that up, only 72 of 1,502 lines of text sat on a rule. The thread that fixed it wrote its own measuring script, checked 16 pages at six widths, and then the script went away with the session.
Every thread after that had to prove it hadn’t knocked anything off the lines, and every one did it differently. Some ran a one-line check from AGENTS.md. Some looked at screenshots. Bugs still got through:
- Tag stickers on the article cards hung over the dates below them. I spotted that one myself, on the Khmer home page in dark mode.
- The like count sat 12px off its line, and the header scrolled sideways on 320px phones. The code review caught both.
- A new “work with me” card pushed the footer off its line when two branches met.
The other chores looked the same. Each page needed screenshots in three languages, light and dark, on a desktop and a phone: twelve per page, set up by hand every time. Every pull request needed its preview link and a risk tier, written out by hand. Agents couldn’t hear the sounds they added, so they asked me to check by ear.
An agent will happily rebuild a throwaway tool every time you ask. It doesn’t mind. But each rebuild checks slightly different things, and none of them survive the session.
Three scripts
So the three most common chores are now scripts in the repository, and AGENTS.md tells every agent to run them.
Checking text against the rules
bun run lines builds the site, serves it, opens every page in Chromium at five widths and measures every line of text against the rules.
bun run lines # every page, at 320, 375, 768, 1024 and 1280px
bun run lines --quick # only 320 and 1280px
bun run lines /about/ --darkIt measures a line the same way the page’s own fallback script fixes one. A run of text starts at the font’s ascent above its baseline, and the rules sit 22px down every 32px from the top of the page:
for (const rect of range.getClientRects()) {
if (rect.width < 1 || rect.height < 1) continue;
const baseline = rect.top + scrollY + up;
let miss = (((baseline - rules.top) % rules.gap) + rules.gap) % rules.gap;
if (miss > rules.gap / 2) miss -= rules.gap;
const line: Line = {
text: node.textContent!.trim().replace(/\s+/g, ' ').slice(0, 60),
where: where(parent),
miss: Math.round(miss * 10) / 10,
baseline,
box: {
x: rect.left + scrollX,
y: rect.top + scrollY,
width: rect.width,
height: rect.height,
},
};
// …count the line…
if (Math.abs(miss) > tolerance) result.off.push(line);
// …then look for a sticker on top of it.
}It also fails a page when a sticker covers a line of text, which is the bug from the article cards. I put the old spacing back to try it, and it named the date and the sticker on top of it.
The first time I ran it on the live site, it found a bug none of us had seen. On the about page, the article titles on the “How I work” cards sat 2px below their lines. The handwriting is bigger than the text around it, so it pushed the whole line down:
✗ /about/ at 375px
+2px off "How I'd do pull request review today" (… > cite.font-hand.text-xl)Each problem also gets a close-up, with the rule drawn in blue and the text’s baseline in red. With dark mode on too, a full run measures over 10,000 lines on every page of the site in about 40 seconds.
Taking the screenshots
bun run shots takes every page you name in English, Khmer and Spanish, light and dark, at 1280 and 375px. Then it lays them out on a sheet for each page and screen size, so a pull request shows six shots in one picture instead of six files.
bun run shots /about/ --before main --mark 'cite.font-hand' --note 'back on its rule' --widths 1280,375 --publish--before main builds the main branch next to yours and puts the old and new pages side by side. --publish pushes the sheets to the branch the screenshots live on and prints the Markdown to paste into the pull request.
I also wanted the proof to point at what changed. --mark takes a CSS selector: it rings that element in every shot, draws an arrow to it with the --note, and puts close-ups of it at the top of the sheet, before and after.
It opens the Khmer and Spanish pages at their own addresses. The home page picks a language for each visitor, and it treats a headless browser as a bot, so an agent that only opened the home page always got English.
Writing up the pull request
bun run pr check runs everything that has to pass before a push: the branch name, the commit messages, formatting, type checks, the build and a quick run of the line check. bun run pr body fills in the pull request template with the preview link and a suggested risk tier.
const HIGH = [
/^worker\//,
/^wrangler\.(jsonc?|toml)$/,
/^\.github\/workflows\//,
];
// Agent instructions change how every later change gets made.
const MEDIUM = [/^\.claude\//, /^AGENTS\.md$/];
const LOW = [
/^src\/content\//,
/^src\/assets\/articles\//,
/^src\/i18n\/ui\.ts$/,
/^docs\//,
/\.md$/,
];The tier comes from the paths a change touches, as in my pull request review setup: the highest tier wins. The script can’t tell when a change adds a new sign-in or an outside integration, so it says so, and that call stays with the author.
What I learned building them
Turn off what you aren’t checking
The first version took over a minute for 80 page views. The slow part turned out to be the 3D avatar. Headless Chromium draws WebGL in software, so every frame on a page with the avatar took most of a second. The line check doesn’t need the avatar, only its frame, so it now starts Chromium with 3D turned off, and each page takes half a second instead of four and a half.
export async function launch({ webgl = true } = {}): Promise<Browser> {
try {
return await chromium.launch({ args: webgl ? [] : ['--disable-3d-apis'] });The screenshot script keeps 3D on, because there the avatar is the point.
Fake what isn’t there
The like button needs a Cloudflare Worker that only runs in production, so on a local build it stays hidden. Threads used to fake its answer by hand, each a little differently. Now the scripts’ small server answers with the same made-up count every time, and every screenshot shows the button.
Escaped bugs become checks
In my pull request article I said that when a bug gets past review, the fix should add whatever would have caught it. The stickers over the dates are now a failing check instead of something I happen to spot in a screenshot. So is any page that scrolls sideways.
The agents now follow the same rule. Next to the scripts sits a skill that walks an agent from a new branch to an open pull request, and its last step asks what the change taught. A bug that got past the checks becomes a new check. A gotcha that cost a retry becomes a one-line lesson in the skill, and once a script handles it, the lesson goes. Every change to the skill lands in the same pull request as the code, so I review it like the code.
There are six more chores on the list, like waiting for the preview to rebuild before saying it’s ready, and a sound check for ears that aren’t there. They’ll come one at a time, when they’ve annoyed me enough.
What does your agent keep rebuilding from scratch?
0likes
thank you!