Building a production application with AI agents

A retrospective on Low Card Wild — a cross-platform multiplayer card game taken from nothing to a deployed, tested product in fourteen weeks by a single engineer working through AI agents.

Subject
Low Card Wild
Period
3 Apr – 9 Jul 2026
Team
One engineer
Status
Complete, unreleased
Author
James Uriarte

Executive summary

Five findings

The project delivered a complete, server-authoritative multiplayer application with 507 automated tests and end-to-end coverage on physical hardware. The engineering interest is not that it shipped, but what the process cost and where it broke.

  1. 01

    The constraint moved from producing code to verifying it. Code generation ceased to be scarce within days. Confidence that the code was correct never did. Every practice that survived to the end of this project is a verification practice.

  2. 02

    Unreviewed velocity was not progress. The highest-output month produced 103 commits and an application that could not survive a round transition. The lowest produced 17 and closed out multiplayer on real devices.

  3. 03

    Agents optimize literally for whatever signal they are given. A request framed around suppressing a symptom produced nine consecutive pull requests that suppressed the symptom. All nine passed review and testing. None fixed the defect.

  4. 04

    Systematic audit outperformed continuous review. A review process that had approved every merge for two months found 48 real defects — including a server leaking hidden state to clients — the first time it was pointed at the whole system rather than at diffs.

  5. 05

    Structural diagnosis stayed human; implementation did not. Agents implemented well once told what the change was. Deciding what the change was remained the engineer's work, and was the scarce input throughout.

What was built

Scope

Low Card Wild is a rummy-style card game built on one unusual rule: the lowest rank in a player's hand is wild. It supports solo play against AI opponents at three difficulties, pass-and-play on a single device, and online multiplayer through a lobby code.

Online matches are server-authoritative — the client renders, the server adjudicates — which is a requirement rather than a preference, since a client trusted to score its own hand is a client that can cheat. That decision is the source of most of the engineering difficulty described below, because it means the rules exist twice: in Dart on the device and in TypeScript on the server.

The game itself had been on the author's list for the better part of two decades. What changed was not the idea but its feasibility: building a cross-platform multiplayer product single-handed was, until recently, not a realistic solo undertaking. The object of the exercise was to learn agentic development on something real, and a real product means one taken to completion rather than to demonstration. The application is finished and unreleased.

Delivered system
ClientFlutter and Dart; card table rendered with the Flame engine
ServerNakama, TypeScript runtime modules, authoritative match loop
DataPostgreSQL; local persistence for saves, settings and achievements
DeliveryDocker Compose behind Caddy with automatic TLS, on a hardened Linux host
PlatformsiOS and Android, landscape, tested on physical devices of both

Measured, not estimated

Measures

Figures below were taken from the repository rather than recalled. The test count and coverage figure come from a run of the suite while this document was being written.

Repository as of 9 July 2026
ValueMeasureNote
14Weeks elapsedFirst commit to last
261Commits on mainSole author
111Pull requests mergedNone in the first three weeks
507Unit and widget testsAll passing, 13 second suite
56.2%Line coverage3,208 of 5,707 lines
24End-to-end testsOn physical Android and iOS handsets
14,011Lines of DartApplication code
10,584Lines of test code0.76 lines of test per line of application
1,976Lines of TypeScriptAuthoritative server rules
48Audit findingsSingle pass, remediated in seven batches

The test-to-application ratio is the figure that matters most. It is not presented as a virtue: it is the reason the structural refactor in phase three was possible at all, and its absence is why phase one stalled.

How the working method changed

Process

The project ran in four distinct phases. The difference between the first and the last accounts for most of what is worth reporting.

Phase one

Unconstrained generation

3 – 9 Apr · 67 commits · 0 pull requests

One week, sixty-seven commits, committed directly to main. Describe a feature, accept the result, commit, proceed. It was genuinely fast, and produced a playable game.

The commit log records the cost. Sixteen consecutive commits contend with a single card-reordering animation — use easeOutCubic, remove live index swapping, clear drag state before moveCard, snap neighbors instantly on drop — with one message repeated verbatim on consecutive commits. Nothing was verified beyond visual inspection, so each change was a conjecture, and a conjecture that looks correct is indistinguishable from a fix until it fails.

  • direct to main
  • no automated tests
  • no review
  • repeated remediation
Phase two

Establishing controls

28 Apr – 9 May · 133 commits · 59 pull requests

