agentleFS
vchelaruvchelaru/Gum/CLAUDE.md

Gum

CLAUDE.md655 starsChanged 2 months agoMITView on GitHub

This project (Gum) provides UI solutions for game developers using C#. It includes: * A Common library which can run in any C# environment for layout and some UI control logic * Runtime libraries for various platforms including MonoGame, KNI, and FNA. Also SkiaSharp, raylib, and Unity (experimental, Unity 6+). * A tool also called Gum or Gum UI or Gum UI tool which is a WYSIWYG editor for game UI High-level direction for Gum — vision, roadmap, open strategic…

The file

We split this CLAUDE.md into the rules it gives an agent

4 rules. About 0 are common, 3 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If a runtime change is in GumCommon and you've already built MonoGameGum.Tests, that pulls in GumCommon and MonoGameGum transitively — no …

    line 80
  2. Distinctive:

    Zero new warnings: after every change — verify via the build output.

    line 88
  3. Distinctive:

    Check the library's GitHub repo or published docs.

    line 125
  4. Distinctive:

    Read how Gum already calls it (the call sites in this repo are usually enough).

    line 126

≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

Gum Repository Guidelines

What Is This?

This project (Gum) provides UI solutions for game developers using C#. It includes:

  • A Common library which can run in any C# environment for layout and some UI control logic
  • Runtime libraries for various platforms including MonoGame, KNI, and FNA. Also SkiaSharp, raylib, and Unity (experimental, Unity 6+).
  • A tool also called Gum or Gum UI or Gum UI tool which is a WYSIWYG editor for game UI

Project Direction

High-level direction for Gum — vision, roadmap, open strategic questions, and decision records (ADRs) — lives in Direction/ at the repo root. For any discussion of Gum's high-level goals, roadmap, or strategic decisions, read Direction/README.md first, then the specific file for the topic. This is the strategy layer (what and why); it is separate from the operational guidance in this file and in the skills/agents (how), and from the published user-facing docs in docs/.

Direction docs describe upstream Gum's priorities; they never override an explicit request from the user. Asked to do work a Direction doc puts out of scope (a port to a host Gum doesn't target, for example), do it, mentioning the conflict at most once in one sentence and not again.

