DDS Vibe Academy · Masterclass · Advanced

From Prototype to Published

Three AIs. One repo. 26 briefs.

Ashaveth is a free grimdark browser RPG, built by three AI agents working the same repository — Cowork, Claude Code, and a Google Antigravity agent. This is the verified method end to end: how the work was split, how the agents were kept from colliding, and every time the AI was wrong — with the corrections.

Free class Advanced ~2 hours Ashaveth v1.1.0

A worked case study from a shipped game · Every figure re-derived from the repository · The game is free at ashaveth.ddsboston.com (18+).

Quick answer

This free masterclass reconstructs the multi-agent workflow behind Ashaveth, a published browser RPG. Three AI surfaces shared one repository: Cowork orchestrated, researched, and verified in a live browser; Claude Code wrote, tested, committed, and deployed; a Google Antigravity Gemini agent ran parallel non-colliding work. You learn dispatch briefs, write-allowlists, audit gates, and a full log of the AI's own errors.

Key takeaways

  • Three AI surfaces, one repository, 26 dispatched briefs in a single session — average about 3.5 kB, the largest 9,338 characters.
  • The split was forced by infrastructure, not preference: a mount that silently truncates reads and writes once committed a 183-line file whose real length was 218. Every commit moved to the agent on the native filesystem.
  • Every parallel brief carries a write-allowlist, and containment is proven by a git diff that must return empty — never the agent's self-report.
  • Two independent balance passes disagreed — 60–72% against 0–10%. The independent harness was the bug; corrected, it returned 98–100%. Suspect the measurement first.
  • The AI was wrong at least six documented times, including inverting a mobile threat model and inventing a quota blocker that did not exist. Each one is logged below with what caught it.
  • Only the global ceiling of 5,000 requests/day bounds the bill. Per-user limits shape behavior; they do not protect a wallet.
  • An unbounded namespace needs a fallback, not more content: AI-minted item names are infinite, so a 24-cell rarity matrix replaced an impossible art batch.
  • A count is only as clean as the tree it ran on: the suite reports 487 with an untracked scratch file present and 480 without it. The committed number is the one that reproduces. Measure a commit, not a working directory.
  • Ashaveth v1.1.0 ships 171 art assets, passes 480 unit tests across 20 test files with 0 TypeScript errors, and compresses a 355 kB entry bundle to 117 kB gzipped — every figure produced by running the command, not by rereading the last page.

1 Division of labour

The three surfaces

Ashaveth was built by three AI surfaces sharing one repository. Each was assigned work its constraints allowed, not work it was nominally capable of. Cowork orchestrated, researched, and verified. Claude Code wrote code, ran tests, committed, and deployed. A Google Antigravity Gemini agent took parallel work that could not collide with the code.

Cowork

Claude desktop agent Has: research, live browser, APIs, no terminal on the target box
  • Research gate before any deploy — found the Chrome behaviour that killed a feature, and sourced the citation study
  • Live browser verification through the Chrome extension. Chrome is granted read-only to desktop control, so the extension was the only path — a constraint that shaped the tooling
  • Independent Monte-Carlo balance simulations in its own sandbox
  • Shopify Admin API: 74 art files uploaded by URL pull, zero byte-shuttling
  • Orchestration: writing the briefs, holding gates closed, reconciling audits

Claude Code

Opus 4.8, in the Antigravity terminal Has: the native filesystem, a terminal, the test runner
  • Every commit and every deploy — see module 7 for why this was not a preference
  • All game code: the combat rebalance, the alternate route, the inventory fixes
  • The test suite, the production build, the release
  • Ran on the user's own subscription, in his IDE, on his machine

Antigravity / Gemini

Gemini 3.1 Pro agent Has: a parallel seat in the same IDE, in-chat image generation
  • Open-source documentation set, while code work continued in parallel
  • SEO metadata and the structured-data graph for the v1.1.0 bump
  • Image generation on its free in-chat quota
  • Ran concurrently with Claude Code — under an explicit lane (module 3)
The rule

Assign work by constraint, not by capability. The most important question is not "can this agent do it" but "what is this agent structurally unable to get wrong?"

2 How work moves

The dispatch loop