Work shifted from building features to defining the rules under which features could be built. A standing instruction document set out the application's invariants as constraints, prohibited modification of the main working copy, and required an isolated worktree, a branch and a pull request for every task.

A review panel was introduced alongside it: eight specialist reviewers covering architecture, security, game rules, coverage, performance, interface, test execution and scope, run in parallel on every pull request, with a ninth agent synthesizing their reports. Continuous integration moved onto a dedicated runner and became the sole authority on merge. Secrets moved to compile-time injection.

This phase produced almost no user-visible functionality. It was the highest-leverage work in the project.

  • standing instructions
  • worktree isolation
  • eight-reviewer panel
  • CI as gate
  • issue tracking
Phase three

Structural refactor

5 – 17 Jun · 22 commits · 24 pull requests

With controls in place the codebase could be decomposed deliberately. Seven lettered milestones, one pull request each: a pure game state extracted from the presentation layer, a controller unifying every action path, an AI driver making that controller opponent-agnostic, a separate achievement evaluator, and a deck-and-variant seam. The card table migrated to a game engine; client and server moved to a versioned protocol.

A full-system audit followed, returning 48 numbered findings remediated in seven batched pull requests in a single day, grouped by severity rather than by file.

Twenty-two commits against twenty-four pull requests: essentially one reviewed, tested unit of work per commit. Phase one closed sixty-seven commits without opening a single one.

  • milestones A–G
  • engine migration
  • protocol v2
  • 48 findings
Phase four

Hardware and release readiness

18 Jun – 9 Jul · 29 commits · 29 pull requests

The remaining work was physical. iOS repeatedly claimed hand-drag gestures for its own bottom-edge system gesture, taking three separate pull requests to resolve. A shader compilation failure broke launch on certain Android GPUs, and cold-start latency had to be measured rather than assumed. Production infrastructure went up behind automatic TLS.

The final two commits are the most instructive. One extended the end-to-end suite to drive two physical handsets through the same online match. The other corrected those tests to run in landscape — the orientation in which the game is actually held, and which no quantity of passing portrait tests had surfaced.

  • gesture arbitration
  • GPU shader failure
  • production deploy
  • two-device E2E
Commits Pull requests merged
Commits and merged pull requests per month April: 103 commits, 6 merged pull requests. May: 105 commits, 53 merged. June: 36 commits, 36 merged. July: 17 commits, 16 merged. 025 5075100 1036 10553 3636 1716 AprilMay JuneJuly
Exhibit 1 — Commit volume against reviewed work, by month. April moved a large volume of code, almost none of it reviewed. By June the two series converge, at which point every commit represents a reviewed and tested unit of work. Throughput did not fall when commit count did; the unit of work grew.

Where the process broke

Failure analysis

Three failures are worth reporting in detail. Each was a category of problem rather than an isolated defect, and each changed how the remainder of the project was run.

Failure one

Nine pull requests against a single defect

On 4 May a card animation began throwing layout exceptions during round transitions. Resolving it consumed pull requests #15 through #23 over two days. The branch names record the trajectory: fix-2, fix-3, fix-4, fix-5, missing-import, robust-layout-fix, ironclad-layout-safeguards.

Each change was defensible. Each passed its tests. Each was reviewed and merged. And each addressed a symptom — deferring the calculation, guarding against an incomplete layout, adding a further safeguard — because the agent had been asked to stop the exception rather than to establish why the coordinates were wrong. Given a symptom to eliminate, an agent will reliably produce something that eliminates the symptom.

Resolution

Reframing the problem from “stop the exception” to “the animation reads positions before they are known — move the read.” Computing positions inside the builder that already held the state removed the entire class of defect. The diagnosis was the engineer's contribution; the implementation was not.

Failure two

One specification, two implementations, no arbiter

Offline play requires the rules in Dart on the device. Online play requires the same rules in TypeScript on the server. The game logic therefore exists twice, written by different agents, in different languages, at different times.

The two implementations diverged. Wild-card detection and run validation are subtle enough — surplus wilds extending a run from either end, all-wild sets being legal — that two independently plausible implementations can disagree on real hands. A review-checklist instruction to confirm the two remain synchronized is worth very little against that.

Resolution

A language-neutral conformance corpus: one JSON file of cases covering wild detection, group validation, hand scoring and go-out eligibility, which both implementations must satisfy. Divergence ceased to be something a reviewer might notice and became something a test fails on.

Failure three

Forty-eight defects surviving continuous review

