PullMD Part 2: From Spec to Running Container
The spec from Part 1 was done by mid-morning of March 2. Architecture thought through, API design clear, comment format decided. I handed the spec to Claude Code expecting it to spin up an Express server, implement the Reddit fetch logic, and eventually write a test or two along the way. What happened instead: Claude Code wrote four test files first. No server, no Reddit logic, no Express. Four tests. The tests coming first was neither luck nor magic — I’d been working with the superpowers skills for a while, so Claude Code was no new territory. What’s noteworthy is rather why it stuck so consistently to test-first.
Four Tests Before a Single Line of Production Code
In a good half hour late on the evening of March 2, four test files existed in the repo before a single line of Reddit logic was written. They covered exactly what would later make up the architecture of the tool: URL normalization as its own concern, the Reddit JSON fetch via mocks, an HTML fallback parser for when the JSON route gets blocked, and a separate integration test that only makes real Reddit calls on demand.
What those four files describe together isn’t “a tool that fetches Reddit.” It’s a tool that fetches Reddit in multiple fallback stages, that normalizes URLs reliably before starting, and that separates fast unit tests from slow integration tests. The tests define the architecture — before a single line of implementation exists.
What Actually Happened
While researching for this article, I went back into the backup logs to see what Claude Code actually did that night. The picture is more concrete than “Claude Code decided on tests first” — and more useful for the reader.
The workflow that produced the result was a pipeline of several interlocking superpowers skills:
- The spec from the morning’s claude.ai concept chat was broken down in Claude Code with a planning skill into numbered tasks — “Task 1: npm dependencies”, “Task 2: URL Normalization Module”, “Task 3: Redirect Resolution”, and so on up to roughly Task 6.
- For each task, a fresh subagent was dispatched (
subagent-driven-developmentskill). Each subagent only gets its own task description as context — not the rest of the session, not other tasks. - Each implementer subagent was explicitly instructed to work “following TDD” — meaning the inner loop is
test-driven-development: write the failing test, verify it fails, write the minimal implementation, verify the test passes, commit. - After each task, a spec-compliance reviewer subagent checks whether the implementation actually matches the plan.
In the backup logs you can see the first implementer briefs from that evening verbatim: “You are implementing Task 2: URL Normalization Module … with TDD” at 21:52 CEST, “Task 3: Redirect Resolution” at 21:58, “Task 5: Markdown Formatting” at 22:04, “Spec compliance review for Task 6” at 22:12. Four test-file mtimes between 21:53 and 22:26 — that’s not a single flash of insight, but the mechanical result of four parallel TDD implementer runs plus a reviewer stage.
The Skill Stack Behind the Pattern
“Tests first” wasn’t emergent default behavior here, but an enforced workflow property. Anyone using Claude Code on a new project who wants the same effect can get there with a stack of four skills from the superpowers plugin:
brainstormingclarifies before any implementation: what’s being built, what constraints, what tradeoffs. In my case the spec chat on claude.ai filled that role — the output was detailed enough to feed directly into a plan.writing-plansbreaks the spec down into numbered, self-contained tasks that can be described and tested individually.subagent-driven-developmentorchestrates: a fresh subagent per task, whose context contains only that task description. That prevents Claude from accidentally building Task 5 on inconsistent assumptions from Task 2.test-driven-developmentis the inner loop per task: failing test → minimal implementation → green test → commit.
If you’re reading this and thinking “I can say ‘tests first’ to Claude Code without those skills” — formally true. But without the plan structure, Claude Code has no anchors to write meaningful tests against. The skill stack enforces the structure that the tests then derive their meaning from.
The exact test filenames, mtime sequences, and complete subagent briefs from that night live as a historical note in my vault — for this article, the workflow arc is enough; the detail would smother the read.
Dockerfile, Compose, .dockerignore (March 4)
Two days later, on March 4 (09:30, mtime of the .dockerignore), the service moves into a container. The Dockerfile is a standard Node.js image, nothing special — no headless browser, no second process, one lean container. At this point the service needs exactly that: fetch Reddit JSON, convert to markdown, output. Node.js is enough.
The docker-compose.yml gets Traefik labels: a subdomain following my home network’s internal scheme, automatic cert resolver. The service runs on my Ubuntu Docker host behind Traefik, which terminates TLS and routes the subdomain. That’s the setup I use for all my self-hosted services — Traefik picks up a new container, reads the labels, issues a certificate, done.
The .dockerignore is small: node_modules/, data/, .git/. Nothing more needed. The service has no build artifacts, no local state that belongs in the image. A clean image that starts on the host with docker compose up -d.
The PWA with Web Share Target (March 8)
Six days after the first container deploy, the PWA layer arrives — and that’s the part I actually built the tool for.
On March 8, at 16:37, icon-192.png and icon-512.png land on the server. The icons had a distinctive theme color: Reddit orange, the bold orange from the Reddit logo. In hindsight, telling — the tool was genuinely still a pure Reddit tool at that point, and the color was the quiet proof of it. Three hours later, at 19:52, favicon.ico follows.
The manifest.json itself doesn’t get its mtime until March 20 — but the share_target block was conceptually planned from day one, it was already in the spec. The later mtime reflects a revision, not the initial creation. The key entry:
"share_target": {
"action": "/share",
"method": "GET",
"params": {
"title": "title",
"text": "text",
"url": "link"
}
}
What this means in practice: install the PWA, open the Reddit app, pull up a post, tap “Share.” The service appears in the Android share sheet — alongside WhatsApp, Signal, and the usual suspects. Tap the entry, the browser opens the PWA with the Reddit link already in the query parameter, extraction runs, markdown appears on screen. Copy button. Done.
The First Aha Moment on the Phone
I remember exactly the moment it worked for the first time. Reddit app, a long thread about a problem I was working on. Tapped “Share,” selected the service as target, brief pause — and then clean markdown on the screen. No navigation, no cookie banner, no sidebar. Just the post, the comments, indented by depth.
The thought that followed wasn’t “cool, it works.” It was more like: this is so much faster than I expected. The old workflow was: copy URL, switch to another tab, paste the URL, wait for output. Now it was: one tap in the share sheet, and the markdown is there. The difference sounds small. But in practice that’s the difference between a tool you use and one you keep forgetting.
The Tests Paid Off Later
Over the following weeks the test files kept growing alongside the code. Every time I changed something larger — URL handling, comment format, fallback logic — the tests broke first. That showed me what would have broken retroactively, before it hit the server. The test suite that came out of that first night almost as a byproduct of the skill stack proved its worth in practice — precisely because it wasn’t bolted on afterward, but co-formulated the architecture decisions in the first place.
What Came Next
For three weeks the service ran quietly — Reddit only, only on the phone, only for me. Then, on the morning of March 20, I wrote one sentence into a session that broke the project out of its cage. → PullMD Part 3: Beyond Reddit, Beyond Readability
Part 2 of 9 in the PullMD series.
← PullMD Part 1: Ctrl+A and the Junk Context | PullMD Part 3: Beyond Reddit, Beyond Readability →