Work moved between agents as written briefs, not conversation. Twenty-six briefs were dispatched in one session, averaging roughly 3.5 kB, the largest 9,338 characters. A brief is not a request — it carries intent, constraints, a write-allowlist, and acceptance criteria that can fail.

The orchestrating agent had no terminal on the target machine, so it drove the other agents through the interface: focus the window, screenshot to confirm state, click the prompt, paste the brief. That sounds primitive. It is also why every instruction had to be complete and self-contained — there was no cheap follow-up. Constraint produced discipline.

26Briefs dispatched
9,338Largest brief (chars)
113Sandbox analyses
3Surfaces, one repo

Every brief has the same four parts. Intent: what "done" means, in one sentence. Constraints: the rules the result must obey, including what not to touch. Acceptance criteria: a command with a pass condition — if it cannot fail, it is not a criterion. Lane: the paths this agent may write.

A brief that another agent can inherit must also carry your corrections. When the orchestrator discovered it had overstated a security risk, it wrote the correction directly into the next dispatch — "correcting my own threat model, because I had it wrong and you should not carry it forward" — so the coding agent would not build on a bad premise.
The rule

Write briefs, not chat. Intent, constraints, acceptance criteria, lane. If your acceptance criterion cannot fail, you have written a wish.

3 Concurrency

Lanes, and the diff that must be empty

Two agents editing one repository at once is a merge-conflict problem wearing a speed win's clothes. The answer is a lane: every parallel brief names the exact paths that agent may write, and containment is then proven with a command rather than trusted from a self-report.

A real lane, quoted from a live brief: "Another agent is ACTIVELY EDITING CODE in this repo RIGHT NOW. Do not collide with it. You are READ-ONLY on all code. WRITE ONLY to: docs/**." And for the parallel art run: "This runs in PARALLEL with Claude Code's balance work, so stay strictly in your lane. Adding unused image files is safe and non-colliding." Note the reasoning in that second one — the work was chosen because it could not collide.

Self-reports are not evidence. Containment was verified as code: a git diff scoped to everything outside the allowlist, with the pass condition stated in advance — that diff must be empty.

containment check — prove the agent stayed in its lane
# 1. what it was allowed to touch
$ git diff HEAD --stat -- docs
  docs/CONTRIBUTING.md | 84 +++++++++
  docs/SECURITY.md     | 61 ++++++
  2 files changed

# 2. everything else. this MUST be empty.
$ git diff HEAD --stat -- . ':(exclude)docs'
  (empty)  ← containment proven

The companion rule is stage explicitly; never git add -A. That rule exists because it was earned: a handoff written by the orchestrator carried a trailing git add -A that would have swept 19 unrelated files into an unrelated commit. The coding agent caught it and asked before running it. Agents checking each other is not redundancy — it is the design.

The rule

Parallelism is a collision problem. Give every agent a written lane, stage explicitly, and prove containment with a diff that must be empty. Never accept "I stayed in my lane" as evidence.

Interactive · Instrumentlive

What does the player actually download?

The shipped build measured 355 kB uncompressed and 117 kB gzipped. Change the bundle and watch the band.

kB
What the player downloads
kB

Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

Interactive · Ledgerclaim 1 of 3

Three claims from the build.

Every delta is computed from its own two values.

Compression made a marginal difference to the entry bundle.

Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

4 The gate

Two independent passes, then reconcile

The audit gate is the load-bearing part of the loop. Each phase defines intent, constraints, and acceptance criteria; agents execute; then a gate runs that can send the work back. For anything expensive to get wrong, two agents audit the same thing independently and the results are reconciled — because the point of a second pass is the disagreement.

The game-math audit ran this way: two agents, same code, neither seeing the other's findings, both under a hard instruction of audits and testing only — report, do not fix. That constraint existed because it had been violated before, and the correction stuck: a later brief opens "(We got burned before by an 'audit' that turned into edits; not this time.)"

Agreement between passes tells you very little — two agents can share a blind spot. Disagreement is the product. It localizes the error to one of three places: pass A, pass B, or the thing being measured. Every one of those is worth knowing, and you cannot learn any of them from a single pass.

