A cross-project asset catalog, and a correction to what this server told the model about making art. Design: ASSET-BANK.md.
Spawn generates a cdn/ asset on first fetch of its path and caches it there forever — the path is the asset — and it has no catalog API. So sharing across projects already costs nothing (the same path is the same asset, which is what Spawn's own "use the same file name and it will be the same asset" means), but nothing anywhere records which names worked. A name cannot be re-rolled: a path that produced something good is unrecoverable knowledge once you forget the spelling, and a path that produced something bad is permanently bad. That judgment is what the bank keeps.
Added
- The asset bank, a user-level catalog in
~/.spawn-mcp/assets/(SPAWN_ASSET_BANKoverrides; a setting pointing at a.jsonfile is read as its parent directory). User-level rather than per-repo on purpose: the team ledger lives in a repo's shared.gitbecause a team is scoped to one game, which is exactly wrong for a store whose value is crossing games. Concurrent MCP processes share it through the same cross-process lock the team ledger uses. Five tools, taking the solo list from 27 to 32.spawn_asset_scan: harvest everycdn/path from one or more directories, recording which files cite it and which game it belongs to. Reads raw file text rather than the compiled spec, so it also catches paths assembled in script strings and works on a project that does not compile. A?animations=query normalizes away, so one asset never becomes three rows.spawn_asset_search: query by text, name, category, filename prefix, kind, slug, namespace, verdict, project, game, or reuse count. Ranks named, described and known-good assets above bare path matches and sinks bad ones, and reports the true match count rather than the page size, so a truncated result never reads as "that is all there is".facets: truereturns counts by category, kind, style family and namespace over the whole match set, which is why there is no separate listing tool.spawn_asset_note: a name, a category, description, tags, a good/bad verdict, andreplacedBy— including for a path that has not been scanned or even used yet, since the moment before the first fetch is when the name is still changeable. There is deliberately no delete: a bad name is the one record that cannot be recovered by scanning again.spawn_asset_preview: existence check plus, for images, the bytes inline so the model can judge the art instead of inferring it from a filename. Checks the storage host rather than the/cdn/cook route, because the cook route generates on first fetch and answers 401 for anything it will not cook anonymously — it conflates "does not exist" with "not allowed to ask", and asking there is not side-effect-free. Storage answers a plain 200/404 and never cooks. The storage prefix carries an environment and a version segment, so it is resolved by following one redirect and re-resolved when a cached prefix starts missing, rather than hardcoded.
spawn_asset_sync, the authoritative fill. There is no asset API —/api/agent/v1/assets,/mediaand/cdnall answer404 not_found, and the only account-wide endpoint is the game list — so the closest thing to a catalog is the set of specs your own games have pushed. The sync lists every game on the account, fetches each one's current server-side spec, and harvests itscdn/paths, attributing each to the script it appears in. This sees what a local scan structurally cannot: games with no checkout on this machine, and assets a teammate or Savi pushed that never reached your disk. On the account it was built against, a local scan of three projects found 169 assets and the sync found 408 — the same game with 105 references on disk had 298 in its pushed spec. Slow on purpose (one spec fetch per game, each carrying every script source; four at a time, six games ≈ 3s). Specs are fetched outside the bank lock so a multi-minute sync cannot stall other agents' notes, and a game that errors is reported and skipped rather than losing the whole run.- Every asset tool recommends a sync when the bank is empty, never synced, or over a week stale — not just search. The damage happens at the moment of invention: a stale bank answers "no match", the model coins a fresh path, and an asset that already exists under a good name is regenerated under a second one. Because a path cannot be re-rolled, those two names can never be merged afterwards, so the warning belongs on every surface that could precede it.
- Names. A name is a short unique handle, and every tool that takes a path takes a name instead —
spawn_asset_preview path="knight"rather than sixty characters of style family and hyphenation, which is the same problem the bank exists to solve. Collisions are refused rather than silently reassigned, since a handle that resolves to two things is not a handle. Lookup is case-insensitive, andreplacedByaccepts a name too. - Your own categories, kept separate from the filename's. The token a path starts with (
model-,texture-) is nowprefix, andcategoryis free-form and assigned by you ("enemies", "ui-icons"). They are different axes — one is a type that largely restateskind, the other is what the asset is for — and both are searchable. Assigned fields are the only ones written to disk; derived ones are recomputed on read, so a rescan can never overwrite a judgement and a grammar change cannot leave an asset mis-filed. - Reuse is counted in games, not directories. In team mode one game is several worktrees, each its own project directory, so counting directories reported a three-agent team as three games — a headcount of your own agents dressed up as a reuse signal. Each use now records the
variantIdfrom that project's own.env(never the process fallback, which during a multi-directory scan would stamp one variant onto every project), and the count collapses uses that share one. A project naming no variant counts on its own rather than being quietly merged. Reuse across games feeds ranking, andminGamesfilters on it. - Sharded storage, one file per style family plus
_meta.json, with therootnamespace split by filename prefix (root-effect.json,root-sfx.json) because it has no style family and is typically the largest group — on the bank this was developed against, 116 of 170 assets, which would have recreated the single oversized file sharding exists to avoid. Measured at 821 bytes/asset a 10,000-asset bank is ~7.8 MB and parses in ~19 ms, so this is explicitly not a search optimisation: it keeps a one-field note from rewriting the whole catalog, and keeps each file openable by a human. A corrupt shard loses that one family rather than the catalog. - Namespace classification and collision warnings. Paths are read as
moodboard(the documentedmoodboard-<slug>/<category>-<name>.<ext>form),root,custom, oringested. Arootpath is a bare global name shared with every other Spawn game — if naming is creating and nothing namespaces it,cdn/model-tree.glbis whatever the first fetch anywhere produced — so it warns on every retrieval. A moodboard slug outside the nine canonical families is noted but allowed.ingested(public.<base64>) paths are opaque uploads, not names, and are ranked last so they are never offered as a style example.
Fixed
- The session guide no longer tells the model it cannot make art.
spawn_sessionandspawn_getting_startedboth claimed "you CANNOT generate images or conjure 3D models through this MCP … your art levers are cdn/ assets that already exist", and pointed at Savi instead. Naming a path is the generation lane, so this steered agents away from the engine's main art affordance and toward untextured primitives — the exact failure the 1.3.0 skills work existed to prevent. It now explains the mechanism, the moodboard convention, the canonical slugs, that a path cannot be re-rolled, and that bare names are globally shared.