aseprite-ai-artist
AGENTS.md473 starsChanged 44 days agoMITView on GitHub
Guidance for agents and humans changing this code. For using the tool, see the README. The repository is a pnpm workspace driven by turborepo. The plugin stays the root package on purpose — the marketplace installs from ./ — and gallery/ and apps/web/ are outside the published files list, so they never ship. pnpm-lock.yaml is the workspace lockfile; the root package-lock.json is kept only for bin/aseprite-ai-artist.mjs, which builds a git install with npm install. After changing root dependencies, refresh it…
- Installs packages
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,542 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
The file
We split this AGENTS.md into the rules it gives an agent
6 rules. About 0 are common, 0 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 47
Never let a failure become a disk edit: When Aseprite is not attached, tools refuse with not_connected and doNotFallBackToDisk.
- Very specific: line 77
function t["key"]()is not valid Lua: Use t["key"] = function(). - Very specific: line 80
Assigning
cel.imageinvalidates the old handle: Read img.width/height into locals before the swap or the next line raises "Tried to … - Very specific: line 114
Use an isolated Aseprite for e2e — ASEPRITE_USER_FOLDER pointed at a scratch directory — so a test never touches someone's real documents.
- Very specific: line 135
Commit, then tag v<version> — the workflow fails on purpose if the tag and the manifest disagree, because npm would otherwise ship the …
- Very specific: line 158
Read docs/README.md inside that installed package first, then read the relevant pages from its docs/ directory before changing Turborepo …
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Working on this repository
Guidance for agents and humans changing this code. For using the tool, see the README.
Shape
| Path | What |
src/ | The MCP server, bridge, control client and CLI (TypeScript) |
extension/ai-artist.lua | Everything that runs inside Aseprite — every command handler, for both modes |
headless/runner.lua | Headless mode's stdin/stdout loop around the same extension, run by aseprite -b (ADR-0008) |
rules/ | Pixel-art craft, served as rules:// resources |
skills/ | Workflows, served as skill:// resources and MCP prompts |
agents/, hooks/ | Claude Code plugin surface; omp reads the same tree as a marketplace plugin |
omp/ | omp extension replaying hooks/hooks.json with omp's event API; hook text lives in hooks/shared.mjs |
opencode/ | OpenCode V2 plugin: the fourteen commands, skills and the two hooks, from the same SKILL.md files the omp extension reads |
tests/ | Node tests, plus a Lua harness that runs inside aseprite -b |
docs/adr/ | Why things are the way they are. Read these before arguing with them. |
gallery/ | Workspace package: community generations, benchmark prompts, and the zod schema + checker both are held to (ADR-0006) |
apps/web/ | The SvelteKit site that renders the gallery and the benchmark; deploys from main only. Feature-Sliced Design under src/ (ADR-0007); routes/ stays a thin router and pnpm --filter @pebbly/web run lint:fsd enforces the layer rules |
The repository is a pnpm workspace driven by turborepo. The plugin stays the
root package on purpose — the marketplace installs from ./ — and gallery/
and apps/web/ are outside the published files list, so they never ship.
pnpm-lock.yaml is the workspace lockfile; the root package-lock.json is
kept only for bin/aseprite-ai-artist.mjs, which builds a git install with
npm install. After changing root dependencies, refresh it with
npm install --package-lock-only --ignore-scripts — CI's npm ci job fails
when it drifts.
The rules that are not negotiable
Nothing may be declared and not implemented. A schema field, an op value
or a check name that the Lua side never reads is worse than a missing feature:
the call validates, succeeds, and does nothing. A 2026-09-08 audit found six of
these at once — transform.scope, gradient.dither, validate's outline and
banding checks, tag's repeat, and a linked flag hardcoded to false. If
you add a field, add the branch that reads it in the same change, and a test
that fails without it.
The tool count stays at or under 24. A test enforces it. The whole design is
in ADR-0003: every tool schema costs
context on every turn of every conversation, including the ones that never touch
Aseprite. If you need new behaviour, add an op to an existing noun.
Never let a failure become a disk edit. When Aseprite is not attached, tools
refuse with not_connected and doNotFallBackToDisk. An agent that "recovers"
by editing the .aseprite file makes changes the user cannot see and their next
save destroys. This is the single most important behaviour in the project.
Headless mode (ADR-0008) writes files because
someone chose it — the operator at startup, or the user through preflight
mode="headless"; nothing may ever switch into it as a recovery from a failed
live call, and it refuses any file a live window has open.
Every mutation goes inside app.transaction. One agent action must be one
Ctrl+Z for the user.
Never leave the user's active sprite, layer or frame changed as a side
effect. Use preserving_site.
Unsupported means unsupported. An unknown command returns
unsupported_command. Never no-op silently.
Lua gotchas that have already cost a day
Aseprite's Lua environment is not plain Lua, and its deviations fail silently:
json.decodereturnsuserdata, not a table. Everything from the wire goes throughto_plain()at the boundary. Do not skip it.- A decoded JSON array yields nothing from
pairsbut answers#andipairs. A decoded JSON object also answers#— with its key count. Onlyvalue[1] ~= nildistinguishes them. Getting this wrong turns a 40-op draw batch into zero ops that report success. print()inside a WebSocket callback does not reach stdout. Trace through the status file instead.function t["key"]()is not valid Lua. Uset["key"] = function().- A Lua table cannot hold a
nilvalue, so an absent field is a missing key and never an explicit null. Output schemas use.nullish(), not.nullable(). - Assigning
cel.imageinvalidates the old handle. Readimg.width/heightinto locals before the swap or the next line raises "Tried to access a deleted 'ImageObj'". cel.imagereturns a fresh wrapper each read, sorawequalnever matches. Comparecel.image.idto tell whether two cels share one image.Sprite:newFrame()bypasses the command machinery, andLinkCelswill not link a frame made that way. Create throughapp.command.NewFramewhen the frame has to be linkable.NewFrameLinkdoes not exist in 1.3.- A scratch
Spritemust be created insidepreserving_site, not before it. - Global
printmust be restored on every path —lua.runhijacks it to capture output, and a throw that skips the restore leaves every other script's output swallowed for the rest of the session. - A tilemap cel needs a
ColorMode.TILEMAPimage, built from anImageSpec. The generic cel helper hands back an RGB image and every stamp is silently lost. - Tile index 0 is Aseprite's reserved empty tile. Engines index their atlas
from 0, so leaving it in an exported atlas shifts every real tile by one and
the map renders one tile off everywhere. The exporter drops it;
getkeeps it so atlas position still matches the index you pass tostamp.
Testing
pnpm test # colour, rendering, bridge transport, MCP surface
pnpm run test:extension # the real Lua handlers, headless, against a real sprite
pnpm run test:e2e # the whole chain; needs a live Aseprite (see tests/e2e.mjs)
pnpm gallery:check # every generation and benchmark prompt against gallery/src/schema.ts
pnpm --filter @pebbly/gallery test # the checker's own rulesThe unit tests will not catch an Aseprite API misunderstanding. Every one of the
gotchas above was found by test:e2e after the isolated tests were green. Run
it before claiming a change to the Lua side works.
Use an isolated Aseprite for e2e — ASEPRITE_USER_FOLDER pointed at a scratch
directory — so a test never touches someone's real documents. Start it with
ASEPRITE_AI_ALLOW_LUA=1 too: the e2e undo checks use run_lua, and the
extension refuses it otherwise.
Style
Comments explain why, especially where the code looks wrong but is not. The Lua gotchas above are all documented at their call sites for exactly that reason. Do not add comments that restate the code.
Releasing
Publishing is automatic: a published GitHub release triggers
.github/workflows/release.yml, which runs the suite and then
npm publish --access public --provenance.
To cut one:
- Bump
versioninpackage.jsonand move the[Unreleased]heading inCHANGELOG.mdto that version. - Commit, then tag
v<version>— the workflow fails on purpose if the tag and the manifest disagree, because npm would otherwise ship the manifest's version under the release's name. - Publish the GitHub release for that tag.
Two things that will bite:
- The npm account has 2FA on writes. The
NPM_TOKENsecret must be a granular token with bypass 2FA enabled (or a classic Automation token); an ordinary token gets403 Two-factor authentication ... is requiredafter building the whole tarball. - The npm scope is
@pebbly; the GitHub organisation iswith-pebbly. They are different names on purpose —github.com/pebblybelongs to someone else.
A GitHub release marked prerelease publishes under the next dist-tag, so it
does not become what a bare npm install resolves to.
<!-- BEGIN:turborepo-agent-rules -->This is NOT the Turborepo you know
Turborepo configuration, task behavior, and CLI commands can vary between installed versions and may differ from your training data. Resolve the turbo package from this file's directory or relevant workspace; in monorepos, it may not be visible from the repository root. For example, run node -p "require.resolve('turbo/package.json')" from a workspace that depends on turbo.
Read docs/README.md inside that installed package first, then read the relevant pages from its docs/ directory before changing Turborepo configuration or commands. Heed deprecation notices. These bundled docs match the installed package version and are available without network access.
This block is written and re-added by turbo before repository-scoped commands when an AI agent is detected. In the Turborepo source repository, its template is defined in crates/turborepo-cli/src/cli/agent_guidance.rs. Removing the managed block while updates are enabled means a later qualifying invocation will add it again. Set "agentGuidance": false in the root turbo.json or turbo.jsonc to opt out; this does not remove an existing block. Keep the block committed with your work to avoid an uncommitted change on the next agent invocation.
<!-- END:turborepo-agent-rules -->Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