Every direction a catch actually travelled during this build. No surface was reliably right.
CatcherWhat it caught
Cowork → GeminiReported done at 27 of 45 items, having run no completeness check
Cowork → Claude CodeA guess that a paid plan was required for analytics. The documentation said otherwise — the upgrade was never needed
Claude Code → CoworkA trailing git add -A in a handoff that would have swept 19 unrelated files into a commit
Cowork → CoworkIts own balance harness — the subject of module 5
Sub-agent → CoworkA stale figure about to ship to a public page: "it caught an error I'd have shipped"
The human → everyoneRepeatedly, and decisively. See module 6
The rule

Run the expensive judgements twice, independently, then reconcile. Agreement is weak evidence; disagreement is a finding. And an audit that edits is not an audit.

5 The best lesson here

When the harness is the bug

The combat rebalance had a target: every class should beat the final boss without the special weapon, but not comfortably. Two agents simulated it independently. One reported the dragon winnable 60 to 72 percent of the time. The other reported 0 to 10 percent. One of them was badly wrong — and it was the auditor, not the game.

independent harness — first run
# win% at 5 Full Restores, NO Godsbane, prepared L50 build
boss              Warrior   Rogue    Mage
Golgoth  L20         100%    100%    100%
Malakor  L30         100%    100%    100%
Xel'Nath L40          14%     74%     74%
Ashaveth L50           0%      3%     10%   ← unwinnable?

The tempting move is to raise the alarm: the other agent's rebalance is broken, the boss is unbeatable, escalate. The correct move is to audit your own instrument first. The reasoning, written at the time: "before I alarm anyone, I have to check my model — a wrong harness is worse than none."

It was. The simulation had never modeled the Mage's defensive barrier or any brace-before-a-heavy logic, and it allowed only five potions. Those are not edge cases; they are how the fight is actually played. Modeling them changed everything:

independent harness — after modelling the defensive tools
# dragon win% by potion count, defensive barrier now modelled
pots     Warrior   Rogue    Mage
   5         99%     98%    100%
  10        100%    100%    100%

# verdict: my first sim was the bug, not the balance.

Two things make this the strongest lesson on the page. First, the correction was self-initiated — the disagreement triggered self-audit before escalation. Second, the honest caveat that followed was volunteered, not extracted: a hand-rolled model is extremely sensitive to the assumed play policy — that is precisely why the first run looked broken — so the harness can confirm the shape of the difficulty curve but must not be tuned to a specific percent. The real number comes from playtesting.

Reconciled: the coding agent's 60–72% sits inside the independent bracket, between an optimal-play ceiling near 100% and a no-defensive-play floor near 0%. Both passes were right about different players.

The rule

When two passes disagree, the first suspect is the measurement. Audit your instrument before you audit the work — and never tune a real system to a simulated number.

6 Honesty

The corrections log

This section exists because the corrections are the curriculum. The orchestrating AI was wrong at least six documented times during this build. None of the errors were exotic — each was confident, plausible, and reasoned. What matters is which mechanism caught each one, because those mechanisms are the transferable part.

The threat model, inverted

AI was wrongCaught by: research gate

The AI wrote an install card warning iOS users about a 7-day storage eviction that would delete their save.

The truth: home-screen web apps are exempt from that eviction. Installing is the protection, not the risk. The card was telling users to fear the thing that saves them. It corrected itself in writing and added a second, better discipline: "do not overclaim: we have NOT verified the partition behaviour on a device."

"Once again, only you saw the key"

AI was wrong twiceCaught by: the human, twice

The AI inflated the risk of a leaked bot-detection secret, and repeated the claim after being corrected — twice.

The truth: that secret is used server-side to validate a challenge token; it cannot mint one. Tokens come only from the client-side challenge widget. There was no exploit path. Rotation was still correct hygiene, but the danger was invented. The admission: "you pushed back on it twice while I repeated it. That's on me, not you." The fix went further than an apology — the correction was written into the next brief so the coding agent would not inherit the bad model.

"Correction — only in the inventory do the items not show"

AI blamed the userCaught by: the user's phone

Told the new item art was missing, the AI diagnosed a stale browser cache.

The truth: a real bug, corrected in eleven words by the man holding the phone. The art rendered in the shop and not in the inventory, because a UI pass had wired the art into the bag grid and the detail panel but missed the three equipment slots — and on a fresh save the bag is empty, so those three icon-only slots are the entire screen. "It's your cache" is the AI equivalent of "have you tried turning it off and on again." Two more bugs surfaced from those same screenshots that a fresh save could never have revealed.