A systematic audit of the whole codebase in June returned 48 numbered findings. Among them: the server broadcasting state that exposed opponents' hidden hands, achievements crediting the wrong player after a drop, pass-and-play routing every hand action to seat zero irrespective of whose turn it was, and callbacks firing after disposal.

None had been caught by roughly 500 passing tests, and several had been approved by the same review panel that later found them. The uncomfortable conclusion is that reviewing a diff and auditing a system are different activities, and that agents are markedly stronger at the second than at the first.

Resolution

Seven batched pull requests in one day, grouped by severity rather than by file — server hardening, then correctness of achievements, then lifecycle safety, then low-priority cleanups. Grouping by severity gave the dangerous fixes scrutiny proportionate to their risk instead of burying them among cosmetic corrections.

What was in place by the end

Controls

None of these are novel. They are ordinary engineering controls applied to a collaborator that works quickly, does not tire, and retains nothing from yesterday.

Invariants as standing constraints

The rules of the domain are recorded in a document agents must read before any task, expressed as constraints rather than preferences. Code violating them is rejected irrespective of whether its tests pass — on the basis that the tests may be wrong too.

Isolation as a safety property

Every task begins by creating a disposable worktree; writing to the main working copy is prohibited. An agent that goes wrong does so inside a directory that can be discarded. The cost is seconds and it removes an entire class of incident.

A review panel rather than a reviewer

Eight reviewers with narrow briefs run in parallel and a ninth synthesizes. The panel is mandatory: specialists are not skipped because a diff appears small. A single general-purpose reviewer converges on generic advice, whereas narrow briefs produce specific objections.

Reviewers cannot author

Review agents operate in enforced read-only mode, forbidden from repairing what they find and from offering to. A reviewer that fixes its own findings has become the author, and the review becomes ceremony.

A single unpersuadable authority

Agents run tests locally and report coverage, but continuous integration is the only gate that counts. An agent can be argued into believing a change is sound. A build cannot.

Verification on real hardware

Twenty-four end-to-end tests run on physical Android and iOS handsets, two of which drive both devices through a single online match. Simulators do not claim your drag gesture for a system swipe, and they do not fail to compile a shader.

What generalises

Conclusions

Offered with the caveat that this is one application, one engineer and fourteen weeks.

Verification is the binding constraint

Code production stopped being scarce almost immediately; confidence in correctness never did. Every practice that survived to the end of this project is a verification practice, and every week invested in one repaid itself.

Standing instructions outperform conversation

Direction given in conversation applies to that conversation. Rules written into a document agents must read first apply to work carried out unobserved — which is most of it.

The signal chosen is the design decision

Ask for an exception to stop and you receive nine pull requests that suppress it. Ask for coverage to rise and you receive tests that raise coverage. Optimization is relentless and literal, which makes the choice of signal far more consequential than the wording of any request.

Audit and review are different disciplines

A panel that had approved every merge for two months found 48 real defects the moment it examined the system rather than the diffs. Diff review has a blind spot the size of the codebase. Schedule audit separately and expect it to indict work already approved.

Structural judgment did not transfer

Agents implemented capably once told what to change. The interventions that actually unblocked this project — the positions are read too early; these two implementations need a shared corpus; this state belongs outside the presentation layer — were framings rather than patches, and remained the engineer's work throughout.

Commit cadence misleads as a progress measure

April produced 103 commits and an application that could not survive a round transition. July produced 17 and completed multiplayer on real devices. Anyone tracking this project by commit volume would have judged it failing precisely as it began to work.

Nothing reports whether the result is any good

The final substantive work was not defect repair. It was the hand needing to sit in an arc clear of the bottom edge, cards needing to be dragged rather than tapped, discarding needing to be a throw rather than a button press. No test requested any of it. It came from holding the thing and finding it wrong, which remains entirely unautomated.

Appendix

The delivered application

Captured from a debug build driven programmatically on an iPhone simulator.

Exhibit 2 — One round, unedited. Draw from the deck, drag a card onto the discard pile, three AI opponents take their turns, then hand scoring. 43 seconds.
Table stateOpponents seated around the felt; hand held in an arc clear of the bottom edge.
Six cards heldDrawing enables going out. Cards are drag-only; discarding is a throw onto the pile.
Round settlementEvery hand opened and its groups outlined, so a score can be checked rather than trusted.
Manual scoringWith assists disabled the player scores their own hand. The three costs 25: threes were wild that round.
Online lobbyHost and share a code; unfilled seats take AI opponents. Matches survive a disconnect.
Match configurationOpponent count, difficulty and human seats set per match rather than globally.