How prose-scrub got a scaffolder and real parsers
The build details behind prose-scrub: why a CLI made for an agent needed a new scaffolder, how the tree parsers replaced regular expressions, and why TypeScript 7 has no compiler API to lean on.
Contents
A page of text with three lines lit stands under a comb-shaped gate that catches them. The writer, a block with a nib, tilts toward the gate and lifts it, and the page slides through. A person walks in, pushes the page back under and lowers the gate again, and a fresh writer in grey comes in beside the page for the rewrite.
A CLI for an agent needed a new scaffolder
I suggested building it with mkcmd, my scaffolder for TypeScript CLIs. Claude found that mkcmd's init only works through interactive prompts and started reaching for expect to drive them. Then I remembered what I'd built: "mkcmd is for humans, not agents. You wouldn't like it." Twenty seconds later: "Wanna duplicate and refactor mkcmd to be useful for agents :3".
That became mkcmd-agent. Every input is a flag, --json prints exactly one object, exit codes are 0, 1 and 2, and a describe command lists every command and flag as JSON. The fork also fixed six old mkcmd bugs, from a version flag that read the wrong package.json to exiting 0 when given no command.
Two doors side by side. At the left door, which has one key that nothing presses, a writer block waits and nothing happens. At the right door, which has a row of five slots, the writer pushes a peg into every slot at once and one small sheet of JSON slides out.
To test the docs, I had a Haiku agent build a CLI from them alone. It built a small notes tool with eleven tests and found one real problem: the docs suggested a shell alias, and an agent runs every command in a fresh shell, so the alias is gone by the next call. Two of its complaints were wrong (both things were documented), and rerunning its CLI turned up two framework bugs we fixed.
Building prose-scrub on top of it found more. A failing check has to return its findings along with the error, so errors gained a details field. The hint that tells you where to cd after init got a fix, and the test for that fix caught a macOS quirk where /var is a symlink to /private/var. Later I put it plainly: "Mkcmd was a brief flirtation with tui development and ai-free software". mkcmd-agent 0.2.0 removed prompts entirely. Its docs are at mkcmd.mackenziebowes.com.
"Show me the part of prose-scrub that parses trees"
The rhythm findings looked odd to me, so I asked whether they were coming from code comments, and then: "Show me the part of prose-scrub that parses trees". There wasn't one. prose-scrub found prose by blanking out anything that looked like code with regular expressions. In TSX that counted CSS values and terminal commands as sentences. In Markdown it glued every heading onto the sentence after it. The rhythm numbers, including the 0.61 a subagent had just reached on my résumé, weren't trustworthy, and Claude stopped that subagent.
The rewrite uses real parsers: mdast for Markdown and MDX (with GitHub tables and front matter), parse5 for HTML, and the TypeScript compiler for TS, TSX and JS. Rules now run on whole text blocks with an exact source position for every character, so a phrase that wraps onto the next line is caught. HTML entities and JavaScript escapes are decoded, so \u2014 in a string counts as the dash it prints. Alt text, meta descriptions and front-matter titles are checked, and types, class lists and imports are skipped because the parser says what they are. Each of the six new tests failed on the old code first.
The same page twice. On the left a broad brush sweeps across it: a heading fuses with the sentence after it into one long bar and a block of code is counted as prose, both in tomato. On the right a parser cuts the page into separate blocks with gaps between them, and only the prose blocks light up.
Rhythm now skips headings, list items and table cells, which looks like the relaxation all over again. The difference is evidence. My deslop skill's rhythm script deletes exactly those lines before it measures, so the parser now matches the reference. And against the old extractor, on my résumé, mkcmd-agent and this site, no rule found fewer problems and rhythm flagged 6 more files, 16 instead of 10. The mkcmd-agent README, the file the first relaxation let through, still failed. A follow-up fix stopped labels like "FIG. A" from counting as sentences, and that took rhythm to 20 flagged files.
TypeScript 7 has no compiler API
Installing typescript brought in 7.0.2, the native Go port, and in it createSourceFile was undefined: the npm package ships the tsc binary and no JavaScript API. Claude told me it was pinning TypeScript 5. I wrote back: "No compiler API? We had to pin 2 versions back? wth". It checked the registry, found 6.0.3, the last release written in JavaScript, and moved to that. The full API was there and the typecheck passed.
A plug labelled createSourceFile approaches a box marked 7.0.2, which is a plain binary with a blank face, and stops and recoils. It moves to a box marked 6.0.3, which has a socket cut out of it, and seats there as the socket lights up.