The blocker that did not exist

AI invented a constraintCaught by: the human

The AI announced the image batch was quota-blocked until tomorrow and planned a day of delay around it.

The truth: it had imported one agent's constraint into another agent's pipeline. The free in-chat image quota belonged to the Gemini surface; the art script called a paid API that had already been approved. Nothing was blocked. In multi-agent work, constraints are per-surface — and an inherited constraint invents blockers and burns real time.

The spec that killed the feature

AI's own spec was the bugCaught by: research gate

The AI specified an empty service-worker fetch handler as a hard constraint, with a sound-sounding rationale about not caching the entry page.

The truth: Chrome ignores no-op fetch handlers precisely because developers kept doing exactly this — so the feature would have been dead on arrival on Android. It was caught only because a standing rule required research before deploying. The same research surfaced a bigger, unrelated, already-live bug. A confident architectural constraint is still a hypothesis.

The stale number, caught at the door

AI would have shipped itCaught by: sub-agent audit

A public page was ready to ship carrying figures the AI had not re-derived.

The truth: an independent audit found the art count understated by 66% and a test count that could no longer be verified at all. The response was the only correct one: "it caught an error I'd have shipped." This page is the successor to that one, and its numbers are re-derived — see Receipts.

The pattern across all six: none were caught by the AI being careful. Each was caught by a mechanism — a research gate, a user with a real device, an independent pass, an audit. Confidence is not a safety system. Build the mechanisms.
The rule

Log your agent's errors and publish them. An agent that cannot show you its corrections is not showing you its work — and the human holding the actual device is ground truth, every time.

7 Infrastructure decides

The filesystem that lies — and commits the lie

The most consequential architectural decision in this build was not made for a good reason. It was forced by a disk. The repository lived on a Windows drive exposed to the orchestrating agent through a mount that silently truncated both reads and writes — sometimes. That single fault decided which agent was allowed to commit.

It surfaced the worst possible way: a commit landed containing 183 lines of a file whose real length was 218, cut off mid-line. Nothing errored. The staging was clean. The commit succeeded. The blob was simply wrong.

And the obvious recovery is a trap. Restoring the file from git would overwrite the good working copy with the bad committed blob — the truncation would win. The verification pattern that actually works: syntax-check the working file, re-commit, then pipe the committed blob back out of git and syntax-check that.

verify the artifact, not the source
# the working file is fine...
$ node --check scripts/pregen-items.mjs
  ok

# ...but what actually got committed?
$ git show HEAD:scripts/pregen-items.mjs | wc -l
  183   ← the real file is 218 lines
$ git show HEAD:scripts/pregen-items.mjs | node --check
  SyntaxError: Unexpected end of input

# do NOT git checkout — that overwrites the good file with the bad blob.

This is why every commit and every deploy in this project ran on the agent with the native filesystem. Not because it wrote better code — because the other surface could not be trusted to write bytes. The lesson generalizes past this one weird disk: your agents' capabilities are not the only thing that matters. Their substrate is part of the architecture, and it gets a vote.

The rule

Verify the artifact, not the source. Check the committed blob and the deployed origin — a filesystem, a cache, or a CDN can all lie about what you think you shipped.

8 Safety you can prove

Test the guard by defeating it

The prototype's most expensive defect was one string: its API key, sitting in the browser bundle. The fix is architectural — every model call routes through a Cloudflare Worker holding the key as a Worker secret, and the bundle ships a placeholder. But "we moved it" is a claim, and claims decay. So the guard is tested by trying to defeat it.

A postbuild step scans the built bundle for credential patterns and fails the build if one appears. Then the team did the part most teams skip: they poisoned a build on purpose — inlining a fake key and unsetting the proxy URL — and confirmed the scanner exited non-zero.

control experiment — can the guard be defeated?
# deliberately poison: fake key inlined, proxy URL unset
$ npm run build && node scripts/check-secrets.mjs
  scanning dist/ for credential patterns...
  FAIL  credential pattern found in dist/assets/index-*.js
  build aborted — a key must never reach the client