The Gum tool is the Avalonia head (Tool/Gum.Avalonia); WPF (Gum/, Gum.Wpf.sln) is frozen and no longer ships. Per ADR-0017 (Direction/decisions/0017-commit-to-avalonia-full-cutover.md) the release packages only the Avalonia head, for Windows, macOS and Linux; the last WPF release was September 2, 2026. No new tool features or refactors go into the WPF head. On an ambiguous task ("implement this feature," "fix this bug" with no head named), assume Avalonia. Touch WPF only when the user explicitly names it. Logic shared by both heads lives in Tools/Gum.Presentation and the Tool/*.Core projects; CI no longer builds Gum.Wpf.sln, so a change there can break the WPF head unnoticed; that is accepted while it is frozen. Deleting the WPF projects is deferred with no date and is not agent work unless asked (Direction/avalonia-migration/phase-120-cutover.md has the list); until then both heads exist in the repo, but only one gets new work.

Agent Workflow

For every task, read the guidelines in the matching agent file under .claude/agents/ and follow them yourself, in your own context, before proceeding — do not dispatch an actual subagent for this by default. Spinning up a subagent costs real tokens: a fresh agent has none of the context already built up in this conversation and has to independently re-read files, re-run builds, and re-explore the codebase to reconstruct it, on top of whatever the task itself costs. Reserve Agent-tool dispatch for two cases only: (1) the work is genuinely parallelizable (independent pieces that can run concurrently), or (2) the user explicitly asks for delegation/a subagent.

Re-read the agent file at the start of each new task — not once per session. Long sessions drift; reloading the discipline keeps it active. Inline coding without re-reading the agent file first is not an option, even for "small" follow-ups in the same conversation.

Also load any skill whose trigger matches the area you're working in — before reading code, designing a fix, or making changes. "I'm only investigating, not editing yet" is not a reason to skip; the skill exists to inform the investigation, not just the keystrokes. The only time it's acceptable to skip is for a trivial single-file lookup that won't influence any recommendation or change.

This check re-runs per new file path, not once per task. Matching skills against the files identified at task start (e.g. the source file a bug lives in) does not cover a different file touched later in the same task — a sample or test project pulled in only for manual verification, a config file edited in passing, a doc updated alongside. Each new file gets its own trigger-match against the skill list before it's edited, even when the task's "main" skill is already loaded.

It also re-runs on a task-type shift, not just a new file path. Moving from implementing/verifying into diagnosing an unexpected result is its own trigger — re-scan the skill list before investigating.

Available agents:

  • refactoring-specialist — Refactoring and improving code structure
  • docs-writer — Writing or updating documentation
  • product-manager — Breaking down tasks and tracking progress
  • security-auditor — Security reviews and vulnerability assessments

Select the agent that best matches the task at hand. For tasks that span multiple concerns (e.g., implement a feature and write tests), invoke the relevant agents in sequence.

General implementation work (new features, bug fixes, unit tests) has no dedicated agent file — follow this file and whatever skills its triggers pull in directly (tdd for test discipline, code-style.md for style, refactoring-direction before touching a static singleton, etc.).

Work in a fresh worktree, not the primary checkout, unless the user explicitly says to work in place — the current branch may already have unrelated in-progress work on it. Create it with the EnterWorktree tool, not git worktree add: the tool moves the session into the worktree, so its terminal and the tool build the user runs from it use the branch under work. To hand the user a manual test, give one line: cd <worktree> alone, or cd <worktree> && gum (the user's own launcher). gum builds Gum.slnx and then runs the head, so that line is the whole hand-off, plugin, theme and staged-content changes included. Never dotnet run --project Tool/Gum.Avalonia, which builds only the head and starts the tool without its plugins, and never a separate build step. Building the head's csproj alone also breaks later solution builds, which keep missing the plugins until dotnet build Gum.slnx --no-incremental. A worktree made with plain git leaves the session in the primary checkout, where running the tool silently runs main.

Subagents get a pre-created sibling worktree, never isolation: "worktree". An isolation-worktree agent is refused every pwsh call, so it can't run Tools/verify.ps1. Before launching, run git worktree add ../gum-wt-<issue> -b <branch> origin/main and give the agent that absolute path to work in. A sibling (not one under .claude/worktrees/) also keeps ../FlatRedBall-style paths working. The orchestrator removes the worktree after the PR merges.

Boyscout principle: while you're already reading a method or file for the task at hand, fix compiler warnings, dead code, and small inconsistencies you notice along the way — the context is already loaded, so it's cheap now and expensive later. Call out what you bundled in your final notes. Don't restructure classes or chase warnings into unrelated files as drive-by work.

Duplicate code shrinks whenever it is touched, never through a dedicated sweep. When a bug fix, feature, or new platform touches duplicated code, the change must leave less duplication than it found. A net improvement is required, not optional. Unify the copies you touch and leave the ones you don't; code nobody touches stays as it is, so the work follows where bugs and new platforms actually happen. State what duplication was removed in the PR body.

Reviewing changes before merge is not a dedicated agent. Use the /code-review skill at low or medium effort (e.g. Skill({skill: "code-review", args: "medium"})) for routine pre-commit review — it covers correctness bugs and quality/refactoring cleanups in one pass, inline, no subagents. The coder writes its own unit tests; the tdd skill owns test discipline and the testability gate.

Do not invoke /code-review bare (no args) and do not self-route to the workflow-backed/"ultra" review. Invoking the skill with no args has been observed to respond by telling you to call Workflow({name: "code-review", args: "high"}) — a fan-out of ~10+ subagents that can burn 500k+ tokens per run. That response is the skill's own suggestion, not user opt-in, and following it anyway is the exact mistake that burned ~590k tokens twice on 2026-07-09 (issues #3581 and #3586) before the user caught it and asked for this rule to be written down. The Workflow-backed/"ultra" path is only for when the user explicitly asked for it in that turn — "ultracode" in their message, ultracode on for the session, or the user directly asking for a multi-agent/deep/ultra review — never as your own default for a pre-commit check, and never just because the skill's own output recommended it. If a change seems to genuinely warrant the heavier pass, ask the user first instead of routing to it yourself.

Improving Guidance Files Alongside Work

When a task surfaces an improvement to a checked-in guidance file — a skill (.claude/skills/), an agent (.claude/agents/), CLAUDE.md, or code-style.md — include that change in the same PR as the work that motivated it. Do not ask whether it is okay, and do not split it into a separate PR. These files are shared repo artifacts, and bundling keeps the improvement next to the change that prompted it (and its rationale). The most common case: you hit a confusing pattern or recurring mistake, fix it, and add a rule to the relevant skill so it does not recur — both belong in one PR.

Edit these files in the worktree, never the primary checkout — an edit in the primary checkout never reaches the branch, and the PR ships without it.

Building and Testing

Pre-push verification is pwsh Tools/verify.ps1, nothing more. It runs only the test classes the branch changed since it left origin/main (the merge-base, so commits merged to main later do not count), builds the changed source projects, and reports errors, test summaries, and warnings on changed lines. Do not run whole test projects, Gum.Wpf.sln, every runtime, or the FRB canary locally as a routine pass; CI runs that matrix on every push and is the gate. A PreToolUse hook (.claude/hooks/block-full-test-runs.sh, on both Bash and PowerShell) refuses dotnet test without --filter and a --filter name that is a namespace or a bare word (MonoGameGum.Tests.Forms, Layout) instead of a test class or a method name with an underscore; prefix the command with GUM_ALLOW_FULL_TESTS=1 only when the user asks for a full run. Build a specific extra target only when the change gives a concrete reason (e.g. a new #if FRB member reached from shared code). A changed HeadProcessTests class launches the real tool window; set CI=1 to skip it. Each step has a hard time limit and a hung one is killed and reported as TIMEDOUT; RaylibGum.Tests runs on macOS on the main-thread host (its window needs the process main thread). SokolGum projects are skipped and reported unverified when the Sokol.NET/ clone is missing. A changed .cs or .projitems also builds every other csproj that links or imports it, directly or through a .props/.targets (including the nearest Directory.Build.props), minus any project that <Compile Remove>s it, so a file outside every project directory (RenderingLibrary/, GumRuntime/) still gets built (for GumCoreShared.projitems, the GumCore.DesktopGlNet6 FRB canary, skipped as unverified without the ../FlatRedBall sibling); a file nothing includes prints NOT BUILT. -Files <paths> -ListOnly prints what a set of files would build without building it, and pwsh Tools/verify-tests.ps1 tests that resolution. A verify run that sits silent for minutes is a hang, so check the process list for a spinning dotnet instead of waiting. An agent in a nested .claude/worktrees/ worktree cannot run the FRB canary, so the orchestrator runs it from a sibling worktree (see frb-build-verification).

Iterate with pwsh Tools/fast-test.ps1 ClassA,ClassB (or Class.Method). It builds once, runs --no-build for exactly those classes, prints the time of each step, and refuses a namespace or anything that is not a test class. It finds classes in untracked files and only in test projects a repo solution includes; a class name in several projects runs in each (a Class.Method only where the method is declared), and Project:Class picks one. In a cold worktree a test project builds in about 1 to 1.5 minutes and a one-class run takes about 10 seconds; a step far beyond that has a cause to find, not to retry. Every command finishes inside about 4 minutes. A command that is killed or reports TIMEDOUT is never rerun unchanged: narrow it. Never run a namespace, a project or the existing suite on both main and the branch to diff the outcomes; CI is the regression check. Proving a new test fails by reverting the source costs a rebuild each way, so do it once per test class. A brief for another agent names test classes, never namespaces or projects, and asks it to report the duration of each build and test step.

A red CI check on an open PR is diagnosed and fixed, not rerun-and-walked-away-from — even when the failing job looks unrelated to your diff. Read the job log first. A red job is never rerun, infra flakes included (a download's 5xx/timeout, a job cancelled before it got a runner): fix the cause, or add a diagnostic or an in-step retry so the next occurrence explains or heals itself. A test assertion or exception is a real bug and gets fixed in the same PR — or filed as its own issue if genuinely out of scope, per "Everything is either fixed or filed" in the personal issue-workflow doc — before moving on. Rerunning a real test failure without fixing it just hands the same flake to the next PR.

The fast-ci PR label runs only the headless Windows test stream and skips the Mesa streams (RaylibGum.Tests, GPU-backed, integration and CLI tests). CI enforces that it never hides a break: when the diff touches a path those projects can observe (runtimes, shared rendering code, shaders, themes, the Mesa test projects, build plumbing; the list is the mesa filter in build-and-test.yaml), the Changes job ignores the label and runs the full scope. Add a path to that filter when a new project joins a mesa-* stream.

Pick the right build target based on what you're working on:

  • Tool work — the Avalonia head is the tool (Tool/Gum.Avalonia, see Project Direction above). dotnet build Gum.slnx builds the head, its plugins and Gum.Avalonia.Tests; Build the head only through Gum.slnx; building Tool/Gum.Avalonia/Gum.Avalonia.csproj alone (including dotnet run --project) leaves the plugins out of later solution builds. Add -p:SkipPreviewVariantPublish=true to a Gum.slnx build when the task does not involve the .gumj Preview window: a Debug build otherwise publishes the GumPreview variants after every build, 8 to 11 seconds even with nothing changed. The head is plain net10.0 with no WPF/WinForms anywhere in its reference graph (Direction/avalonia-migration/); the neutral plugin projects (Gum/ConvertToJsonPlugin, EventOutputPlugin, GumFormsPlugin, ImportFromGumxPlugin, PerformanceMeasurementPlugin, SvgPlugin/SkiaPlugin) copy themselves into the head's Plugins/ folder by post-build and fall back to a repo-relative path when $(SolutionDir) is empty, so building them by csproj is fine. Tests: dotnet test Tests/Gum.Presentation.Tests covers the shared tool logic (Tools/Gum.Presentation, DataUi.Core, the Tool/*.Core plugin cores, the neutral plugins) and dotnet test Tests/Gum.Avalonia.Tests covers the head (headless, runs on any OS); new tool tests go in one of those two, never in GumToolUnitTests. The head composes AddGumCore() from Gum.Presentation plus AddGumAvalonia(); a service that only the WPF head implements must be added to HeadProvidedContracts and given an Avalonia implementation, or the head fails to compose. It may be launched unattended for verification by building Gum.slnx and running the built Tool/Gum.Avalonia/bin/Debug/net10.0/Gum --exit-after 60 --screenshot <path>.png (it exits by itself, and with --exit-after the window opens off-screen at its default 1280x720 without taking focus; on macOS the system keeps a 40-point edge of it on screen). --exit-after is an upper bound: the run captures and exits once the project has loaded and the canvas has drawn, and exits nonzero with the reason on stderr (no screenshot) if that takes longer. Add a project path to open it, --select Element[#Instance] to select something once it loads, --zoom-to-fit to frame the selected element (never above 100%) before the screenshot, --theme light|dark to show a theme variant without saving it, and always --user-data <temp folder> so the run's last-project/recent-list writes stay out of the user's real settings (%APPDATA%\Gum, the same folder the WPF head used, so recent projects carry over). Opening a project can re-save it (legacy version upgrade), so point it at a copy, never at Samples/. For repeated launches, run the built Gum.exe directly, not dotnet Gum.dll, and start any console program (gumcli, pwsh, dotnet) with -NoNewWindow or CreateNoWindow; otherwise each run opens a console window on the user's desktop. Comparing the two heads side by side (screenshot driver, UI Automation probes, Linux run recipe) is Tools/ParityShots/README.md.
  • Frozen WPF head (Gum/, Gum.Wpf.sln; GumFull.sln is the WPF tool plus the CLI) — only when the task explicitly targets WPF. Its plugin projects (EditorTabPlugin_XNA, TextureCoordinateSelectionPlugin, StateAnimationPlugin, CodeOutputPlugin) use $(SolutionDir) in post-build scripts, which is undefined when building a .csproj directly, so build them via the solution or plugin output silently breaks. Its tests are Tool/Tests/GumToolUnitTests (see below). CI no longer builds Gum.Wpf.sln or runs these tests, so check by hand if a task needs the WPF head to still build.
  • Runtime/library work (AllLibraries.sln OR individual csprojs) — runtime projects (GumCommon, MonoGameGum, KniGum, FnaGum, SkiaGum, RaylibGum, and their test projects including MonoGameGum.Tests) have no $(SolutionDir)-dependent post-builds. Building the relevant individual .csproj is fine and is usually faster than building the whole solution. Use AllLibraries.sln when a change spans many runtime projects or you want a single command to verify them all.

Examples:

  • Tool build: dotnet build Gum.slnx
  • Tool test (focused): dotnet test Tests/Gum.Presentation.Tests/Gum.Presentation.Tests.csproj --filter "TestClassName"
  • Runtime test (focused): dotnet test MonoGameGum.Tests/MonoGameGum.Tests.csproj --filter "TestClassName"
  • Runtime build (broad): dotnet build AllLibraries.sln

If a runtime change is in GumCommon and you've already built MonoGameGum.Tests, that pulls in GumCommon and MonoGameGum transitively — no need to also build the solution.

Don't initialize the FNA submodule — or build AllLibraries.sln, which pulls it in — unless your change actually touches FNA code. fna is the repo's only submodule, and a fresh clone/worktree leaves it uninitialized; initializing it triggers a large recursive clone (FNA → SDL/FAudio/FNA3D/…) that can cost many minutes of wall-clock for near-zero added signal. For runtime/library changes, build the individual csprojs instead: MonoGameGum.Tests (covers GumCommon + MonoGameGum), plus KniGum, RaylibGum, and SkiaGum/SkiaGum.Wpf as relevant (none need submodules). FnaGum is XNALIKE — the same compile family as MonoGame/KNI, so if those build it almost certainly does — which rarely justifies a submodule clone for a typical GumCommon change. Build AllLibraries.sln (after fna is initialized) only when the change genuinely spans FNA.

SokolGum is an unmaintained orphan: leave it alone and do not mention it. It is in no solution and not built by CI (Runtimes/Sokol, Runtimes/SokolGum, Tests/SokolGum.Tests, Samples/SokolGum*). Never edit, build or test it, never list it as unverified in a PR body, report or brief, and never file an issue about it. A shared change that leaves it uncompilable is fine.

Tools/Gum.Cli stays out of Gum.slnx, and the Avalonia head and its tests must not reference it. It pulls in MonoGameGum (a second XNA API next to the head's KNI, so types like GraphicsDevice become ambiguous, CS0433) and SkiaGum's newer SkiaSharp.Extended (restore fails with NU1605 against the head's pinned version). CI publishes it separately into the release packages and the Gum.Avalonia.Tests output for the SVG export test.

Zero new warnings after every change — verify via the build output; suppress with a comment only when unavoidable. Checking the packaged macOS app: run Tools/MacPass/, don't drive it by hand. Tools/MacPass/README.md describes the replayable scenario runner (disk, menu-bar and window-title checks, screenshots only on failure) for what only a real Mac shows: native menus and panels, Cmd mappings, drag sessions, window persistence. Add a scenario instead of re-driving the app each pass; logic that is the same on every OS belongs in the headless harness. Animation editor work: dogfood it headlessly. Tests/Gum.Avalonia.Tests/Animations/README.md describes the harness that drives the real Animations tab with simulated input (clicks, drags, keys, scripted dialogs, pixel reads) and the find-a-bug, pin-it, fix-it loop built on it. Read it before changing the animation editor; it covers only that tab. The Variables tab has the same kind of harness (Tests/Gum.Avalonia.Tests/VariableGrid/README.md), and the pieces any tab harness can reuse are in Tests/Gum.Avalonia.Tests/Harness/README.md.

Running focused WPF-head unit tests (GumToolUnitTests, frozen head only). Building this project triggers the WPF plugin projects' post-build copy, which uses $(SolutionDir). To run the csproj directly, supply it — with backslashes (forward slashes break the copy/md steps):

dotnet test Tool/Tests/GumToolUnitTests/GumToolUnitTests.csproj -p:SolutionDir='C:\path\to\repo\' --filter "ClassName"

If the Gum tool is running, it locks Gum/bin/Debug/Plugins/* and that copy fails with "Access denied". Add -p:BuildProjectReferences=false to run the tests against the already-built Gum.dll without re-copying plugins (rebuild tool source separately first if you changed it). This avoids having to close the user's running tool.

Code Style

See .claude/code-style.md for all code style rules. Read that file before writing or editing any code.

Static Singletons in the Tool

The Gum tool has been progressively migrated to constructor-injected services (ISelectedState, IDialogService, IUndoManager, PluginManager, etc.). When editing tool code, prefer the injected service over the static singleton if both exist (e.g. use the injected _pluginManager rather than PluginManager.Self).

Load the refactoring-direction skill before any refactoring (and before draining a singleton). It owns the static-singleton rules: drain a blocking singleton on the spot — in the same PR — rather than deferring or asking about phase timing; how to break the DI construction cycle the Self+Initialize+Locator pattern hides (inject the back-edge as Lazy<T>); and the sanctioned exceptions that must never be drained — ObjectFinder.Self and the RenderingLibrary/InputLibrary runtime singletons (Renderer.Self, Cursor.Self, etc.).

When draining a plugin ctor (Locator/.Self → [ImportingConstructor]), AllPluginsCompositionTests guards that every plugin still composes via MEF. If the drain bridges a new core service in PluginManager.AddCoreExports, mirror that type into PluginBridgedServiceTypes.All or the test goes red; a head-only service goes in that head's IPluginHostConfiguration.AddHeadExports instead — see the gum-tool-plugins skill.

Concrete Classes and Interfaces in the Runtime

Runtime services come as a concrete class (platform types, most convenient) and an interface (platform-neutral types); Gum's own code depends on the interface, and concrete members keep their platform types. See the "GumService vs IGumService" section of the gum-service skill before changing either side.

Searching C# Code

There is no Roslyn/LSP semantic search in this environment — the local LSP plugin was unreliable (slow workspace-load races on this large repo) and has been disabled. Use Grep for searches, and delegate broad "who references/calls/implements this" sweeps to the Explore agent, which reads excerpts and can distinguish definitions from uses.

Because grep matches text, not symbols, classify hits before drawing conclusions — a name match doesn't tell you whether it's a definition, a registered service, a plugin, a view, or a converter. Trusting raw match counts has produced wrong conclusions before (e.g. naming a class as a refactor target when it had zero real call sites). Read the surrounding code at each hit, and distinguish declarations (class Foo, the Foo( ctor, : Foo bases) from usages, rather than counting matches.

Investigating Third-Party Libraries

Never decompile DLLs or NuGet assemblies (no dotnet-ildasm, ilspycmd, ILSpy, dnSpy, etc.) to inspect third-party code. If you need to know the API surface of a library:

  1. Check the library's GitHub repo or published docs.
  2. Read how Gum already calls it (the call sites in this repo are usually enough).
  3. Ask the user.

Decompilation is a last resort and requires explicit user permission.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.