Reference

Script (ci/godot/hyperdot.ps1): commands test (default), build, export, setup, detect, doctor, init, plan, bench, shot, status, audio, soak, credits, sfx, playtest, help, deck and telemetry (help [name]: a Markdown page of every command, skill and subagent, or the page of one, from help.md).

deck is the Steam Deck check in one run, at 1280x800: the textsize runner (its resolutions forced to 1280x800), the layout runner on the steam_deck profile with the off-screen check only, shot, and bench with -BudgetMs, 33.3 ms (30 fps) by default. -Scene names the scenes for all four (bench measures the first; without it, textsize and layout take their JSON files' scenes or the main scene). Every part runs even when one before it failed; the run fails with deck-failed, and deck.parts[] gives each part's own code (json.md). -Resolution other than 1280x800 is refused. Results go to deck-results (textsize/, layout/, screenshots/, bench/). bench measures this machine at the Deck's resolution, not a Deck's speed.

Parameter Default
-Project detected Folder with project.godot
-Solution nearest .sln or .slnx at or above the project (no higher than the git root, if there is one) that lists its .csproj, else the .csproj What dotnet build / dotnet test run on
-GodotVersion from csproj, else 4.7.2 e.g. 4.7.2, 4.8-rc1
-Runners detected validate, architecture, dotnet, gdunit4, godottest, gut, flows, smoke, juice, playthrough, l10n, data, textsize, layout, placement; none to only build
-Filter Passed to each runner's filter
-Retry 0 (off) test: run a runner that failed again, up to this many times (at most 5); a test that passes only on a retry passes but is listed in flaky[], summary.md and the run history. Tests in the project's quarantine.json (owner and end date each) run but do not fail the run (json.md)
-TimeoutSec 120 for status and a plain plan, none for plan -Do sync/move/... status, plan: a time limit in seconds for what can wait on something outside the script (git, a GitHub call, a long folder walk); 0 turns it off. When it runs out the result keeps what was gathered and partial lists each skipped step and what it waited for (json.md); a GitHub call of plan -Do sync that outlives it stops the command with command-timeout. Refused on other commands (invalid-arguments)
-Coverage off One merged coverage report
-MinCoverage 0 (off) Fail below this line coverage percentage
-Preset every preset export: which presets
-Run off export: launch builds this machine can run; Web builds open in a browser (a debug export of the preset, served on 127.0.0.1, with cross-origin isolation headers only for a threads build)
-DebugBuild off export: a debug build (--export-debug) in place of a release build, marked debug: true in exports[]. Android signs it with the debug keystore, so a first run on a phone needs no release keystore; with -Run, a Web preset's browser run uses this build instead of making a second one
-Browser chromium export -Run of a Web preset: chromium, firefox, webkit (Playwright's browsers: npx playwright install --with-deps <name>), chrome, msedge (the installed browsers); comma-separated. Needs Node.js; the playwright package comes from HYPERDOT_PLAYWRIGHT, the global npm folder, or is installed into the kit's tools folder. HYPERDOT_WEB_HEADED=1 opens a browser window instead of running headless
-GameVersion export: the game's version, e.g. the tag v1.2.3-rc.1: its numbers go into config/version for this export only (Windows file version, macOS and Android versions), all of it into the C# assembly's InformationalVersion, and Android presets get version/code major*1,000,000 + minor*1,000 + patch. auto reads it from git: the nearest v* tag and the commits since (1.2.3.4, 1.2.3+4.gabc1234; without a tag 0.0.0.<commits>); a shallow clone fails with shallow-checkout (json.md)
-GitHubOutput off detect: also write what it found to $GITHUB_OUTPUT; export: write version and android-version-code there
-Results, -Output test-results, build Where results and exports go (bench defaults to bench-results, soak to soak-results, audio to audio-results, deck to deck-results, shot to screenshots, credits to the project folder, sfx to <project>/sounds/sfx, playtest to playtest-results). The run log, hyperdot.log, goes in the command's results folder (-Output for shot). An export, bench, shot, audio or soak folder inside the project gets an empty .gdignore, so Godot neither imports nor exports it
-Frames 120 How long smoke runs and launched exports play; how long shot waits before each picture. 1 or more: 0 would make Godot's --quit-after never quit, so it is refused as invalid-arguments
-FixedFps 0 smoke: run at this fixed rate of game time (--fixed-fps), so -Frames is game time, as fast as the CPU allows and the same on every machine; 0 is real time
-GameArgs Your game's flags, passed after -- to smoke runs, the juice and playthrough runners (after their own args), export -Run, bench, shot, soak (every server and client) and audio -Mix; comma-separated
-Scene main scene bench and audio -Mix (first scene), shot (every scene), deck (every part; bench the first): res:// paths
-Seconds, -Warmup 10, 3 bench: how long to measure, after a warm-up that is not counted; audio -Mix: how long to record (default 60); soak: how long to run after the warm-up
-BudgetMs 0 (off) bench: fail when the p99 frame time is above this; deck: the Deck's budget, default 33.3
-MaxDrawCalls 0 (off) bench: fail (bench-over-draw-calls) when the scene averages more draw calls a frame than this
-MaxLoadMs 0 (off) bench: fail (bench-over-load-time) when the scene takes longer than this from load() to its first frame drawn (loadMs)
-GpuValidation off bench, shot, export -Run: run with the graphics API's validation layers (--gpu-validation, --accurate-breadcrumbs) and fail on their errors (gpu-validation-errors, gpuValidation). Needs Vulkan (Forward+ or Mobile) and its validation layer (Linux: vulkan-validationlayers, and mesa-vulkan-drivers without a GPU; Windows, macOS: the Vulkan SDK); a run that was not validated fails with no-gpu-validation. export -Run then launches the build with a window, so it needs a display
-Label bench: the run's name in bench.csv
-Sweep bench: <name>=<v1,v2,...>, one run per value with --<name>=<value> after -GameArgs (the game reads it like any of its flags), each a bench.csv row labelled <-Label> <name>=<value> and its own bench.<name>-<value>.json and .log; sweep in -Json holds p99 and the rest against the value. A value over -BudgetMs, -MaxDrawCalls or -MaxLoadMs is marked, not a failure; a run that cannot be measured fails the sweep (bench-sweep-failed) after every value ran
-HotspotCell 0 (10 m in 3D, 512 px in 2D) bench: the grid cell that frame times are grouped into by camera position, for hotspots (where it is slow). playtest: the heat maps' cell, over playtest.json's (default 2 m on the xz plane, 64 px on xy); given to playtest, it must be above 0 (invalid-arguments)
-Last 50 telemetry: how many of each command's newest runs to read; the command to report on is the word after it (telemetry test), one of build, test, export, bench, shot, audio, soak, credits or deck
-ReportIssue off doctor: when files the kit wrote (AGENTS.md, CLAUDE.md, docs/kit-help.md, .claude/settings.json and the like) are not committed in the game's repository, shows the issue it would file in nick-vas/hyperdot (file names only, no contents, absolute paths or game name) and creates it with gh after a yes typed or piped in. Without the flag doctor only warns and posts nothing; with no gh, no login, an existing open issue or no yes, it prints the link or command instead
-Sessions <project>/playtests/sessions playtest: the folder of session files to read: the telemetry batches playtest builds wrote or sent (.json, one batch or an array; .jsonl, one batch a line), searched recursively
-Resolution project setting bench, shot: window size, e.g. 1920x1080
-RequireResolution shot: fail with shot-size when a picture is not the size asked for with -Resolution (a project whose stretch aspect is keep renders at its base size, with bars); without it that is a warning, and shots[].requested and shots[].sizeMatches say which
-NameBySize off shot: put each picture's own size in its name (main.1920x1080.png, with a locale main.fr.1920x1080.png), so the pictures of one scene at several sizes sit side by side and a picture that is not the size asked for says so in its name. A -Baseline taken before the names carried the size is still found under its old name
-Baseline shot: a folder of earlier screenshots; each new one is compared with the one of the same name (not the -Output folder)
-MaxChangedPercent shot -Baseline: fail with shot-changed when a picture differs by more than this percentage of pixels, or changed size; without it -Baseline only reports
-Locale, -Pseudo shot: also take each scene in these locales (fr,ar,ja) and pseudolocalized, and fail (shot-text) on text that is cut off, off screen or missing glyphs
-ColorVision shot: also save each picture as people with colour blindness see it, protan,deutan,tritan (<name>.<kind>.png, shots[].colorVision)
-MaxOffPalettePercent shot: fail with shot-off-palette when more than this percentage of a picture is in colours off the palette table in docs/STYLE.md (shots[].palette, <name>.palette.png); without it the check only warns, and there is none without a palette. With the flag and no palette: no-palette
-PaletteTolerance 3 shot: how far (CIEDE2000) a colour may be from a palette colour, or from a blend of two, and still count as on the palette; 0.5 to 50 (art.md)
-Uncapped, -MaxFps off, 0 bench: V-Sync off; or a frame-rate cap
-Headless off bench: no window (CPU timings only; works without a display)
-Sound all sfx: which sounds: coin, laser, explosion, powerup, hit, jump, blip (or bfxr's names, pickup_coin...)
-Count 1 sfx: sounds per kind, from consecutive seeds; with -Vary, how many variations
-Seed 1 sfx: the first seed, a number or text; the same seed gives the same sound as bfxr2's library
-Vary sfx: a .bfxr file to make variations of, each parameter moved by up to -Amount (0.05) of its range
-PeakDb -6 sfx: the peak every sound is scaled to, in dBFS; bfxr's sounds overshoot by up to about 2 dB between samples
-Mix off audio: also record the game's own mix with Movie Maker (needs a display) and judge its loudness
-TargetLufs -24 audio -Mix: the integrated loudness the mix must reach, within 2 LU; -18 for phones and handhelds
-MaxOnsetMs 20 audio: warn when an sfx or ui file takes longer than this to come within 10 dB of its loudest 10 ms (onsetMs)
-Clients 4 soak: headless clients started next to the one server
-MaxGrowthMbPerHour 0 (report only) soak: fail (soak-leak) when a process's memory grows steadily faster than this (one-time steps do not count; the window is -Seconds, so a short run resolves only large leaks). The gate is the smallest of three slopes after the warm-up (whole window, first half, second half; all reported as growthWholeWindowPerHour, growthFirstHalfPerHour, growthSecondHalfPerHour), so a one-time step (JIT tier-up, a GC heap resize) is not read as an hourly leak; a leak that runs the whole window is over the limit in both halves and fails. The price is sensitivity to a leak that starts late: 2000 MB/h from the 7th of 20 samples reads 558 MB/h, and a slower one can pass in a short soak. When the gate passes but the whole-window or second-half slope is over the limit, a warning (not a failure) in soak.warnings[] and the log says so; soak longer (-Seconds), since a step stops mattering as the window grows and a leak does not. A -Seconds too short for 3 memory samples (under about 3 s) is refused as invalid-arguments, since the growth could not be measured; a run cut short to fewer than 3 samples adds a "too few samples" warning.
-StopGraceSeconds 30 soak: after the run, how long the server has to quit once sent SIGTERM (Kubernetes' default grace period; Docker's is 10 s, systemd's 90 s); reported under soak.stop
-RequireCleanStop off soak: fail (soak-unclean-stop) when the server does not quit with 0 within -StopGraceSeconds of SIGTERM
-JunkPps 0 (off) soak: flood the server's own port on 127.0.0.1 with this many junk UDP packets a second (loopback only), to measure how it holds up; reported under soak.junk
-Topic help: the command, skill or subagent whose page to show; also the second word, help shot
-Json off One JSON document on stdout, logs on stderr (json.md)
-Quiet off Godot and dotnet output only in hyperdot.log in the results folder (-Results, or -Output for shot), not on the console. Use with -Json for agents
-Do, -Id, -Game status plan: init, status, done -Id <gate>, reopen -Id <gate>, add, move -Id <task> -Column, remove -Id <task>, sync, link
-Title, -Column, -Board plan add title; add/move column (an id or the board list it maps to, such as "To Do"); sync GitHub Project (experimental; <owner>/<number>, its URL, or just an owner such as @me to make a new project); link -Column the item's column, link -Board the plan's project
-Profile full plan -Do init: jam writes a plan for a game jam of 2 to 9 days, sized in days (ci/godot/templates/plan-jam.json) instead of the seven phases
-Handoff, -Checked, -Verdict status: record one subagent handoff (the subagent's name, what it checked, its verdict) in .godot/hyperdot-handoffs.jsonl instead of reporting; the newest 200 are kept and status returns them as handoffs[] (json.md)
-Feature, -Item plan add/link: the GDD feature a task delivers (F3); link: the task's project item id (PVTI_...)
-McpDiagnostics off init: also register minimal-godot-mcp (GDScript diagnostics, game console)
-McpAseprite none init: also register aseprite-mcp for this Aseprite (the program, or its folder such as C:\Program Files\Aseprite)
-Mcp none init: godot-mcp registers that editor MCP server; godot-ai prints its steps
-Agents off init: also add AGENTS.md, CLAUDE.md and docs/kit-notes.md (one line each time the kit gets in the way), and enable this kit as a Claude Code plugin in .claude/settings.json; the plugin supplies the skills, the subagents (godot-verifier, godot-architect) and the hooks, so nothing is copied into the game
-Templates off setup: also install the export templates (export installs them when it needs them)
-Plugin off init: the same as -Agents (kept for scripts written when -Agents copied the skills)
-CopySkills off init: with -Agents, copy the skills and subagents into the game instead of enabling the plugin, for an agent host that loads no plugins. Refused (the step says why) when the game already enables the plugin: each skill would load twice and the copy would go stale
-PluginSource github init: where the plugin comes from. github: this kit's GitHub repository (nick-vas/hyperdot, or the fork HYPERDOT_KIT_REPO names; works on any machine and in cloud sessions; the manifest declares no version, so Claude Code takes every commit on main as a new version; fetch it with claude plugin update hyperdot@hyperdot or turn on the marketplace's auto-update). local: the folder this kit runs from, loaded in place (a git pull in it updates every game). A game that already enables the plugin from the other source keeps it unless -PluginSource is given (the old settings.json is kept as .bak)

Environment variables, all optional:

Variable
GODOT_BIN A Godot binary to use instead of the kit's own download (in ~/.hyperdot); setup -Templates still installs the kit's Godot when this one has no export templates for the project's version
GODOT_TOOLS_DIR Where the kit keeps what it downloads (Godot, the export templates, .NET when it installs one, its other tools) instead of ~/.hyperdot
GODOT_VERSION The Godot version when neither -GodotVersion nor the project names one (default 4.7.2)
HYPERDOT_TIMEOUT Seconds a program the kit runs (Godot, dotnet) may run before it is stopped (default 1800; 0 turns the limit off), and how long setup waits for another run installing the same Godot (then install-busy); not a whole number fails with invalid-arguments
HYPERDOT_IGNORE_ERRORS A regex for engine error lines to leave out of the error count, on top of the known noise; an invalid regex fails with invalid-arguments
HYPERDOT_PLAYWRIGHT, HYPERDOT_WEB_HEADED Web export -Run: where the playwright package is; 1 opens a browser window (see -Browser)
HYPERDOT_KIT_REPO init -PluginSource github: the owner/repo the plugin is fetched from, for a fork of the kit (default nick-vas/hyperdot)
HYPERDOT_EDIT_CHECK off turns off the plugin's edit hook (hooks/edit-check.ps1): after Claude edits a .gd, .tscn, .tres or .gdshader file it loads that one file in headless Godot (ci/godot/tools/editcheck.gd, about 0.2 s whatever the project's size) and tells Claude at once if the file does not compile or names something that is not there. A scene is loaded, not instantiated, so no project code runs from the hook. It says nothing when the file loads, and nothing when it cannot check (no Godot found, the game not imported yet, a file under addons/, a C# file). It never blocks. It uses GODOT_BIN, else the kit's install under ~/.hyperdot; it is not a test run, and the Stop hook still decides when "done" is true
HYPERDOT_STOP_GATE off turns off the plugin's Stop hook (hooks/stop-gate.ps1), which, once for each set of changes, stops Claude from ending its turn while the game changed after the last full passing test run (a working tree that passed a full run, recorded as tree in the history, counts as tested whatever its file times say, so a checkout, pull or merge back to tested code is not blocked), and tells it to run the tests (after a failed run, or a second stop, it lets the turn end, so Claude must report the result). It checks only that the tests passed after the last edit, not the design
GH_TOKEN plan -Do sync (experimental): optional; the GitHub CLI reads it, and it needs the project scope (see plan.md); HYPERDOT_PROJECT_SYNC=off disables the plugin's project-sync Stop hook

Workflow inputs (godot.yml), all optional:

Input Default
project, godot-version, runners, filter detected As the script parameters
retry 0 As -Retry
coverage, min-coverage off Coverage report; fail below a percentage
pr-comment true Results comment on pull requests (needs pull-requests: write)
exports none all, or a comma list of preset names
run-exports false Launch each export on a runner of its own OS
release false Publish exports as a GitHub Release on tags (needs contents: write; with no exports the run fails rather than ending green without a release); exports of a tag carry its version (-GameVersion)
runs-on ubuntu-latest windows-latest also works. iOS presets always export on macos-latest
lfs false Pull Git LFS files; the action keeps .git/lfs in the Actions cache (keyed on git lfs ls-files -l), so only new objects are downloaded
submodules, retention-days Checkout option and artifact retention
timeout-minutes 60 Longest each job may run before GitHub cancels it (its own limit is 6 hours)
game-version Exports' version on runs that are not for a tag (tag runs use the tag): 1.2.3, or auto (-GameVersion auto; the export jobs then fetch the whole history)
environment none GitHub environment of the export jobs: its protection rules apply (required reviewers, tags only), and its secrets of the names below are used
kit-coverage none For Hyperdot's own self-test: records what the kit ran (HYPERDOT_COVERAGE) and uploads it as artifacts with this prefix; games leave it empty

Workflow secrets (godot.yml), all optional, for signing during the export. Pass them by name or with secrets: inherit, or keep them in the environment. Files are base64 (base64 -w0 release.keystore): the workflow decodes them into the runner's temporary folder, puts the path in Godot's variable, and deletes them at the end of the job; no secret is printed.

Secret Godot's variable
GODOT_ANDROID_KEYSTORE_RELEASE_BASE64 GODOT_ANDROID_KEYSTORE_RELEASE_PATH (the file)
GODOT_ANDROID_KEYSTORE_RELEASE_USER, GODOT_ANDROID_KEYSTORE_RELEASE_PASSWORD The same names (keystore and key password must be the same)
GODOT_WINDOWS_CODESIGN_CERTIFICATE_BASE64 GODOT_WINDOWS_CODESIGN_IDENTITY (the .pfx file), with GODOT_WINDOWS_CODESIGN_IDENTITY_TYPE=1
GODOT_WINDOWS_CODESIGN_PASSWORD The same name
GODOT_MACOS_CODESIGN_CERTIFICATE_BASE64 GODOT_MACOS_CODESIGN_CERTIFICATE_FILE (the .p12 file)
GODOT_MACOS_CODESIGN_CERTIFICATE_PASSWORD The same name
GODOT_MACOS_NOTARIZATION_API_KEY_BASE64 GODOT_MACOS_NOTARIZATION_API_KEY (the .p8 file)
GODOT_MACOS_NOTARIZATION_API_KEY_ID, GODOT_MACOS_NOTARIZATION_API_UUID The same names

The preset still decides whether to sign (codesign/enable and the like); a variable that is set takes the place of the preset's value, so the credentials stay out of export_credentials.cfg.

jobs:
  godot:
    permissions: { contents: write }
    uses: nick-vas/hyperdot/.github/workflows/godot.yml@v1
    with: { exports: all, release: true, environment: release }
    secrets: inherit

Your own workflow: the composite action runs one command with setup and caching, then uploads the results or the build.

      - uses: nick-vas/hyperdot/.github/actions/godot@v1
        with:
          command: export        # test, build or export
          preset: Windows
      - run: echo "upload the Windows build to itch.io, Steam, ..."

Its inputs are the workflow's (command, preset, run-exports, game-version, lfs, ...). Besides presets, preset-platforms, name, results and results-artifact, an export sets the outputs build-path (the folder with one subfolder per preset) and, with game-version, version (1.2.3-rc.1, 1.2.3+4.gabc1234) and android-version-code. For game-version: auto, check out with fetch-depth: 0; for lfs: true, with lfs: false (the action pulls, from its cache first). Android exports keep ~/.gradle/caches and ~/.gradle/wrapper in the cache when Godot's Gradle build used them.

Runners: GitHub's Linux, Windows and macOS runners; setup downloads the macOS build of Godot (universal, Godot.app or Godot_mono.app) on macOS, so macOS and iOS presets can export there. Your own machines: self-hosted-runners.md.

Cost: jobs run in the repository that calls the workflow. They're free on public repositories; on private ones they use your Actions minutes, and Linux runners are the cheapest (Windows minutes count about 2×). One test job builds once for every framework. Exports and Windows runners only run when you ask for them.

Claude Code cloud sessions run Linux. Install PowerShell in the environment's setup script, then use the script as usual:

mkdir -p /opt/pwsh && curl -fsSL https://github.com/PowerShell/PowerShell/releases/download/v7.6.6/powershell-7.6.6-linux-x64.tar.gz | tar xz -C /opt/pwsh && ln -sf /opt/pwsh/pwsh /usr/local/bin/pwsh

More on each framework, the five most popular Godot test projects, and how to set each one up: godot-pipelines.md.

architecture.json (optional, in the Godot project folder) configures the architecture runner:

{ "rules": { "parent-path": "error", "per-frame-alloc": "off" }, "maxAutoloads": 6, "simulation": ["sim", "src/Rules"] }
Rule Warns when
parent-path a node path climbs two or more levels (../.., GetParent().GetParent())
ui-mutates-state a UI script (extends a Control type, or lives under ui/, hud/, menu/) assigns to an autoload's field
per-frame-lookup _Process/_PhysicsProcess looks up nodes (GetNode, $Node, FindChild, groups)
per-frame-alloc C# _Process/_PhysicsProcess allocates (collections, StringName/NodePath, an action name as a string literal such as Input.IsActionPressed("jump"), interpolated strings, LINQ), even in a rare branch
event-not-unsubscribed a script subscribes to an autoload's C# event without -=, or connects a GDScript lambda to an autoload's signal without disconnect
too-many-autoloads the project has more autoloads than maxAutoloads (default 8)
audio-on-master a scene saves an AudioStreamPlayer, AudioStreamPlayer2D or AudioStreamPlayer3D with no bus, so it plays on Master and no volume setting but the master one reaches it
enet-server-channels create_server/CreateServer is given a channel count: Godot 4.7.x passes it as the server's incoming bandwidth limit in bytes per second, so every client's input stalls
untrusted-load load, GD.Load or ResourceLoader.Load (or a threaded load) is given a path that starts with user://: a .tres or .tscn that a player, a mod or a download put there runs any script embedded in it
culture-parse C# float.Parse, double.Parse, decimal.Parse (or TryParse) without CultureInfo, NumberFormatInfo or another format provider on the line: on a German system "0.5" parses as 5, on a French one it throws
steam-removed-api a call to IsSteamRunningOnSteamDeck or RequestCurrentStats (either casing): removed or deprecated in Steamworks SDK 1.65, and gone from GodotSteam 4.21+ and Steamworks.NET 1.65
steam-env-dotnet C# Environment.SetEnvironmentVariable("SteamAppId" or SteamGameId: on Linux and macOS .NET keeps its own copy of the environment, so the native Steam API never sees it; OS.SetEnvironment works
frame-dependent-lerp _Process/_process lerps a value toward a target and assigns it back to the same value, with a weight that has no delta in it: Position = Position.Lerp(target, 0.1f), x = Mathf.Lerp(x, target, 0.1f), x += (target - x) * 0.1f, GDScript lerp, lerpf, lerp_angle, .lerp, .slerp. A fixed fraction per frame converges faster at 144 fps than at 30 and ignores time scale; 1 - Mathf.Exp(-rate * delta) does not. Not flagged: a weight that names delta (or a variable assigned from it), a lerp between two other values, _physics_process (a fixed rate), a call split over lines or made from a helper method (why)
nondeterministic-sim Only in the folders simulation lists (prefixes or globs, from the project folder): C# Math/MathF/Mathf Sin, Cos, Tan, Asin, Acos, Atan, Atan2, Exp, Log, Pow, Cbrt (each OS's C library; Sqrt and + - * / are exact), new Random(/Random.Shared, GD.Randf/Randi/RandRange, clocks (DateTime.Now, Stopwatch, Time.GetTicksMsec), HashCode.Combine and .GetHashCode(), and foreach over a Dictionary or HashSet the file declares; GDScript sin, pow and the like, the global randf/randi, Time.get_ticks_*. Not flagged: SortedDictionary, a seeded RandomNumberGenerator (rng.randf()), anything outside the folders (determinism)

Each rule is warning (default: reported, never fails), error (fails the runner) or off. Test folders and addons/ are not scanned. The fixtures in tests/architecture-fixtures show one example of each.

assets.json (in the Godot project folder) says where each image, sound, model, font and shader came from. doctor reads it when the project has such files, and warns about:

It also reports how many files need a credit (attribution licences such as CC-BY-4.0 and MIT):

{ "assets": [
  { "path": "art/kenney_ui/**", "source": "https://kenney.nl/assets/ui-pack", "author": "Kenney", "licence": "CC0-1.0" },
  { "path": "shaders/dissolve.gdshader", "source": "https://godotshaders.com/shader/...", "author": "...", "licence": "MIT" },
  { "path": "art/player/**", "source": "own", "licence": "proprietary" },
  { "path": "art/boss/**", "source": "commissioned", "author": "Jane Doe", "licence": "proprietary",
    "agreement": "WFH-2026-03", "rights": "assigned" },
  { "path": "music/**", "source": "commissioned", "author": "Max Mo", "licence": "LicenseRef-MaxMo-Music",
    "agreement": "MM-2026-01", "rights": "licensed", "uses": ["game", "trailer", "store"] }
] }

The path patterns work like this:

The licence field takes an SPDX identifier, or LicenseRef-<name> for custom terms such as the Sonniss bundles.

Placeholders (stand-in art, sound or text that the real asset replaces before release; the Placeholders table of templates/TECH-DEBT.md) are entries with "placeholder": true, and optionally due (yyyy-MM-dd, when the real one is due) and status: needed (nobody is making it yet, the default) or in-progress. They need source and licence like any entry, since a stand-in ships if nobody replaces it. doctor warns with the list, and fails for any past its due date, as it does for an ended quarantine: the date was the team's own; replace the asset or agree a new date. status counts them (placeholders in its JSON).

{ "path": "art/boss/**", "source": "own", "licence": "own", "placeholder": true, "due": "2026-12-01", "status": "in-progress" }

"source": "commissioned" marks work made for the game by anyone else: a contractor, a freelancer, a co-founder. Under US copyright the maker owns it unless a signed agreement says otherwise, so besides licence (proprietary when assigned, LicenseRef-<name> for a licence's own terms) such an entry has:

doctor lists licensed entries with their uses. credits treats commissioned work as the game's own (no third-party credit) unless its licence asks for one. The rules: legal section 4 and gdc/contract-law-201.md.

credits builds the game's credits from assets.json: CREDITS.md, credits.json (for a credits screen) and THIRD-PARTY-NOTICES.txt (each asset's copyright line and licence text, Godot's MIT text and its third-party components read from the engine, and the .NET runtime's notices for a C# game). It fails (credits-invalid) when an entry lacks what its licence asks for. Optional fields help: title, copyright (the original copyright line), modified (what you changed), licenceFile and noticeFile (paths of the upstream licence and NOTICE files). Licence texts for MIT, ISC, BSD-2-Clause, BSD-3-Clause, Apache-2.0, MPL-2.0, OFL-1.1 and Zlib come with the kit (ci/godot/templates/licences). export copies THIRD-PARTY-NOTICES.txt next to each executable. An MPL-2.0 entry (gdext and other Rust bindings) needs source to be the URL of its source code, which the notice gives players (MPL-2.0 section 3.2(a)); copyright is optional for it. The rules: the asset-pipeline skill and store-and-legal.md.

store/steam/ (optional, in the Godot project folder, with an empty store/.gdignore so Godot does not ship it) holds the Steam store art. doctor checks the current sizes (header_capsule 920x430, small_capsule 462x174, main_capsule 1232x706, vertical_capsule 748x896, library_capsule 600x900, library_header 920x430, library_hero 3840x1240 PNG, library_logo PNG with transparency, 1280 wide or 720 tall, shortcut_icon 256 or 512 square, app_icon 184x184 JPEG, optional page_background 1438x810), at least five screenshot* files at 1920x1080 or more in 16:9, and short_description.txt (plain text, no links or time-limited wording, 300 characters at most). Wrong sizes and formats FAIL; missing files and short description problems WARN.

store/press/ (optional, next to store/steam/) holds the press kit. doctor warns when key_art or key_art_notitle (key art with and without the title) is missing, when there are fewer than 6 or more than 10 screenshot* images, and when neither fact_sheet.txt nor press_release.txt (or .md) exists or none gives a contact email address. It never fails on the press kit. The contents: the store-page skill.

juice.json (optional, in the Godot project folder) turns on the juice runner: Godot plays the game headless at a fixed 60 fps for frames frames (default 1800) with args (plus -GameArgs), and each rule checks that every event was followed by each feedback within withinMs (default 100) of game time:

{ "frames": 1800, "args": ["--autoplay", "--seed=1"],
  "rules": [ { "event": "brick_hit", "feedback": ["flash", "sfx"], "withinMs": 100 } ] }

The game prints [juice] event <name> <ms> and [juice] feedback <name> <ms>, with <ms> its game time in milliseconds. A rule fails when an event went unanswered, or never happened (the bot did not reach it). Events too close to the end of the run to be answered are ignored. tests/juice-fixture is a minimal example (-GameArgs --late makes it fail).

playthrough.json (optional, in the Godot project folder) turns on the playthrough runner: Godot plays the game headless at a fixed 60 fps for at most frames frames (default 36000, ten minutes of game time) with args (plus -GameArgs), and every text in expect must appear in a log line, in that order, the ending last:

{ "frames": 36000, "args": ["--autoplay", "--seed=1", "--no-save"], "expect": ["[Run] act 1 done", "[Run] ending reached"] }

smoke passes a bot that is stuck in the first room; this fails it and names the last milestone it reached. The game may quit by itself after the ending. The plan gate production.playthrough is checked as runner:playthrough: met once the runner is set up, so every CI run enforces it. tests/playthrough-fixture is a minimal example (-GameArgs --stuck fails). Other keys are ignored by the runner; "covers": ["GDD:S1", "GDD:S2"] names the GDD's first-session steps it plays, which status reads (gdd.firstSession). /game writes a playthrough.stub.json with the steps before the game has a bot; no runner reads a .stub.json, and it becomes playthrough.json once --autoplay exists.

The l10n runner is picked when the project has translation CSVs (imported as translations, or a first header cell keys or key), .po files or translation remaps. It has seven cases, all but the last without Godot: duplicate keys (the same key and context twice), untranslated (an empty cell; an empty or fuzzy PO entry; fewer plural forms than the language needs, counted for 15 common languages; others are not counted), missing keys (literal keys in tr, Tr, TrN, atr and TranslationServer.Translate calls that no table has; test folders are skipped), too long (a CSV cell over the row's _max characters or _lines lines; _max_<locale> overrides _max), translation remaps (a remap target that does not exist or names no locale) and localized audio (a remapped sound whose sample rate, channels or bit depth differ from the source's, whose length is outside 0.5 to 2 times the source's, or whose loudness is more than 2 LU away; skipped with a warning without ffmpeg; l10n.json sets voiceDurationMin, voiceDurationMax and voiceLoudnessLu) and missing glyphs (headless Godot, tools/glyphs.gd: a character of a translation that the default font or a font of a .tres theme lacks, fallbacks included; characters only a SystemFont could draw are a system-font warning; l10n.json's glyphIgnoreFonts leaves fonts out). tests/l10n-fixture has one of each; see localization.md.

textsize.json (optional, in the Godot project folder) turns on the textsize runner, which measures every visible text on the listed scenes (the main scene by default) at each resolution (1280x800 by default, the Steam Deck), headless: the stretch scale comes from the project settings and the letters' height from the font. A font size under minPx (9) fails; capitals under minPx or a size under wantPx (12) are warnings. Integer scaling with a base bigger than the window fails (drawn 1:1, cut off). Subtitles are text in the subtitleGroup node group (subtitles), or inside a node in it, hidden or not; their sizes are given at 1080p and scaled with each window's height: a font size under subtitleMinPx (28) fails, under subtitleWantPx (46) warns (subtitle-below-recommended). The numbers come from two GDC talks (docs/gdc/subtitles-are-changing.md). -Runners textsize runs it without the file, with the defaults.

{ "scenes": ["res://ui/menu.tscn", "res://ui/hud.tscn"], "resolutions": ["1280x800", "1280x720"], "minPx": 9, "wantPx": 12,
  "subtitleGroup": "subtitles", "subtitleMinPx": 28, "subtitleWantPx": 46 }

layout.json (optional, in the Godot project folder) turns on the layout runner, which lays out the listed scenes (the main scene by default), headless, at each profile and fails on controls that are off screen, outside the profile's safe area, or too small to touch (minTouchDp, 48) or click (minPointerPx, 24 CSS px). Profiles: phone_portrait (1080x2400 at 420 dpi, a 128 px top inset and 66 px bottom), phone_landscape, tablet (2048x1536 at 264 dpi), ultrawide (3440x1440, mouse) and steam_deck (1280x800 at 216 dpi; pointer targets, since the Deck is played with its controls and trackpads), or name:WIDTHxHEIGHT@DPI[:touch][:insets=L,T,R,B]. If the game has a SafeArea autoload with set_insets/SetInsets(left, top, right, bottom), each profile's insets are passed to it first, so the game's own safe-area handling is measured. checks limits what is reported. -Runners layout runs it without the file, with the defaults.

{ "scenes": ["res://ui/menu.tscn"], "profiles": ["phone_portrait", "tablet", "foldable:1812x2176@374:touch:insets=0,96,0,48"],
  "minTouchDp": 48, "minPointerPx": 24, "checks": ["off-screen", "unsafe-area", "small-target"] }

data.json (optional, in the Godot project folder) turns on the data runner, which checks the game's data tables without Godot. Each table names its file (JSON: an array of objects or an object keyed by id; or CSV), its id column and its fields: type (string, int, number, bool), min/max, enum, pattern, ref (another table whose ids the value must name), list (a JSON array, or ; between values in a CSV cell) and optional. Columns that data.json does not list fail ("extra": "allow" to permit them), and so does a CSV table without a committed .import saying importer="keep": Godot imports every .csv as a translation otherwise, and the exported game cannot read the file. Tables can also add up per group (totals), name money flows that need a sink (economy), hold dialogue rules with their translations ("kind": "responses", locales) or levels searched for soft-locks ("kind": "softlock"): data.md.

{ "tables": {
    "items":   { "file": "data/items.json", "id": "id", "fields": { "price": { "type": "int", "min": 0 }, "rarity": { "enum": ["common", "rare"] } } },
    "enemies": { "file": "data/enemies.csv", "id": "id", "fields": { "hp": { "type": "int", "min": 1 }, "drops": { "ref": "items", "list": true } } } } }

placement.json (optional, in the Godot project folder) turns on the placement runner: templates/placement_check.gd on every level under levels (folders or .tscn files), each property in limits set from the GDScript constant it names ("res://player.gd:REACH") or to the value given. Rules, groups and exemptions: game-data section 6.

{ "levels": ["res://levels"], "limits": { "reach": "res://player.gd:REACH", "zipline_max_angle": "res://zipline.gd:MAX_ANGLE_DEGREES" } }

playtest.json (optional, in the Godot project folder) tells playtest how to draw each level's heat maps. Without it, a level's grid covers where its events are, on the xy plane (2D) unless an event has a z, then xz (3D seen from above, Godot's y up), with 64 px or 2 m cells, 16 pixels a cell (fewer for a grid over 64 cells a side):

{ "cell": 64,
  "levels": {
    "hub":  {},
    "cave": { "plane": "xz", "cell": 2, "bounds": [0, 0, 20, 10], "image": "playtests/cave.png" } } }

bounds is the world rectangle [minU, minV, maxU, maxV] the grid covers (u is x; v is y, or z on xz); events outside it are counted as outside. image is a screenshot of exactly that rectangle (a PNG, 8-bit, not interlaced, such as shot saves from a top-down or 2D camera); the maps are then drawn over it at its size, at 65% opacity, and empty cells keep the picture. Listing levels also makes playtest name levels it did not expect (unknownLevels). The events and their fields are ci/godot/templates/playtest-events.json; tests/playtest-fixture is a minimal example.