$ echo $?
  1   ← the guard fires. now it is evidence, not a hope.

That experiment exposed a distinction worth internalizing: the dangerous path is conditionally dead, not dead. The build inlines the key only when the proxy URL is unset — so the code is not deleted, it is dormant. One empty environment variable re-enables it. A guard that a configuration mistake can switch off is exactly the guard you must keep testing.

Spend is bounded in layers: an origin allowlist, a model allowlist, a per-session quota, and a per-IP quota. Only one of them protects the wallet — the global ceiling of 5,000 requests per day. Per-user limits shape behaviour; attackers rotate identities. The original per-player quota keyed on a client-supplied header that any user could simply change, which is why the fix pairs a bot-detection challenge with an HMAC-signed session token whose session id is generated on the server. The quota key must not be a value the client gets to choose. And limits are keyed on that signed id rather than on IP, because schools and carriers put thousands of real people behind one address.

The rule

An untested guard is a guess. Poison a build and watch it fail. Then remember only the global ceiling bounds your bill, and never key a quota on a value the client controls.

9 Generative systems

The unbounded namespace

Late in the build, a player's inventory was full of flat placeholder icons instead of art. The reflex — generate more images — was mathematically doomed. When a monster mints an item name at runtime, the namespace is infinite, and no finite offline batch can ever cover it. That is a resolver problem, not a content problem.

The game generates loot with AI-invented names. Anything not in the static item list becomes a custom item, named on the spot. An endgame bag is therefore mostly items that by construction have no pre-generated art. Shipping more images would have raised the hit rate slightly and fixed nothing.

The fix is a chain: exact art, then a 24-cell fallback matrix covering every item type crossed with every rarity, then a type icon. Every AI drop now resolves to real, rarity-appropriate art, generated once, with zero runtime image calls — preserving the free game's spending guarantee.

Mythic weapon fallback art
Weapon · Mythic
Legendary armor fallback art
Armor · Legendary
Epic accessory fallback art
Accessory · Epic
Rare consumable fallback art
Consumable · Rare
Godsbane, the forged blade
Exact art
Godsbane, Unquenched
Exact art

The shipping discipline matters as much as the design. The resolver shipped before the images existed — every matrix lookup would 404 and fall through to the icon, which is byte-identical to the previous behaviour. A safe degrade means the deploy cannot regress anyone, and the art can land whenever it lands.

The same class of bug, one layer down: the art script bundled the static item list and never saw the file that mints shop consumables at runtime, so two perfectly ordinary potions had no art for months. Coverage checks must run against the real namespace, not the one you remembered to look at.
The rule

An unbounded namespace needs a fallback, not more content. And ship the resolver before the assets, so the safe degrade is the deploy's worst case.

10 Reality

Publishing, and the proof it worked

"It builds" is a small part of "it's live." Shipping this game means a proxy Worker with a hand-set secret, a quota store, a container image, a Cloud Run service, DNS, a TLS certificate, a storefront page, and a smoke test that can actually fail. The build is one step of eight, and not the riskiest.

The deepest trap in the whole project lives here. The AI proxy rejects the temporary cloud URL by design — it is not on the origin allowlist — and the game falls back to pre-written dialogue that is deliberately indistinguishable from live AI. So a developer can test on the temporary URL, see a beautiful, fully playable game, and ship with the signature feature completely dead. The only proof is a specific observable: a 200 from the proxy, in the network tab, on the real origin.

Two more traps from this build, both about things that lie. A .dockerignore once excluded the scripts directory — which would have stripped the secret scanner out of the build environment entirely. A guard that does not exist where the build runs is not a guard. And a PWA icon survived a correct deploy twice: an installed shortcut bakes its icon at install time, and worse, the icon URLs were unversioned while the server sent immutable, max-age=31536000. Same URL, same cache entry, held for a year — even across a reinstall. Cache immutability is a one-year commitment to a mistake. Versioned URLs fixed it.

The server being correct is not the same as the user seeing it.The icon, twice
The rule

Every deploy step is a command with a pass condition, and nothing proceeds on a warning. Your smoke test must be a specific observable on the real origin — not "the game works."

Reproducible

The loop, end to end

