Contents

SnarkCheck

SnarkCheck is a browser checkers game with an AI opponent that heckles you. Every move — yours and its own — gets a remark from a smug-grandmaster persona, and the game closes with a monologue recapping your turning points. It's a small, deliberately silly app.

It exists for a serious reason. A vibe-coded demo is a single prompt, a single file, and a happy path that works on the author's laptop. SnarkCheck was built the other way round: planned first, in one Spec4 round, then handed to a coding agent as seven phase files with the failure modes, the non-functional goals, the approved dependency list, and the deployment already decided. It is an example of what a production-deployable application looks like when the planning is done up front and the coding agent is handed real specs rather than a wish.

The round ran on Spec4 1.5.1 in September 2026, greenfield, with all six planning agents. The plan, the code, its tests, its CI pipeline, its Render blueprint, and every planning call's tokens and cost are committed together in its repository. Three things to know before reading on. The app is deployable, not hosted: the repository carries the Render blueprint and the deploy pipeline, and nothing is running at a public address. The cost figures cover the planning round; the coding agent that built the seven phases ran outside Spec4, and its tokens are not recorded. And the plan and the code do not agree everywhere; the last section says where.

What was built

Three surfaces, from stack.json and deployment-plan.md:

Surface What it is Where it runs
web_client React and TypeScript, built with Vite; the rules engine, board, and settings run in the browser Render static site
commentary_api Node and TypeScript; three routes, each a single model call with a deterministic fallback Render web service
litellm_proxy Self-hosted LiteLLM, Anthropic primary with OpenRouter fallback; holds the provider keys Render private service, unreachable from the browser

130 tests pass — 64 in the client, 66 in the server. ESLint, Prettier, and tsc --strict run clean. GitHub Actions runs lint, typecheck, tests, and build on every push and triggers each service's Render deploy hook only when all four pass on main; Render's own auto-deploy is off. Every model-backed route falls back to deterministic or curated output on timeout, provider error, schema violation, an illegal move, or a repeated line, and logs why. The repository's README.md carries the full account.

The round

Fifteen agents and sub-agents ran, in this order. Sub-agents file their calls under the parent in usage.json.

Ran Under Wrote
brainstormer vision.json
feature_speccer, scout, linker, composer Brainstormer feature_specs.json
tier_analyst Agentifier ai_catalog.json
agentifier, spec_drafter, cross_cutting_analyst, prioritizer Agentifier ai_features.json
designer design/mock.html, design/manifest.json
stack_advisor stack.json
phaser, phaser_seam Phaser phases/phase1.mdphase7.md
deployer deployment-plan.md

Agentifier's four entries in ai_catalog.jsonsnark_engine and its three members — all recommend single_call, and the decision agreed in each case. No tier_decision_rationale was recorded. A checkers commentator is a single-call problem; the interesting decision record on this site is the one on Built With Spec4.

The round ran in two sittings a day apart: 17 minutes for Brainstormer and Agentifier, 34 the next evening for Designer through Deployer. IMPLEMENTED is present.

What it cost

From .spec4/v0/usage.json, Spec4 1.5.1, LiteLLM 1.101.0. Tokens are the provider's figures; cost is LiteLLM's estimate from its community cost map, and your provider's bill is authoritative. Brainstormer includes its sub-agents; Agentifier includes tier_analyst; Phaser includes the seam check.

Agent Model · effort Calls Input Output From cache Cost
Brainstormer claude-opus-5 · default 13 74k 11k 85% $0.38
Agentifier claude-opus-5 · default 24 154k 37k 68% $1.26
Designer claude-sonnet-5 · medium 1 8k 15k 0% $0.17
StackAdvisor claude-sonnet-5 · medium 36 1.51M 31k 96% $0.76
Phaser claude-sonnet-5 · medium 4 150k 27k 61% $0.43
Deployer claude-sonnet-5 · medium 20 658k 19k 92% $0.44
Total 98 2.55M 140k 90% $3.44

"From cache" is cached_input_tokens over input_tokens, per agent. 2.31M of the 2.55M input tokens were read from cache and 212k were written to it. Every call has error: null; no call is missing usage or cost.

The largest line is Agentifier, a long dialogue on the stronger model. Phaser is fourth: four calls on claude-sonnet-5 at medium, with caching, for $0.43. StackAdvisor put 1.51M input tokens through 36 calls for $0.76, 96% of them cache reads. Designer's single call reads nothing from cache, as the Caching page explains.

The model allocation is the reverse of Spec4's own rounds: the stronger model on the two agents at the front, where the product decisions are made, and the default model at medium on everything after. Neither allocation is a recommendation; both are recorded. Settings

From the plan to the code

Three places where a line in .spec4/v0/ can be checked against the file it produced.

An escalation threshold became an alert rule. phases/phase3.md, in the AI opponent's escalation section:

Fallback rate above 5% over a rolling hour pages the on-call engineer;
sustained fallback above 20% flips a feature flag to run the
deterministic selector exclusively until resolved.

server/src/services/opponent-move-service.ts logs opponent_move on success and opponent_move_fallback with a fallback_reason on every fallback, and the repository's MONITORING.md carries the 5% and 20% thresholds as the alert rules to wire up, with the formula for computing the rate from those two events.

A tier decision became a route. Each single_call entry in ai_catalog.json is one route in server/src/routes//api/opponent-move, /api/commentary, /api/game-end-monologue — each with the fallback the entry's escalation text asks for.

The deployment plan became two files. deployment-plan.md names the three services, the shared secret between API and proxy, the CI stages, and the decision to turn off Render's auto-deploy so every deploy is gated. render.yaml and .github/workflows/deploy.yml are those decisions as configuration.

Where the plan and the code differ

The plan is advisory. Spec4 writes it; the coding agent builds; nothing enforces one against the other, as the Spec-driven development page says. In SnarkCheck's build the two came apart in three places, one in each direction and one neither.

Omitted. deployment-plan.md calls for Sentry error tracking on web_client and commentary_api, performance spans around each LiteLLM call, and a guardrail_violation event tag. None of it is in the code. Fallbacks and content-filter trips are recorded as Pino log events with a fallback_reason field, and nothing computes a rate or pages anyone. MONITORING.md in the repository records the gap.

Exceeded. deployment-plan.md specifies litellm_proxy as a Render web service with access "restricted to commentary_api via shared secret". The agent, in phase 7, made it a Render private service (pserv) instead: no public address at all, reachable only over Render's private network, with the shared secret as a second layer. The code is stricter than the plan.

Stayed inside. stack.json is the approved dependency list, and Phaser cannot add to it without the developer's yes. The built package.json files declare 35 packages. Every library among them is in stack.json; the entries that are not named there are companions of ones that are — react-dom, @vitejs/plugin-react, eslint-config-prettier, jsdom, and type packages. The coding agent added no library of its own.

What was verified. phases/phase7.md ends with a Verification section. .spec4/v0/phase7-verification.md records what was run against it: the full automated suite in both packages; a live smoke test of the API against the real proxy and provider, including CORS rejection of an unapproved origin; and a network-interruption test — the proxy container stopped mid-session, a fallback move returned in 9 ms with source: "fallback", and the health check recovering when the container restarted. The full manual browser click-through the phase asks for was not run, because no browser tool was available in that session, and the note says so.