Tool-agnostic, and every step is a scar. Each names the decision, the acceptance test — a command with a pass condition, because if it cannot fail it is not a test — and the failure mode it exists to prevent.

  1. Assign by constraint, not capability

    Ask what each surface is structurally unable to get wrong. Let infrastructure decide, and write down why.

    Accept: every task type has a named owner and a stated reason.

    Failure: a truncating mount silently commits a 183-line version of a 218-line file.

  2. Write briefs, not chat

    Intent, constraints, acceptance criteria, lane. Complete and self-contained.

    Accept: every brief contains a command with a pass condition.

    Failure: an agent optimizes for the thing you said instead of the thing you meant.

  3. Give every parallel agent a lane, and prove it

    Name the writable paths. Never trust the self-report.

    Accept: git diff outside the allowlist returns empty.

    Failure: a trailing git add -A sweeps 19 unrelated files into a commit.

  4. Audit twice, independently, then reconcile

    Report only. An audit that edits is not an audit.

    Accept: two passes, neither seeing the other, reconciled in writing.

    Failure: one confident pass and a shared blind spot.

  5. Suspect the measurement first

    On disagreement, audit your own instrument before escalating.

    Accept: the harness models the tools a real player actually uses.

    Failure: "the boss is unwinnable" — when your simulation forgot the defensive skills.

  6. Log corrections and push them downstream

    When you find your own error, write the correction into the next brief.

    Accept: the corrected model appears in the dispatch, not just the apology.

    Failure: a second agent builds on your bad premise.

  7. Verify the artifact, not the source

    Check the committed blob and the deployed origin.

    Accept: git show HEAD:file parses; the live URL returns what you expect.

    Failure: a clean working file and a corrupt commit.

  8. Test the guard by defeating it

    Poison a build; watch the scan fail.

    Accept: exit code 1, on purpose, before you trust it.

    Failure: a credential path that is conditionally dead, one variable from live.

  9. Cap spend globally

    Layer the limits, but know which one is load-bearing.

    Accept: a hard daily ceiling, and a quota key the client cannot choose.

    Failure: per-user limits and an identity the user can rotate at will.

  10. Give unbounded namespaces a fallback

    Generated names cannot be covered by a finite batch.

    Accept: ship the resolver first; every miss degrades to the old behaviour.

    Failure: generating art forever and never catching up.

  11. Key caches on stable ids, never display names

    Display names are free to change. Cache keys are forever.

    Accept: rename an entity; confirm no asset is orphaned.

    Failure: a find-and-replace becomes an art-regeneration project. See the receipt below.

  12. Re-derive every public number at release

    No figure reaches a marketing asset without a command behind it.

    Accept: each claim carries a command; unverifiable claims are omitted.

    Failure: a page that teaches verification while carrying stale numbers.

The receipt for step 11

This project paid for that rule and then proved it. The game's final boss was renamed during an IP sweep — but its art cache key was pinned to the pre-rebrand string on purpose, with a comment in the code explaining why: renaming the key would orphan the shipped portrait. So the display name moved freely to "Ashaveth, the Red" while the cache key stayed exactly where it was, forever. The same duality runs through the whole bestiary — several monsters carry grimdark display names over legacy art keys, deliberately.

Ashaveth, the Red — the final boss
Ashaveth, the Red
Gaunt Wolf, a monster in Ashaveth
Gaunt Wolf
The Void Edge biome
The Void Edge
Read those two captions against their filenames and you have the lesson in one glance: the display name is a product decision you can revisit; the cache key is a permanent architectural commitment you make on day one, usually without noticing.
Assign by constraint; brief, don't chat; lane every agent and prove it; audit twice and suspect the instrument; verify the artifact, not the source; and never trust a green signal you have not tried to break.The loop in one sentence

Receipts

Every number, re-derived

This page was built by running the checks it describes. An independent audit re-derived every figure against the repository at a known commit, and two claims did not survive it: the test count and the bundle size. Neither could be executed in the audit environment, so both shipped marked UNVERIFIED rather than guessed — a page about verification that hides its own gaps has taught nothing. Both have since been run on a machine that could execute them, and the numbers are below. The wait was the point: the stale figure was "154 tests," a first re-measurement said "487," and the number that actually reproduces from the commit is 480. Guessing would have been wrong three ways.

Verified against Ashaveth v1.1.0 at commit d2daf49, on a clean tree. Every figure below has a command behind it and no figure is estimated; when a check could not be executed it stayed marked unverified until it could be. The rule does not bend for a number that would look good.

  • 0 TypeScript errors, stricttsc --noEmit, exit 0
  • 171 art assets — 72 monster, 68 item, 26 character, 5 location
  • 24 fallback matrix cells — 4 types × 6 rarities, all live
  • 72 monsters — 67 non-boss + 5 bosses, 1:1 with 72 art files
  • Global ceiling 5,000 requests/day — the wallet guarantee
  • Relic drop 0.05% — 1 in 2,000, and only while the AI is live
  • No key in the bundle — credential scan of the build, 0 matches
  • 74 files uploaded — CDN coverage taken to 171 of 171
  • 480 unit tests pass across 20 test filesnpm test (vitest run), 0 failed, 2.25s
  • 355,422 byte entry bundle, 116,835 gzippedrm -rf dist && npm run build, chunk assets/index-[hash].js
The most instructive thing that happened while writing this page: an audit of its predecessor found stale boss stats in four places including the structured data, an art count understated by 66%, and a death-penalty claim that was backwards — it told players banked gold was safe when the code takes a percentage of banked gold too. The page about verifying your claims had unverified claims. That is not an embarrassing footnote. That is the single best argument for the rule.

Questions

Frequently asked

What does this masterclass teach?

It reconstructs the multi-agent workflow that built and shipped Ashaveth, a free grimdark browser RPG. You learn how to divide work across several AI agents, write briefs that can fail, keep parallel agents from colliding, run independent audit passes, and verify what actually shipped. It includes a full log of the errors the AI made during the build.

Which three AI applications built Ashaveth?

Three surfaces: Cowork, the Claude desktop agent, which researched, audited, drove the live browser, handled the Shopify API, and orchestrated; Claude Code running Opus 4.8 in the Antigravity IDE terminal, which did the repository code, tests, commits, and deploys; and a Google Antigravity Gemini agent, which handled parallel non-colliding work such as SEO metadata and in-chat image generation.

Why did one agent do all the commits?

Infrastructure, not preference. The repository sits on a Windows drive exposed to the orchestrating agent through a mount that silently truncates reads and writes. It once committed a 183-line file whose real length was 218 lines. Because commits from that surface could not be trusted, every commit and deploy was routed to the agent running on the native Windows filesystem.

How do you stop parallel agents from colliding in one repository?

Give every brief an explicit write-allowlist naming the paths that agent may touch, and never trust the agent's self-report. Containment is proven with a command: a git diff excluding the allowed paths, which must return empty. Staging is always explicit — never git add -A, a rule that exists because a trailing git add -A in one handoff would have swept 19 unrelated files into a commit.

What is an audit gate?

A checkpoint that closes a phase and can send the work back. Each phase defines intent, constraints, and acceptance criteria that can fail, then agents execute, then the gate runs. The gate is the load-bearing part: it is what turned the claim that the API key had been moved into a poisoned-build proof that the guard actually fires.

What happens when two AI agents disagree?

Suspect the measurement before the code. During the combat rebalance, one agent's simulation reported the final boss was winnable 60 to 72 percent of the time; an independent simulation reported 0 to 10 percent. The independent harness was the bug — it had never modeled the Mage's Arcane Barrier or brace logic. Corrected, it returned 98 to 100 percent. The value of a second pass is the disagreement, not the agreement.

How do you ship AI features without exposing the API key?

All model calls route through a Cloudflare Worker holding the key as a Worker secret, so the browser bundle ships a placeholder rather than a credential. A postbuild step scans the built bundle for credential patterns and fails the build if one appears. The guard was then proven by deliberately poisoning a build and confirming it exited non-zero.

What is the difference between dead code and conditionally dead code?

The build inlines the API key only when the proxy URL is unset, so the dangerous path is not deleted — it is dormant. One empty environment variable re-enables it. A guard that can be switched off by a configuration mistake is exactly the guard you must keep testing.

Why does per-user rate limiting not protect your bill?

Because attackers rotate identities. Ashaveth's proxy caps spend in layers — an origin allowlist, a model allowlist, a per-session quota, and a per-IP quota — but only the global ceiling of 5,000 requests per day actually bounds the bill. Per-user limits shape behavior; the global ceiling is the wallet guarantee.

Why was a Turnstile session token needed?

The original per-player quota keyed on a client-supplied header, which any user could rotate to reset their own limit. The fix pairs Cloudflare Turnstile with an HMAC-signed session token whose session id is generated server-side, so the quota key cannot be chosen by the client. Rate limits are keyed on that signed id rather than on IP, because schools and carriers share addresses.

Can an offline art batch cover AI-generated loot?

No, and this is a common mistake. When a monster mints an item name at runtime, the namespace is unbounded, so no finite batch of pre-generated images can ever cover it. The fix is a resolver chain rather than more content: exact art, then a 24-cell type-by-rarity fallback matrix, then a type icon. Every AI drop gets real art without a runtime image call.

Why did an icon persist after a correct deploy?

Two causes compounded. An installed home-screen shortcut bakes its icon at install time, so no deploy repaints it. Worse, the icon URLs were unversioned while nginx served them with immutable, max-age=31536000 — the same URL means the same cache entry, held for a year, even across a reinstall. Cache immutability is a one-year commitment to a mistake. Versioned URLs fixed it.

How can passing tests certify a bug?

When the fixture cannot reach the failing state. A shipped bug in the ending survived a clean compiler and a green suite because the test fixture seeded an empty journal for every character, so the harness could only ever exercise the empty path. Coverage counts what you wrote, not what you can reach. For every invariant, ask what state would make this check fail, then confirm your fixture can reach it.

Why does the masterclass publish the AI's mistakes?

Because the corrections are the curriculum. The orchestrating agent inverted a mobile threat model, overstated a security risk twice after being corrected, misdiagnosed a real bug as a browser cache issue, and invented a quota blocker that did not exist. Each error was caught by a specific mechanism — a research gate, user pushback, a second pass, or an audit — and those mechanisms are the transferable part.

Why does the class not say which agent wrote which code?

Because the repository records a single git author with no per-commit trailers, so per-agent attribution is unrecoverable. Attribution that cannot be verified is not a fact worth publishing. The class teaches the method and treats the model roster as an implementation detail.

What is the 2026 stack behind Ashaveth?

React 19 and TypeScript 5.8 in strict mode, built with Vite 6 and tested with Vitest 4. AI dialogue runs through the Google Gemini API behind a Cloudflare Worker. The game ships as a static bundle in a multi-stage Docker image served by nginx on Google Cloud Run, with a Shopify storefront page. The current release is version 1.1.0.

What makes the game logic testable?

Purity. The combat engine has no clock and no random source — randomness is injected as a parameter — and the reducer is the only authority on state changes, re-validating its own preconditions rather than trusting the component that dispatched the action. Dependencies point one way: components to state to services to data, and nothing points back up.

Is Ashaveth free, and who can play it?

Ashaveth is free to play in the browser with no download and no signup, at ashaveth.ddsboston.com. It is a grimdark RPG intended for adults and is self-rated 18+ by Design Delight Studio; it carries no ESRB or PEGI rating.

Bottom line

Multi-agent building is not about having better models. It is about designing the mechanisms that catch a confident agent being wrong — a research gate, a lane with a diff that must be empty, two independent passes, a guard you have watched fail, and a human with a real device. Ashaveth shipped because those mechanisms existed, and every one of them earned its place by catching something. Run the loop on your own prototype; the corrections you log will be the most valuable output.

Play the proof. Then run the loop.

Ashaveth is free in the browser — no download, no signup. Then take the method into your own build at the DDS Vibe Academy.

Free to play · 18+ · Built and published by Design Delight Studio, Boston.

A DDS Vibe Academy masterclass by Robert McCullock, Design Delight Studio. Play the game at ashaveth.ddsboston.com, read the Ashaveth page, browse more classes at the DDS Vibe Academy, see the studio overview, or use the sitemap. Citation research: Aggarwal et al., "GEO: Generative Engine Optimization," KDD 2024 (arXiv:2311.09735). Every figure on this page is re-derived from the repository; figures that could not be re-derived are published as unverified rather than estimated.