gda is an agent-first CLI for the Godot engine: every operation is one command
with structured JSON output, so you build and inspect a game without opening the
editor. Two kinds of operation: headless (a one-shot godot --headless per
call — scenes, scripts, exports) and live (against a running game via the
gda-daemon).
Grammar
gda <group> <command> [options] --json
Exactly one JSON result is printed to stdout; all engine noise (warnings,
progress, the engine banner) goes to stderr. Read stdout, ignore stderr unless
debugging. info / version / help / schema / skill are top-level meta
commands (no group).
Setup
- Engine — set
GDA_GODOT to your Godot binary (or pass --godot PATH).
- Project — resolved by
--project DIR → $GDA_PROJECT → the current
directory; the directory must contain project.godot. Resolving a project runs
that project's autoloads at engine startup.
- Projectless — file-path-only operations run with no project; they resolve
filesystem paths but not
res://. Meta commands never inherit a project
($GDA_PROJECT/cwd is ignored, so an invalid one cannot break them); gda info still accepts an explicit --project, validated as anywhere else, while
the other meta commands take none.
- Provenance —
gda --version --json reports which gda is running: its
version, the executable and interpreter paths, package_path (the directory the
running code was imported from), install_kind (wheel, editable, or
unknown when the install metadata cannot be read — gda will not guess), and,
for an editable install, the source checkout with its Git revision and
dirty flag. No Godot is spawned, so it also works where an engine spawn
fails. Run it first in a long session and keep the output: an editable install
can change revision under you mid-run, so this is what ties your results to the
code that produced them. Bare gda --version stays one human-readable line.
- User data (headless runs) — each headless run's engine log goes to a private
temporary file, so a read-only Godot application-data directory is not fatal and
concurrent runs never contend. Run ANY script that writes
user:// — a save, a
settings file, a test suite's fixtures — under a root of your own:
gda --user-data-root DIR script run res://tests/all.gd --json. The option is
GLOBAL, so it comes before the group, and $GDA_USER_DATA_ROOT is the same
switch; both place the log and user:// under DIR, and a target gda cannot
create is refused as user_data_unwritable before the engine starts. A
successful script run reports what it actually got: engine_data_path (the
directory the engine resolved user:// beneath) always, plus user_data_root
and log_file when you gave a root — the one case in which the log survives the
run, so read it there rather than hunting for it. Check those before you read a
failed save as a game bug — and check them on a failure too: script_failed,
launch_timeout and script_aborted carry the same three keys under evidence,
omitted rather than null where a path is not a fact. Those three codes and no
others: every other script run failure carries no placement, engine_crashed
and a script that never ran included. Two limits: Godot reads the export templates from that same
directory, so a release/debug export run under it reports none installed
unless you put templates there — the export_templates_missing failure then names
both directories and carries them as evidence.templates_root_checked /
evidence.templates_root_host, and that second key is how you tell "hidden by the
redirect" (drop it, or use --mode pack, which needs no templates) from "not
installed anywhere" (install them); and Engine sessions are unaffected either way —
the daemon owns their log.
Structured output & errors
Always pass --json. It selects the channel for BOTH outcomes: without it a
failure prints as human-readable lines instead of the envelope. A success is the
operation's result object. A failure is
{"error": {"category": "...", "code": "...", "message": "..."}}
Branch on the stable category/code and the exit code, never on prose:
| Exit |
Meaning |
0 |
success |
2 |
gda could not resolve what you asked for: unknown_command, unknown_option |
127 |
environment unusable: binary_not_found, user_data_unwritable, live_unsupported_platform, live_windowed_unavailable, live_windowed_permission_denied, harness_install_permission_denied |
124 |
engine timed out: launch_timeout — gda ended a run that had not returned (read it as below) |
3 |
engine version too old |
4 |
operation-reported failure |
5 |
could not parse the engine's output |
6 |
live operation failed (e.g. daemon_not_running) |
Reading a 124. The environment category describes how the run ENDED, not the
host. Read the captured partial output in diagnostics for how far the run got, then
raise the ceiling with --timeout where the command exposes one; where it does not the
ceiling is gda's own, so reduce the work or give the machine more headroom. Suspect the
binary or the machine only when the capture shows the engine never started. Any engine
error inside that capture is advisory — the verdict is the timeout.
A command or option gda does not recognize is reported the same way — an
unknown_command / unknown_option envelope at exit 2 — and when gda recognizes
the mistake the envelope carries a hint naming the invocation to run instead
({"error": {"code": "unknown_command", "hint": "gda scene get", …}}). Re-issue the
hint; when there is none, gda schema lists every command and
gda help <command> describes one.
A failure that computed evidence also carries it as DATA, under the envelope's
optional evidence key — omitted, never null, on the failures that computed none.
Read it instead of parsing the message: elapsed_seconds / termination_phase
(launched = the engine wrote nothing at all, so suspect the binary or the host;
output_seen = it was alive and did not finish, so raise the ceiling;
aborted_on_error) on a run gda ended, plus timeout_seconds — the reached
ceiling — on the timeout verdict only (an abort stopped short of its ceiling, so
it omits the field; your own --timeout stays in the message), exit_status
(the CHILD's, not gda's exit code) on script run --strict's script_failed,
and script_errors on the script run failures that parse the run's stderr —
the never-ran verdicts, --strict's script_failed, and both runs gda ended. Under a launch_timeout those errors
stay ADVISORY — the verdict is the timeout, so branch on code and read
evidence for the cause.
Each script_errors entry is a {kind, message, path, line} record — the same
four keys, always present, that a successful script run reports as
diagnostics; path and line are null where the engine named neither, which
is the normal case for a load error. The list itself has three states worth
telling apart: absent means this failure's channel does not parse stderr at all,
so read diagnostics; [] means it parsed and recognized nothing, which is
itself a finding; a non-empty list is what it recognized.
Some commands carry a verdict inside a successful result. For
gda script validate --json, read the result's valid field: it is the AGGREGATE
verdict over every script the call validated, false as soon as one of them fails.
Each script's own verdict is an entry under scripts (path, valid,
error_string, diagnostics). A script that does not compile exits 0 with no
top-level error, so do not treat exit 0 or the absence of an Error envelope as a
pass for this command. Validate the whole set you changed in ONE call —
gda script validate a.gd b.gd c.gd --json uses a single engine launch, and
--all validates every script in the project. Check the result's project_root
before you act on a valid=false: it names the project the scripts were compiled
against, and a verdict full of missing-res:// errors (plus the type errors derived
from them) usually means the wrong project, not a broken script — null means no
project was resolved at all. Pass --project for the project that owns the files
and re-read the verdict. gda refuses up front, with target_outside_project, when
the resolved project plainly does not own a path — outside its tree, or claimed by
a nested project.godot, and with no project resolved too (script run and
resource import refuse the same way). It names the owner it found
(evidence.owning_project) but never switches to it, and the message states the
whole re-issue: --project <owner> AND the target respelled relative to that
owner — a relative path anchors at the project, so your original spelling would
not be found under the new one. The same three commands refuse a path whose CASE
does not match the stored file, with path_case_mismatch: such a path opens on a
case-insensitive filesystem (the common default on macOS and Windows) and fails on a
case-sensitive one (the common default on Linux and most export hosts), so re-issue with the evidence.stored_path the refusal
names. --all needs no such check: every project-wide listing
(script list, scene list, script validate --all, project analysis) walks the
res:// tree with the engine's own skip rule, declining the engine cache, a directory
holding a nested project.godot, and one holding a .gdignore (hidden and dot-prefixed
directories gda still enumerates, unlike the engine). So --all reports only
what the resolved project owns; to work on a nested project's files, run with
--project <that project>.
Discovery
gda --help — every group.
gda <group> --help — a group's commands.
gda help <group> <command> — the same help from the command form; with --json it
comes back as {command, text}.
gda <group> <command> --schema — one command's input/output/error JSON Schema
(no Godot spawned), plus argv: how each parameter is written on a command line
(kind positional or option, its position or --option spelling, whether it is
required, a valueless flag, multiple — repeat it per value — or a
json_value — one token carrying the value's JSON). Build the command line from
argv; input_property links each binding to the input property it fills.
gda schema — the whole surface as one JSON manifest, argv included.
gda version --json — which gda is installed and where from; gda info — the
engine's version.
--json placement. Every parser takes it: gda --json <group> <command>,
gda <group> --json <command> and gda <group> <command> --json mean the same thing — a --json
written before the command applies to the command it invokes — so any spelling works, as do several
at once. gda schema --json is accepted too, and idempotent: the manifest is already JSON. Two
limits: the --help FLAG always renders TEXT (gda --json --help returns the same help, never
JSON — use gda help <command> --json for a structured payload), and the flag is not a command, so
gda <group> --json and a bare gda --json with no command are both the usage error
Missing command. (exit 2).
Headless commands (Godot 4.4+, all platforms)
| Group |
Commands |
scene |
create, get, list, get-exports, delete, validate, preflight (.tscn files; validate is the STATIC verdict get does not give — a scene loads fine with its script and texture missing, so check dependencies resolve and attached scripts compile before trusting it; invalid exits 0 with valid: false plus one problem per problem file — COMPOSED over the sub-scenes it references — normally the ones it instances, but a reference is followed when its path ends in .tscn/.scn OR its [ext_resource] line declares type="PackedScene", a union because Godot loads every [ext_resource] whatever it is called while ResourceSaver will put a PackedScene in a plain .res (one saved under a non-scene extension AND declared as something else stays outside) — so a parent whose child is broken is invalid too and every problem carries scene, the file it was found in, in the same canonical spelling as the result's path (read its path/nodes against THAT file); three kinds of edge are reported instead of followed — cyclic_instance for a cycle, unreadable_sub_scene for a scene that loads but carries no [gd_scene] text to walk (a binary .scn, or a PackedScene in a .res), and instance_depth_exceeded for a scene no route reaches within 16 levels of sub-scenes; the last two say that subtree is UNCHECKED, not sound (validate it directly, or re-save it as .tscn, for its own verdict), and the depth bound is on the shortest route so the verdict does not depend on declaration order, and it covers gda's walk only, not the engine's own load of the chain; staged: unresolved dependencies suppress the script compile/binding pass, so repair them and rerun for the rest. preflight is the DYNAMIC one: it boots the scene headless, waits for _ready, and reports status (ready/not_ready/timeout) plus the script errors it recognized — read started; leaked objects or resources at exit are recognized too (a shutdown_leak diagnostic, printed by the engine after the scene ran, about the whole process) — reported in diagnostics but NOT gating started, which answers how the boot went; a timeout verdict also carries elapsed_seconds and timeout_seconds (the --timeout it reached), the same evidence pair the launch_timeout envelope gives elsewhere — it names the consumed ceiling, not the cause: a stuck scene and a healthy one whose --frames window outruns the same ceiling read alike, so pick a larger --timeout or fewer --frames from what the scene should do, and rerun — both keys are on that verdict only and omitted from every other. Passing validate is not "it works": check both) |
node |
add, get, list, set, remove, duplicate, move, connect-signal, disconnect-signal (nodes within a scene) |
script |
create, get, list, set, delete, attach, validate, run (.gd files; validate takes SEVERAL paths at once — one engine launch for the whole batch, one aggregate valid plus a per-file entry under scripts — or --all for every script in the project; run executes a project script one-shot (address it project-relative or as res:// — the two portable forms, which script validate takes too; run alone refuses absolute paths) and passes its exit_status/stdout/stderr through — stdout above 64 KiB is truncated to its leading bytes with the COMPLETE stream spilled to the file named in stdout_file (stdout_bytes/stdout_truncated disclose it; a spill gda cannot write is the typed stdout_spill_failed, never an unbounded result), and a non-zero quit() is still success, so read exit_status, or pass --strict to get a script_failed failure (exit 4) whose diagnostics carries the script's own stdout and stderr — --strict fails a run for EITHER the non-zero status or a shutdown_leak diagnostic, the engine's exit-time report that the PROCESS left objects or resources alive (an autoload's leak counts; its RID leak reports are not recognized), which a status-only gate never sees because a suite can pass, quit(0) and still leak; a script that never ran — missing, or a failed parse/compile — always fails; --timeout <s> sets the ceiling (default 120) and a run that reaches it fails with launch_timeout carrying the captured partial output, the elapsed seconds and a termination phase; add --completion-marker <line> naming a line your script prints when its work is done — a caller-declared liveness contract, not a death detector: gda ends the run once it observes a recognized error attributable to the entry script, no marker line yet, and then silence on both streams — script_aborted (exit 4) with the captured error, in seconds rather than at the ceiling; declaring the marker asserts the script keeps printing until that line, so have it print progress during quiet stretches longer than ~3s, or omit the marker; a script that writes user:// belongs under gda --user-data-root DIR script run … (see Setup) — the result then names user_data_root and log_file beside the always-present engine_data_path) |
project |
info, get, set, list, add-autoload, remove-autoload, add-input-action, remove-input-action, find-references, dependencies, find-unused-resources, statistics (a WRITE saves through the engine, which reserializes the whole project.godot: it deletes explicit lines whose value equals the engine default, adds or rewrites application/config/features, and reorders the sections. gda restores the deleted lines and reports added_settings, rewritten_settings, restored_settings and sections_reordered on every write result — read them when the file is tracked, and expect the section order to be the engine's) |
resource |
create, get, set, delete, uid, import (.tres files and project assets; import ensures importable assets — PNGs and other files the engine imports — are in the project cache: clean-worktree loading; a script needs no import and reports not_importable; an invalid or failed asset says why in reason — branch on that rather than reading prose: sidecar_marked_invalid means the ENGINE failed the last import (repair the source), sidecar_unparsable/receipt_unsupported mean gda cannot read the artifact (delete the .import sidecar and re-import; detail names the offending line or the receipt), and dest_missing_after_pass means the pass ran and produced nothing. engine_output is the separate half and does not follow reason: the pass's stderr lines naming that asset whenever THIS request ran a pass (empty when none ran; at most 20, engine_output_truncated when more matched) — the engine names an asset it then skips, so an invalid one can carry lines too. They are the lines NAMING the asset, which for some importers is only the verdict and not the root cause, so keep the whole engine stream with gda --user-data-root DIR resource import … and read DIR/logs/godot.log. An invalid reason survives into the settled failed, so a real run still names the pre-pass check. A real run also reports created — every file the pass created, classified cache_owned/source_adjacent like export run's — and skipped, the entries its inventory could not read, so created is complete when skipped is 0) |
export |
list, get, run, smoke (export a preset by name; --mode release/debug/pack; get also reports templates_root, the export-templates directory it checked, and templates_root_host, set when a --user-data-root redirect hid templates installed on the host; run also reports project_tree_mutations — the export runs the editor import pass, so it names every file it created anywhere under the project, each classified cache_owned/source_adjacent against the cache_root it names, plus the pre-existing files OUTSIDE that root whose CONTENT it rewrote (only a file whose size or timestamp moved is compared; rewrites inside the cache root are not reported at all, so an empty modified says nothing about the cache) — a cold cache leaves thousands of entries there, skipped counts what could not be read, and a FAILED export reports none of it. smoke RUNS a built artifact headless and hands back its completed process — feed it run's output_path: a macOS .app resolves to the Contents/MacOS file its Contents/Info.plist names in CFBundleExecutable, any other host-runnable file runs as given, an absent path is export_artifact_not_found and anything else is export_artifact_not_runnable; support is bounded to those two shapes with end-to-end evidence on macOS only and no Linux or Windows commitment until probed. It takes no --project (a relative artifact path resolves against your cwd), --arg values reach the game in order after Godot's -- where it reads them with OS.get_cmdline_user_args(), --quit-after N asks the engine to end its main loop normally after N process frames so engine cleanup and its exit-time diagnostics run — it asserts NO project completion — and --timeout (the same default ceiling as script run) is only the external hard bound, so a run it ends cannot prove shutdown-only diagnostics are absent. The result carries exit_status (non-zero is DATA — read it, do not assume success means zero), stdout under the same cap script run uses with stdout_bytes/stdout_truncated/stdout_file, stderr, diagnostics and both paths (artifact, executable); --strict turns a non-zero status OR a shutdown_leak diagnostic into smoke_failed (exit 4) carrying evidence.exit_status and evidence.script_errors. The game runs under a private user:// gda creates and removes, so pass the global --user-data-root DIR to keep what it writes) |
shader |
create, get, set (.gdshader files) |
theme |
create (a loadable .tres Theme) |
Every headless reply carries its floats at full binary64 precision, so a value read back through node get, scene get-exports, project get/project list, resource get, or the echo of a set is the exact number the project holds — 1e-300 reads back as 1e-300, not 0.0; the one residual belongs to the ENGINE's writer — a negative zero reads back as 0.0 (#771). The --value string you send IN is coerced by the engine's own parser, and gda refuses what that parser would destroy: a literal it reads as 0.0 when you did not write zero, or as NaN at all, fails with uncoercible_value (exit 4, target untouched) instead of writing a number you never sent — 2.2250738585072014e-308 and 5e-324, and also 0.000000000000000001, whose 18 leading zeros fill the parser's whole mantissa window (1e-18 is exact). The rule follows the LITERAL, not the property type, so a number inside a Dictionary or Array --value is refused the same way and names the offending literal: --value '{"a": 1e-320}' fails, --value '{"a": 1e-18}' stores exactly. Only real JSON numbers are read — a numeric-looking STRING value ({"a": "1e-320"}) and a numeric-looking KEY ({"1e-320": 1.0}) are stored unchanged (#805). So spell a small or many-digit value in SCIENTIFIC notation carrying only the digits it needs: that also avoids the low-digit loss the parser inflicts on a full-precision literal between 1e-4 and 1e-2, which is disclosed rather than refused (#772). Read the set echo when the exact bits matter.
Live operations (via the daemon; Godot 4.6+, macOS/Linux)
Prerequisites: the project defines application/run/main_scene, or you pass --scene.
For settings it can determine from the project files, daemon start refuses an empty main
scene with live_main_scene_undefined, or a uid:// main scene without its UID cache with
live_main_scene_unresolved — run gda resource import <any existing res:// asset> once,
or open the project in the editor. Main-scene feature overrides, default/custom settings
overlays (override.cfg, or a nonempty application/config/project_settings_override path,
including feature overrides of that path), and escaped application keys defer to the engine; feature overrides
or unrecognized boolean values for application/config/use_hidden_project_data_directory defer only
the UID-cache check. Deferred cases can still show a native alert on macOS even headless
until the readiness deadline tears down the session. An explicit valid --scene res://<scene>.tscn
avoids main-scene resolution. Run gda daemon start first (optionally --scene <res://...> to boot a
specific scene instead of the project's main scene); the engine session launches lazily on
the first operation that requires one. To establish the session deterministically, run
gda daemon wait-ready (--timeout budgets daemon waits and new-work decisions;
a synchronous launch call can delay expiry observation; idempotent while the session is
alive) — success means live reads serve. This matters for the read-only diagnostics: diag errors /
logger tail never launch a session themselves, so right after daemon start they report
engine_session_not_running by design — expected, not a defect; run wait-ready first.
Serving is NOT the same as a cleanly started scene: a script that fails to compile leaves
its node script-less and the session serves anyway, leaving the tree without whatever the
script would have built, and often a blank frame. So wait-ready also reports clean_start
and the startup_diagnostics it recognized in the session log UP TO THE HANDSHAKE — the same
records script run and scene preflight carry, read from the bytes the log held at the
instant the harness handshake completed, so they cover engine startup, the autoloads and the
scene's own scripts; a record emitted during the handshake's own frames lands on whichever
side of that instant it was written. Read clean_start before you treat a screenshot or a
runtime read as evidence about the scene. Both keys are null when gda could not read that
prefix (diag errors then says live_log_unavailable) — never a clean start it did not see.
daemon status repeats that verdict for the serving session without relaunching it, and
diag errors reads the whole log, including everything printed after that instant.
A live_timeout discards the session (its late reply can no longer be attributed), so the
next operation starts a fresh game and the runtime state you had set is gone. Most often
it means the game stopped returning to its main loop — look for a blocking loop or wait in
game code. But the 30s bound is a wall clock while a multi-frame window (--frames,
--await-frames) waits that many ENGINE frames, so on a slow-ticking game a window op
outruns it with the loop running normally: ask for fewer frames when gda logger tail
shows the log kept advancing. A paused SceneTree is NOT a cause; see "paused vs
suspended" below.
screen capture needs a windowed session
(gda daemon start --windowed).
A windowed session needs the host's real desktop session — an on-console GUI login on
macOS, $DISPLAY / $WAYLAND_DISPLAY on Linux. Over SSH, on a headless CI box, or from
a sandbox that blocks the window server, daemon start --windowed refuses before
spawning Godot. Branch on the code, not the sentence:
live_windowed_unavailable — nothing refused the probe and no session is reachable, so
this host cannot show a window. Skip the rendered check; headless live ops (game,
perf, input, diag, logger) still work.
live_windowed_permission_denied — this process is not allowed to even look up the
window server (e.g. a sandbox). It does NOT mean the host has one: macOS refuses the
lookup before resolving it, so a broadly-confined process is refused either way. Re-run
outside the restriction to find out; do not record the machine as display-less on this
code alone.
A refusal from gda daemon start --windowed carries error.probe {name, platform}
naming the OS call that decided — including when the refusal is relayed from an
already-running daemon's lazy Engine-session launch; only the outer
{stdout, stderr, exit_code} transport shape is probe-less.
| Group |
Commands |
daemon |
start, wait-ready, stop, status, install, uninstall (lifecycle; start installs the in-game harness itself, so install is only for doing that step deliberately — e.g. to review or commit the project.godot change — and uninstall reverses it; wait-ready establishes the lazily-launched engine session, with --timeout shared by its waits and new-work decisions, so a first diag errors serves instead of reporting engine_session_not_running; status reports the last successfully established engine session's session_id — the identity a screen capture receipt correlates with, minted anew per established session and retained across a failed replacement launch — and repeats that session's startup verdict, clean_start plus startup_diagnostics, so a caller reads it without relaunching the game) |
game |
tree, find, get, rect, set, call (the running game's runtime scene graph; tree --root <runtime path> --max-depth N bounds the read, and find locates nodes by SELECTOR rather than by path — see "Find a node before you address it" below; get --texture-digest opts a read into content digests for path-less Texture2D values. rect reads a Control's layout OUTPUT, which no storage property carries: the rendered viewport rect (position/size), the same rectangle in the PARENT's space (local_position/local_size), and the two minimum sizes — minimum_size is the class's intrinsic minimum and EXCLUDES the authored custom_minimum_size, while combined_minimum_size is the per-axis maximum of the two, the size a parent Container honors. get cannot serve position, size, global_position or global_rect on a Control — none of them is a storage property — so it refuses with live_unknown_property and the message names game rect and the layout INPUTS, which depend on the parent: a free Control carries offset_left/offset_top/offset_right/offset_bottom and anchor_left/anchor_top/anchor_right/anchor_bottom, while a direct child of a Container carries NONE of those — the engine drops them from its storage set — and carries custom_minimum_size, size_flags_horizontal and size_flags_vertical instead, the rest being the parent Container's own layout. call --method NAME [--args JSON] invokes a method named by the GDA_CALLABLE declaration resolved from the node's attached script along its base chain — use it for a debug/state contract exposed as a method rather than a property. gda calls nothing undeclared, so an undeclared-but-present method is live_method_not_allowlisted and its message names the declared set; a missing one is live_unknown_method, and arguments the declared parameters cannot take (wrong count, a type the engine would not convert, a typed Array[int] parameter) are live_invalid_call_args, refused before the call. The live parser materializes every JSON number as float. NaN/Infinity are refused; RFC JSON excludes them, although some in-memory schema validators accept them as numbers. Finite floats do not inherit the integer bound, but a float whose wire literal Godot's parser reads as 0.0 is refused too — DBL_MIN, any subnormal, and many-digit values such as 1.2345678901234567e-300, none of which any decimal spelling can deliver; a float it does read arrives changed in its low-order bits — 1 ULP at ordinary magnitudes, and tens of doubles for a full-precision literal between 1e-4 and 1e-2, where the parser truncates past 18 mantissa digits (#752). JSON integer values beyond ±(2^53−1) are refused CLI-side because the wire can change them. Standard JSON Schema cannot distinguish an exponent-form float from the equal integer, so the params model enforces the integer-token limit at execution. LIMIT: gda CANNOT verify a declared method has no side effects — the constant records the project's own read-only assertion, and what gda guarantees is only that no undeclared method is called. GDScript forbids redeclaring a base class's constant, so an opted-in inheritance chain has at most one declaration owner; a base owner covers its subclasses and need not define every method it names) |
diag |
errors (structured runtime errors with callstacks; survive a crash) |
logger |
tail (the running game's structured log stream; --raw for verbatim lines, --level <min> to filter by severity, --limit N) |
perf |
monitors, monitor (counters: a one-frame snapshot, or with --frames a bounded window with statistics and optional --budget verdicts / a per-node timeline. A window ALLOCATES inside the game you are measuring — the harness holds the whole window — so every window result reports collector_bytes: the LOGICAL size of what the sampler kept, 8 bytes per stored value over one column per sampled monitor plus one of timestamps. It is a LOWER bound on the real in-game cost — a packed column over-allocates as it grows, and the shared window base's per-frame accumulator is not counted — and it cannot attribute a static_memory rise by itself — before calling anything a game leak, take a matched baseline window (the same --frames and --monitor set on a scene that allocates nothing, on the same host) and compare the two rises. monitors --frames N --summary returns the same statistics, budget verdicts and collector_bytes with the per-frame rows left out (samples: null, samples_omitted: true), so a long window's result does not grow with the frame count; the window is still sampled in full) |
input |
key, mouse-click, mouse-move, action, tap, sequence |
screen |
capture, frames (viewport PNGs; needs --windowed. frames --summary returns the compact aggregate — directory, filename pattern, frame size, total bytes — instead of the per-frame list, so a large capture's envelope stays small; every frame is still written. capture --await-node/--await-property/--await-value [--await-frames] [--await-events] is the predicate-gated form: it fires on the first frame boundary where the property equals the value, optionally injecting input inside the same window — use it for short transients instead of a separate input + capture. With --settle-frames 0, the default, the observed property and the pixels belong to the same COMPLETED frame; a value overwritten before its frame completes is never observable, an injected event's effect is observable from the next boundary, and a declared event that fails makes the capture that typed failure. Every capture result carries a receipt binding the image to its capture event — session_id correlating with daemon status, the LAUNCHED scene's path and header uid (uid null for gda-authored scenes), two frame counters, the gated capture's observed echo, and the written file's SHA-256 — so a plain capture needs no local hashing, and a gated capture's complete evidence is the receipt plus the sibling predicate report. The two counters differ: engine_frame is the process frame the READ was taken at, render_frame the drawn frame the pixels ARE, and they diverge whenever the engine skipped a draw — two captures with the same render_frame are the same drawn frame, so identical pixels there are the engine's doing, not the game's. --settle-frames N on either command lets the game run N more process frames before the read, for a visual that settles after a state change; the default is 0 because a capture has nothing pending to observe, unlike input tap's release. With --await-* the settle runs after the predicate holds, so the report keeps its own frame and the receipt's engine_frame is that frame plus N; on frames it runs once, before the first frame) |
Every live reply carries its floats at full binary64 precision, so a value read back through game get, game call, perf, or a monitored timeline equals the value the running game holds; the one residual belongs to the ENGINE's writer — a negative zero it wrote reads back as 0.0 (#752). A number gda produces CLI-side meets no Godot writer and so does not carry that residual: perf monitors --frames reports its mean and every budget-verdict number (the bounds copied from your own budget file) exactly, negative zero included. Each live result field says which of the two it is.
In the other direction the wire is narrower, and EVERY live command RELAYED to the game applies the same rule — not just game call; the three the daemon answers itself (diag errors, logger tail, daemon wait-ready) send no number to the engine and are outside it. A number gda cannot send unchanged is refused before the request leaves: a float whose wire literal Godot's parser reads as 0.0 (DBL_MIN, any subnormal, many-digit values such as 1.2345678901234567e-300) and a JSON integer beyond ±(2^53−1). It is a usage error on the argv path and invalid_params on --params-json, decided without a running daemon, and it reaches nested values — a sequence event's x, a game call argument inside a dictionary. A float the parser DOES read still arrives changed in its low-order bits: 1 ULP at ordinary magnitudes, tens of doubles for a full-precision literal between 1e-4 and 1e-2. That residual is disclosed, not refused (#752). game set --value is OUTSIDE that rule — the value travels as a STRING, so the wire never sees a number — and has a refusal of its own, decided in the session by the harness: the string is coerced with the engine's own parser and a literal it reads as 0.0 when you did not write zero, or as NaN at all, fails with live_uncoercible_value (exit 6, the running game untouched), the same rule headless --value follows (#772) — containers included, so a destroyed number inside a Dictionary or Array value is refused here too (#805).
gda input injects through two routes, and every result names the one it used
(injection_route). By default input action, input tap --action and a sequence
action event drive Input.action_press/action_release: the action_state route, a
change to the POLLED action state that reaches no _input, _gui_input or
_unhandled_input handler. input key, the mouse commands and the key /
mouse_* sequence kinds push an InputEvent through the root viewport: the
viewport_event route. Rule of thumb — drive event-driven UI (a Control, a
modal, a scrollable) with a key or mouse event, and use an action where the game
polls Input.is_action_*. A successful action injection is not evidence that the
event path works.
--as-event is the explicit opt-in to the OTHER door for an action: gda pushes an
InputEventAction through the root viewport, so handlers matching the action
receive it and the polled state stays untouched. It rides input action,
input tap --action, and a sequence action event ("as_event": true), and the
opted-in result reports viewport_event. The default is unchanged. The matrix, for
one action and the key it is mapped to:
| injection |
Input.is_action_pressed |
_input / _unhandled_input |
_gui_input |
input action (state route) |
yes |
no |
no |
input action --as-event |
no |
yes |
yes (focused Control) |
input key of the mapped key |
no |
yes |
yes (focused Control) |
Reach for it when a Control, a modal or another event-driven handler must react to
an ACTION rather than to the key it is bound to — otherwise a key or mouse event is
the plainer tool.
The matrix describes eligible delivery under Godot's normal propagation and
consumption rules, not proof that every handler ran or a UI action succeeded.
Use the current harness bundled with gda. After updating gda, stop/start an existing
daemon session before using live commands; syncing the installed file does not
reload code in the running game. Mixed-version sessions are not supported.
For a UI activation, use the gesture commands, not a lone event. Godot activates a
Button on the RELEASE, so a bare press never emits pressed; and a focused UI
does not advance when the press and the release land on the same process frame.
gda input mouse-click injects the whole gesture — the initial move, the press,
and the release, one per process frame — and gda input tap --key K /
`gda input tap --action N
…(truncated)
1---2name: gda3description: Drive the Godot game engine from the command line with `gda`, an agent-first CLI with structured JSON output. Use when building, editing, or inspecting a Godot project — create/edit scenes, nodes, GDScript, resources, shaders, themes; run static analysis; export builds (all headless, no editor) — or to control a running game live (runtime scene tree, input simulation, screenshots, performance, runtime logs/errors) via the gda daemon. Use when the user mentions gda, Godot automation, headless Godot, or asks an agent to make/modify a Godot game. Always pass `--json` and read the single result object; run `gda --help` or `gda schema` to discover the full command surface.4---56`gda` is an agent-first CLI for the Godot engine: every operation is one command7with structured JSON output, so you build and inspect a game without opening the8editor. Two kinds of operation: **headless** (a one-shot `godot --headless` per9call — scenes, scripts, exports) and **live** (against a running game via the10`gda-daemon`).1112## Grammar1314```15gda <group> <command> [options] --json16```1718Exactly one JSON result is printed to **stdout**; all engine noise (warnings,19progress, the engine banner) goes to **stderr**. Read stdout, ignore stderr unless20debugging. `info` / `version` / `help` / `schema` / `skill` are top-level meta21commands (no group).2223## Setup2425- **Engine** — set `GDA_GODOT` to your Godot binary (or pass `--godot PATH`).26- **Project** — resolved by `--project DIR` → `$GDA_PROJECT` → the current27 directory; the directory must contain `project.godot`. Resolving a project runs28 that project's autoloads at engine startup.29- **Projectless** — file-path-only operations run with no project; they resolve30 filesystem paths but not `res://`. Meta commands never *inherit* a project31 (`$GDA_PROJECT`/cwd is ignored, so an invalid one cannot break them); `gda32 info` still accepts an explicit `--project`, validated as anywhere else, while33 the other meta commands take none.34- **Provenance** — `gda --version --json` reports which `gda` is running: its35 version, the executable and interpreter paths, `package_path` (the directory the36 running code was imported from), `install_kind` (`wheel`, `editable`, or37 `unknown` when the install metadata cannot be read — gda will not guess), and,38 for an editable install, the `source` checkout with its Git `revision` and39 `dirty` flag. No Godot is spawned, so it also works where an engine spawn40 fails. Run it first in a long session and keep the output: an editable install41 can change revision under you mid-run, so this is what ties your results to the42 code that produced them. Bare `gda --version` stays one human-readable line.43- **User data (headless runs)** — each headless run's engine log goes to a private44 temporary file, so a read-only Godot application-data directory is not fatal and45 concurrent runs never contend. Run ANY script that writes `user://` — a save, a46 settings file, a test suite's fixtures — under a root of your own:47 `gda --user-data-root DIR script run res://tests/all.gd --json`. The option is48 GLOBAL, so it comes before the group, and `$GDA_USER_DATA_ROOT` is the same49 switch; both place the log and `user://` under `DIR`, and a target `gda` cannot50 create is refused as `user_data_unwritable` before the engine starts. A51 successful `script run` reports what it actually got: `engine_data_path` (the52 directory the engine resolved `user://` beneath) always, plus `user_data_root`53 and `log_file` when you gave a root — the one case in which the log survives the54 run, so read it there rather than hunting for it. Check those before you read a55 failed save as a game bug — and check them on a failure too: `script_failed`,56 `launch_timeout` and `script_aborted` carry the same three keys under `evidence`,57 omitted rather than null where a path is not a fact. Those three codes and no58 others: every other `script run` failure carries no placement, `engine_crashed`59 and a script that never ran included. Two limits: Godot reads the **export templates** from that same60 directory, so a `release`/`debug` `export run` under it reports none installed61 unless you put templates there — the `export_templates_missing` failure then names62 both directories and carries them as `evidence.templates_root_checked` /63 `evidence.templates_root_host`, and that second key is how you tell "hidden by the64 redirect" (drop it, or use `--mode pack`, which needs no templates) from "not65 installed anywhere" (install them); and Engine sessions are unaffected either way —66 the daemon owns their log.6768## Structured output & errors6970Always pass `--json`. It selects the channel for BOTH outcomes: without it a71failure prints as human-readable lines instead of the envelope. A success is the72operation's result object. A failure is7374```json75{"error": {"category": "...", "code": "...", "message": "..."}}76```7778Branch on the stable `category`/`code` and the **exit code**, never on prose:7980| Exit | Meaning |81| ---- | ------- |82| `0` | success |83| `2` | gda could not resolve what you asked for: `unknown_command`, `unknown_option` |84| `127` | environment unusable: `binary_not_found`, `user_data_unwritable`, `live_unsupported_platform`, `live_windowed_unavailable`, `live_windowed_permission_denied`, `harness_install_permission_denied` |85| `124` | engine timed out: `launch_timeout` — gda ended a run that had not returned (read it as below) |86| `3` | engine version too old |87| `4` | operation-reported failure |88| `5` | could not parse the engine's output |89| `6` | live operation failed (e.g. `daemon_not_running`) |9091**Reading a `124`.** The `environment` category describes how the run ENDED, not the92host. Read the captured partial output in `diagnostics` for how far the run got, then93raise the ceiling with `--timeout` where the command exposes one; where it does not the94ceiling is gda's own, so reduce the work or give the machine more headroom. Suspect the95binary or the machine only when the capture shows the engine never started. Any engine96error inside that capture is advisory — the verdict is the timeout.9798A command or option gda does not recognize is reported the same way — an99`unknown_command` / `unknown_option` envelope at exit `2` — and when gda recognizes100the mistake the envelope carries a `hint` naming the invocation to run instead101(`{"error": {"code": "unknown_command", "hint": "gda scene get", …}}`). Re-issue the102`hint`; when there is none, `gda schema` lists every command and103`gda help <command>` describes one.104105A failure that computed evidence also carries it as DATA, under the envelope's106optional `evidence` key — omitted, never null, on the failures that computed none.107Read it instead of parsing the message: `elapsed_seconds` / `termination_phase`108(`launched` = the engine wrote nothing at all, so suspect the binary or the host;109`output_seen` = it was alive and did not finish, so raise the ceiling;110`aborted_on_error`) on a run gda ended, plus `timeout_seconds` — the reached111ceiling — on the timeout verdict only (an abort stopped short of its ceiling, so112it omits the field; your own `--timeout` stays in the message), `exit_status`113(the CHILD's, not gda's exit code) on `script run --strict`'s `script_failed`,114and `script_errors` on the `script run` failures that parse the run's stderr —115the never-ran verdicts, `--strict`'s `script_failed`, and both runs gda ended. Under a `launch_timeout` those errors116stay ADVISORY — the verdict is the timeout, so branch on `code` and read117`evidence` for the cause.118119Each `script_errors` entry is a `{kind, message, path, line}` record — the same120four keys, always present, that a successful `script run` reports as121`diagnostics`; `path` and `line` are `null` where the engine named neither, which122is the normal case for a load error. The list itself has three states worth123telling apart: absent means this failure's channel does not parse stderr at all,124so read `diagnostics`; `[]` means it parsed and recognized nothing, which is125itself a finding; a non-empty list is what it recognized.126127Some commands carry a verdict inside a successful result. For128`gda script validate --json`, read the result's `valid` field: it is the AGGREGATE129verdict over every script the call validated, `false` as soon as one of them fails.130Each script's own verdict is an entry under `scripts` (`path`, `valid`,131`error_string`, `diagnostics`). A script that does not compile exits `0` with no132top-level `error`, so do not treat exit `0` or the absence of an Error envelope as a133pass for this command. Validate the whole set you changed in ONE call —134`gda script validate a.gd b.gd c.gd --json` uses a single engine launch, and135`--all` validates every script in the project. Check the result's `project_root`136before you act on a `valid=false`: it names the project the scripts were compiled137against, and a verdict full of missing-`res://` errors (plus the type errors derived138from them) usually means the wrong project, not a broken script — `null` means no139project was resolved at all. Pass `--project` for the project that owns the files140and re-read the verdict. gda refuses up front, with `target_outside_project`, when141the resolved project plainly does not own a path — outside its tree, or claimed by142a nested `project.godot`, and with no project resolved too (`script run` and143`resource import` refuse the same way). It names the owner it found144(`evidence.owning_project`) but never switches to it, and the message states the145whole re-issue: `--project <owner>` AND the target respelled relative to that146owner — a relative path anchors at the project, so your original spelling would147not be found under the new one. The same three commands refuse a path whose CASE148does not match the stored file, with `path_case_mismatch`: such a path opens on a149case-insensitive filesystem (the common default on macOS and Windows) and fails on a150case-sensitive one (the common default on Linux and most export hosts), so re-issue with the `evidence.stored_path` the refusal151names. `--all` needs no such check: every project-wide listing152(`script list`, `scene list`, `script validate --all`, `project` analysis) walks the153`res://` tree with the engine's own skip rule, declining the engine cache, a directory154holding a nested `project.godot`, and one holding a `.gdignore` (hidden and dot-prefixed155directories gda still enumerates, unlike the engine). So `--all` reports only156what the resolved project owns; to work on a nested project's files, run with157`--project <that project>`.158159## Discovery160161- `gda --help` — every group.162- `gda <group> --help` — a group's commands.163- `gda help <group> <command>` — the same help from the command form; with `--json` it164 comes back as `{command, text}`.165- `gda <group> <command> --schema` — one command's input/output/error JSON Schema166 (no Godot spawned), plus `argv`: how each parameter is written on a command line167 (`kind` positional or option, its `position` or `--option` spelling, whether it is168 `required`, a valueless `flag`, `multiple` — repeat it per value — or a169 `json_value` — one token carrying the value's JSON). Build the command line from170 `argv`; `input_property` links each binding to the input property it fills.171- `gda schema` — the **whole** surface as one JSON manifest, `argv` included.172- `gda version --json` — which `gda` is installed and where from; `gda info` — the173 engine's version.174175**`--json` placement.** Every parser takes it: `gda --json <group> <command>`,176`gda <group> --json <command>` and `gda <group> <command> --json` mean the same thing — a `--json`177written before the command applies to the command it invokes — so any spelling works, as do several178at once. `gda schema --json` is accepted too, and idempotent: the manifest is already JSON. Two179limits: the `--help` FLAG always renders TEXT (`gda --json --help` returns the same help, never180JSON — use `gda help <command> --json` for a structured payload), and the flag is not a command, so181`gda <group> --json` and a bare `gda --json` with no command are both the usage error182`Missing command.` (exit `2`).183184## Headless commands (Godot 4.4+, all platforms)185186| Group | Commands |187| ----- | -------- |188| `scene` | `create`, `get`, `list`, `get-exports`, `delete`, `validate`, `preflight` (`.tscn` files; `validate` is the STATIC verdict `get` does not give — a scene loads fine with its script and texture missing, so check dependencies resolve and attached scripts compile before trusting it; invalid exits 0 with `valid: false` plus one problem per problem file — COMPOSED over the sub-scenes it references — normally the ones it instances, but a reference is followed when its path ends in `.tscn`/`.scn` OR its `[ext_resource]` line declares `type="PackedScene"`, a union because Godot loads every `[ext_resource]` whatever it is called while `ResourceSaver` will put a PackedScene in a plain `.res` (one saved under a non-scene extension AND declared as something else stays outside) — so a parent whose child is broken is invalid too and every problem carries `scene`, the file it was found in, in the same canonical spelling as the result's `path` (read its `path`/`nodes` against THAT file); three kinds of edge are reported instead of followed — `cyclic_instance` for a cycle, `unreadable_sub_scene` for a scene that loads but carries no `[gd_scene]` text to walk (a binary `.scn`, or a PackedScene in a `.res`), and `instance_depth_exceeded` for a scene no route reaches within 16 levels of sub-scenes; the last two say that subtree is UNCHECKED, not sound (validate it directly, or re-save it as `.tscn`, for its own verdict), and the depth bound is on the shortest route so the verdict does not depend on declaration order, and it covers gda's walk only, not the engine's own load of the chain; staged: unresolved dependencies suppress the script compile/binding pass, so repair them and rerun for the rest. `preflight` is the DYNAMIC one: it boots the scene headless, waits for `_ready`, and reports `status` (`ready`/`not_ready`/`timeout`) plus the script errors it recognized — read `started`; leaked objects or resources at exit are recognized too (a `shutdown_leak` diagnostic, printed by the engine after the scene ran, about the whole process) — reported in `diagnostics` but NOT gating `started`, which answers how the boot went; a `timeout` verdict also carries `elapsed_seconds` and `timeout_seconds` (the `--timeout` it reached), the same evidence pair the `launch_timeout` envelope gives elsewhere — it names the consumed ceiling, not the cause: a stuck scene and a healthy one whose `--frames` window outruns the same ceiling read alike, so pick a larger `--timeout` or fewer `--frames` from what the scene should do, and rerun — both keys are on that verdict only and omitted from every other. Passing `validate` is not "it works": check both) |189| `node` | `add`, `get`, `list`, `set`, `remove`, `duplicate`, `move`, `connect-signal`, `disconnect-signal` (nodes within a scene) |190| `script` | `create`, `get`, `list`, `set`, `delete`, `attach`, `validate`, `run` (`.gd` files; `validate` takes SEVERAL paths at once — one engine launch for the whole batch, one aggregate `valid` plus a per-file entry under `scripts` — or `--all` for every script in the project; `run` executes a project script one-shot (address it project-relative or as `res://` — the two portable forms, which `script validate` takes too; `run` alone refuses absolute paths) and passes its `exit_status`/`stdout`/`stderr` through — `stdout` above 64 KiB is truncated to its leading bytes with the COMPLETE stream spilled to the file named in `stdout_file` (`stdout_bytes`/`stdout_truncated` disclose it; a spill gda cannot write is the typed `stdout_spill_failed`, never an unbounded result), and a non-zero `quit()` is still success, so read `exit_status`, or pass `--strict` to get a `script_failed` failure (exit 4) whose `diagnostics` carries the script's own stdout and stderr — `--strict` fails a run for EITHER the non-zero status or a `shutdown_leak` diagnostic, the engine's exit-time report that the PROCESS left objects or resources alive (an autoload's leak counts; its RID leak reports are not recognized), which a status-only gate never sees because a suite can pass, `quit(0)` and still leak; a script that never ran — missing, or a failed parse/compile — always fails; `--timeout <s>` sets the ceiling (default 120) and a run that reaches it fails with `launch_timeout` carrying the captured partial output, the elapsed seconds and a termination phase; add `--completion-marker <line>` naming a line your script prints when its work is done — a caller-declared liveness contract, not a death detector: gda ends the run once it observes a recognized error attributable to the entry script, no marker line yet, and then silence on both streams — `script_aborted` (exit 4) with the captured error, in seconds rather than at the ceiling; declaring the marker asserts the script keeps printing until that line, so have it print progress during quiet stretches longer than ~3s, or omit the marker; a script that writes `user://` belongs under `gda --user-data-root DIR script run …` (see Setup) — the result then names `user_data_root` and `log_file` beside the always-present `engine_data_path`) |191| `project` | `info`, `get`, `set`, `list`, `add-autoload`, `remove-autoload`, `add-input-action`, `remove-input-action`, `find-references`, `dependencies`, `find-unused-resources`, `statistics` (a WRITE saves through the engine, which reserializes the whole `project.godot`: it deletes explicit lines whose value equals the engine default, adds or rewrites `application/config/features`, and reorders the sections. gda restores the deleted lines and reports `added_settings`, `rewritten_settings`, `restored_settings` and `sections_reordered` on every write result — read them when the file is tracked, and expect the section order to be the engine's) |192| `resource` | `create`, `get`, `set`, `delete`, `uid`, `import` (`.tres` files and project assets; `import` ensures importable assets — PNGs and other files the engine imports — are in the project cache: clean-worktree loading; a script needs no import and reports `not_importable`; an `invalid` or `failed` asset says why in `reason` — branch on that rather than reading prose: `sidecar_marked_invalid` means the ENGINE failed the last import (repair the source), `sidecar_unparsable`/`receipt_unsupported` mean gda cannot read the artifact (delete the `.import` sidecar and re-import; `detail` names the offending line or the receipt), and `dest_missing_after_pass` means the pass ran and produced nothing. `engine_output` is the separate half and does not follow `reason`: the pass's stderr lines naming that asset whenever THIS request ran a pass (empty when none ran; at most 20, `engine_output_truncated` when more matched) — the engine names an asset it then skips, so an `invalid` one can carry lines too. They are the lines NAMING the asset, which for some importers is only the verdict and not the root cause, so keep the whole engine stream with `gda --user-data-root DIR resource import …` and read `DIR/logs/godot.log`. An `invalid` reason survives into the settled `failed`, so a real run still names the pre-pass check. A real run also reports `created` — every file the pass created, classified `cache_owned`/`source_adjacent` like `export run`'s — and `skipped`, the entries its inventory could not read, so `created` is complete when `skipped` is 0) |193| `export` | `list`, `get`, `run`, `smoke` (export a preset by name; `--mode` release/debug/pack; `get` also reports `templates_root`, the export-templates directory it checked, and `templates_root_host`, set when a `--user-data-root` redirect hid templates installed on the host; `run` also reports `project_tree_mutations` — the export runs the editor import pass, so it names every file it created anywhere under the project, each classified `cache_owned`/`source_adjacent` against the `cache_root` it names, plus the pre-existing files OUTSIDE that root whose CONTENT it rewrote (only a file whose size or timestamp moved is compared; rewrites inside the cache root are not reported at all, so an empty `modified` says nothing about the cache) — a cold cache leaves thousands of entries there, `skipped` counts what could not be read, and a FAILED export reports none of it. `smoke` RUNS a built artifact headless and hands back its completed process — feed it `run`'s `output_path`: a macOS `.app` resolves to the `Contents/MacOS` file its `Contents/Info.plist` names in `CFBundleExecutable`, any other host-runnable file runs as given, an absent path is `export_artifact_not_found` and anything else is `export_artifact_not_runnable`; support is bounded to those two shapes with end-to-end evidence on macOS only and no Linux or Windows commitment until probed. It takes no `--project` (a relative artifact path resolves against your cwd), `--arg` values reach the game in order after Godot's `--` where it reads them with `OS.get_cmdline_user_args()`, `--quit-after N` asks the engine to end its main loop normally after N process frames so engine cleanup and its exit-time diagnostics run — it asserts NO project completion — and `--timeout` (the same default ceiling as `script run`) is only the external hard bound, so a run it ends cannot prove shutdown-only diagnostics are absent. The result carries `exit_status` (non-zero is DATA — read it, do not assume success means zero), `stdout` under the same cap `script run` uses with `stdout_bytes`/`stdout_truncated`/`stdout_file`, `stderr`, `diagnostics` and both paths (`artifact`, `executable`); `--strict` turns a non-zero status OR a `shutdown_leak` diagnostic into `smoke_failed` (exit 4) carrying `evidence.exit_status` and `evidence.script_errors`. The game runs under a private `user://` gda creates and removes, so pass the global `--user-data-root DIR` to keep what it writes) |194| `shader` | `create`, `get`, `set` (`.gdshader` files) |195| `theme` | `create` (a loadable `.tres` Theme) |196197Every headless reply carries its floats at full binary64 precision, so a value read back through `node get`, `scene get-exports`, `project get`/`project list`, `resource get`, or the echo of a `set` is the exact number the project holds — `1e-300` reads back as `1e-300`, not `0.0`; the one residual belongs to the ENGINE's writer — a negative zero reads back as `0.0` (#771). The `--value` string you send IN is coerced by the engine's own parser, and gda refuses what that parser would destroy: a literal it reads as `0.0` when you did not write zero, or as `NaN` at all, fails with `uncoercible_value` (exit 4, target untouched) instead of writing a number you never sent — `2.2250738585072014e-308` and `5e-324`, and also `0.000000000000000001`, whose 18 leading zeros fill the parser's whole mantissa window (`1e-18` is exact). The rule follows the LITERAL, not the property type, so a number inside a Dictionary or Array `--value` is refused the same way and names the offending literal: `--value '{"a": 1e-320}'` fails, `--value '{"a": 1e-18}'` stores exactly. Only real JSON numbers are read — a numeric-looking STRING value (`{"a": "1e-320"}`) and a numeric-looking KEY (`{"1e-320": 1.0}`) are stored unchanged (#805). So spell a small or many-digit value in SCIENTIFIC notation carrying only the digits it needs: that also avoids the low-digit loss the parser inflicts on a full-precision literal between `1e-4` and `1e-2`, which is disclosed rather than refused (#772). Read the `set` echo when the exact bits matter.198199## Live operations (via the daemon; Godot 4.6+, macOS/Linux)200201Prerequisites: the project defines `application/run/main_scene`, or you pass `--scene`.202For settings it can determine from the project files, `daemon start` refuses an empty main203scene with `live_main_scene_undefined`, or a `uid://` main scene without its UID cache with204`live_main_scene_unresolved` — run `gda resource import <any existing res:// asset>` once,205or open the project in the editor. Main-scene feature overrides, default/custom settings206overlays (`override.cfg`, or a nonempty `application/config/project_settings_override` path,207including feature overrides of that path), and escaped application keys defer to the engine; feature overrides208or unrecognized boolean values for `application/config/use_hidden_project_data_directory` defer only209the UID-cache check. Deferred cases can still show a native alert on macOS even headless210until the readiness deadline tears down the session. An explicit valid `--scene res://<scene>.tscn`211avoids main-scene resolution. Run `gda daemon start` first (optionally `--scene <res://...>` to boot a212specific scene instead of the project's main scene); the engine session launches lazily on213the first operation that requires one. To establish the session deterministically, run214`gda daemon wait-ready` (`--timeout` budgets daemon waits and new-work decisions;215a synchronous launch call can delay expiry observation; idempotent while the session is216alive) — success means live reads serve. This matters for the read-only diagnostics: `diag errors` /217`logger tail` never launch a session themselves, so right after `daemon start` they report218`engine_session_not_running` by design — expected, not a defect; run `wait-ready` first.219Serving is NOT the same as a cleanly started scene: a script that fails to compile leaves220its node script-less and the session serves anyway, leaving the tree without whatever the221script would have built, and often a blank frame. So `wait-ready` also reports `clean_start`222and the `startup_diagnostics` it recognized in the session log UP TO THE HANDSHAKE — the same223records `script run` and `scene preflight` carry, read from the bytes the log held at the224instant the harness handshake completed, so they cover engine startup, the autoloads and the225scene's own scripts; a record emitted during the handshake's own frames lands on whichever226side of that instant it was written. Read `clean_start` before you treat a screenshot or a227runtime read as evidence about the scene. Both keys are null when gda could not read that228prefix (`diag errors` then says `live_log_unavailable`) — never a clean start it did not see.229`daemon status` repeats that verdict for the serving session without relaunching it, and230`diag errors` reads the whole log, including everything printed after that instant.231A `live_timeout` discards the session (its late reply can no longer be attributed), so the232next operation starts a fresh game and the runtime state you had set is gone. Most often233it means the game stopped returning to its main loop — look for a blocking loop or wait in234game code. But the 30s bound is a wall clock while a multi-frame window (`--frames`,235`--await-frames`) waits that many ENGINE frames, so on a slow-ticking game a window op236outruns it with the loop running normally: ask for fewer frames when `gda logger tail`237shows the log kept advancing. A paused `SceneTree` is NOT a cause; see "paused vs238suspended" below.239`screen capture` needs a windowed session240(`gda daemon start --windowed`).241242A windowed session needs the host's real desktop session — an on-console GUI login on243macOS, `$DISPLAY` / `$WAYLAND_DISPLAY` on Linux. Over SSH, on a headless CI box, or from244a sandbox that blocks the window server, `daemon start --windowed` refuses before245spawning Godot. Branch on the code, not the sentence:246247- `live_windowed_unavailable` — nothing refused the probe and no session is reachable, so248 this host cannot show a window. Skip the rendered check; headless live ops (`game`,249 `perf`, `input`, `diag`, `logger`) still work.250- `live_windowed_permission_denied` — this process is not allowed to even look up the251 window server (e.g. a sandbox). It does NOT mean the host has one: macOS refuses the252 lookup before resolving it, so a broadly-confined process is refused either way. Re-run253 outside the restriction to find out; do not record the machine as display-less on this254 code alone.255256A refusal from `gda daemon start --windowed` carries `error.probe` `{name, platform}`257naming the OS call that decided — including when the refusal is relayed from an258already-running daemon's lazy Engine-session launch; only the outer259`{stdout, stderr, exit_code}` transport shape is probe-less.260261| Group | Commands |262| ----- | -------- |263| `daemon` | `start`, `wait-ready`, `stop`, `status`, `install`, `uninstall` (lifecycle; `start` installs the in-game harness itself, so `install` is only for doing that step deliberately — e.g. to review or commit the `project.godot` change — and `uninstall` reverses it; `wait-ready` establishes the lazily-launched engine session, with `--timeout` shared by its waits and new-work decisions, so a first `diag errors` serves instead of reporting `engine_session_not_running`; `status` reports the last successfully established engine session's `session_id` — the identity a `screen capture` receipt correlates with, minted anew per established session and retained across a failed replacement launch — and repeats that session's startup verdict, `clean_start` plus `startup_diagnostics`, so a caller reads it without relaunching the game) |264| `game` | `tree`, `find`, `get`, `rect`, `set`, `call` (the running game's runtime scene graph; `tree --root <runtime path> --max-depth N` bounds the read, and `find` locates nodes by SELECTOR rather than by path — see "Find a node before you address it" below; `get --texture-digest` opts a read into content digests for path-less `Texture2D` values. `rect` reads a `Control`'s layout OUTPUT, which no storage property carries: the rendered viewport rect (`position`/`size`), the same rectangle in the PARENT's space (`local_position`/`local_size`), and the two minimum sizes — `minimum_size` is the class's intrinsic minimum and EXCLUDES the authored `custom_minimum_size`, while `combined_minimum_size` is the per-axis maximum of the two, the size a parent `Container` honors. `get` cannot serve `position`, `size`, `global_position` or `global_rect` on a `Control` — none of them is a storage property — so it refuses with `live_unknown_property` and the message names `game rect` and the layout INPUTS, which depend on the parent: a free `Control` carries `offset_left`/`offset_top`/`offset_right`/`offset_bottom` and `anchor_left`/`anchor_top`/`anchor_right`/`anchor_bottom`, while a direct child of a `Container` carries NONE of those — the engine drops them from its storage set — and carries `custom_minimum_size`, `size_flags_horizontal` and `size_flags_vertical` instead, the rest being the parent `Container`'s own layout. `call --method NAME [--args JSON]` invokes a method named by the `GDA_CALLABLE` declaration resolved from the node's attached script along its base chain — use it for a debug/state contract exposed as a method rather than a property. gda calls nothing undeclared, so an undeclared-but-present method is `live_method_not_allowlisted` and its message names the declared set; a missing one is `live_unknown_method`, and arguments the declared parameters cannot take (wrong count, a type the engine would not convert, a typed `Array[int]` parameter) are `live_invalid_call_args`, refused before the call. The live parser materializes every JSON number as float. `NaN`/`Infinity` are refused; RFC JSON excludes them, although some in-memory schema validators accept them as numbers. Finite floats do not inherit the integer bound, but a float whose wire literal Godot's parser reads as `0.0` is refused too — `DBL_MIN`, any subnormal, and many-digit values such as `1.2345678901234567e-300`, none of which any decimal spelling can deliver; a float it does read arrives changed in its low-order bits — 1 ULP at ordinary magnitudes, and tens of doubles for a full-precision literal between `1e-4` and `1e-2`, where the parser truncates past 18 mantissa digits (#752). JSON integer values beyond ±(2^53−1) are refused CLI-side because the wire can change them. Standard JSON Schema cannot distinguish an exponent-form float from the equal integer, so the params model enforces the integer-token limit at execution. LIMIT: gda CANNOT verify a declared method has no side effects — the constant records the project's own read-only assertion, and what gda guarantees is only that no undeclared method is called. GDScript forbids redeclaring a base class's constant, so an opted-in inheritance chain has at most one declaration owner; a base owner covers its subclasses and need not define every method it names) |265| `diag` | `errors` (structured runtime errors with callstacks; survive a crash) |266| `logger` | `tail` (the running game's structured log stream; `--raw` for verbatim lines, `--level <min>` to filter by severity, `--limit N`) |267| `perf` | `monitors`, `monitor` (counters: a one-frame snapshot, or with `--frames` a bounded window with statistics and optional `--budget` verdicts / a per-node timeline. A window ALLOCATES inside the game you are measuring — the harness holds the whole window — so every window result reports `collector_bytes`: the LOGICAL size of what the sampler kept, 8 bytes per stored value over one column per sampled monitor plus one of timestamps. It is a LOWER bound on the real in-game cost — a packed column over-allocates as it grows, and the shared window base's per-frame accumulator is not counted — and it cannot attribute a `static_memory` rise by itself — before calling anything a game leak, take a matched baseline window (the same `--frames` and `--monitor` set on a scene that allocates nothing, on the same host) and compare the two rises. `monitors --frames N --summary` returns the same statistics, budget verdicts and `collector_bytes` with the per-frame rows left out (`samples: null`, `samples_omitted: true`), so a long window's result does not grow with the frame count; the window is still sampled in full) |268| `input` | `key`, `mouse-click`, `mouse-move`, `action`, `tap`, `sequence` |269| `screen` | `capture`, `frames` (viewport PNGs; needs `--windowed`. `frames --summary` returns the compact aggregate — directory, filename pattern, frame size, total bytes — instead of the per-frame list, so a large capture's envelope stays small; every frame is still written. `capture --await-node/--await-property/--await-value [--await-frames] [--await-events]` is the predicate-gated form: it fires on the first frame boundary where the property equals the value, optionally injecting input inside the same window — use it for short transients instead of a separate input + capture. With `--settle-frames` 0, the default, the observed property and the pixels belong to the same COMPLETED frame; a value overwritten before its frame completes is never observable, an injected event's effect is observable from the next boundary, and a declared event that fails makes the capture that typed failure. Every `capture` result carries a `receipt` binding the image to its capture event — `session_id` correlating with `daemon status`, the LAUNCHED scene's path and header uid (uid null for gda-authored scenes), two frame counters, the gated capture's observed echo, and the written file's SHA-256 — so a plain capture needs no local hashing, and a gated capture's complete evidence is the receipt plus the sibling `predicate` report. The two counters differ: `engine_frame` is the process frame the READ was taken at, `render_frame` the drawn frame the pixels ARE, and they diverge whenever the engine skipped a draw — two captures with the same `render_frame` are the same drawn frame, so identical pixels there are the engine's doing, not the game's. `--settle-frames N` on either command lets the game run N more process frames before the read, for a visual that settles after a state change; the default is 0 because a capture has nothing pending to observe, unlike `input tap`'s release. With `--await-*` the settle runs after the predicate holds, so the report keeps its own frame and the receipt's `engine_frame` is that frame plus N; on `frames` it runs once, before the first frame) |270271Every live reply carries its floats at full binary64 precision, so a value read back through `game get`, `game call`, `perf`, or a monitored timeline equals the value the running game holds; the one residual belongs to the ENGINE's writer — a negative zero it wrote reads back as `0.0` (#752). A number gda produces CLI-side meets no Godot writer and so does not carry that residual: `perf monitors --frames` reports its `mean` and every budget-verdict number (the bounds copied from your own budget file) exactly, negative zero included. Each live result field says which of the two it is.272273In the other direction the wire is narrower, and EVERY live command RELAYED to the game applies the same rule — not just `game call`; the three the daemon answers itself (`diag errors`, `logger tail`, `daemon wait-ready`) send no number to the engine and are outside it. A number gda cannot send unchanged is refused before the request leaves: a float whose wire literal Godot's parser reads as `0.0` (`DBL_MIN`, any subnormal, many-digit values such as `1.2345678901234567e-300`) and a JSON integer beyond ±(2^53−1). It is a usage error on the argv path and `invalid_params` on `--params-json`, decided without a running daemon, and it reaches nested values — a sequence event's `x`, a `game call` argument inside a dictionary. A float the parser DOES read still arrives changed in its low-order bits: 1 ULP at ordinary magnitudes, tens of doubles for a full-precision literal between `1e-4` and `1e-2`. That residual is disclosed, not refused (#752). `game set --value` is OUTSIDE that rule — the value travels as a STRING, so the wire never sees a number — and has a refusal of its own, decided in the session by the harness: the string is coerced with the engine's own parser and a literal it reads as `0.0` when you did not write zero, or as `NaN` at all, fails with `live_uncoercible_value` (exit 6, the running game untouched), the same rule headless `--value` follows (#772) — containers included, so a destroyed number inside a Dictionary or Array value is refused here too (#805).274275`gda input` injects through two routes, and every result names the one it used276(`injection_route`). By default `input action`, `input tap --action` and a sequence277`action` event drive `Input.action_press`/`action_release`: the `action_state` route, a278change to the POLLED action state that reaches no `_input`, `_gui_input` or279`_unhandled_input` handler. `input key`, the mouse commands and the `key` /280`mouse_*` sequence kinds push an InputEvent through the root viewport: the281`viewport_event` route. Rule of thumb — drive event-driven UI (a Control, a282modal, a scrollable) with a key or mouse event, and use an action where the game283polls `Input.is_action_*`. A successful action injection is not evidence that the284event path works.285286`--as-event` is the explicit opt-in to the OTHER door for an action: gda pushes an287`InputEventAction` through the root viewport, so handlers matching the action288receive it and the polled state stays untouched. It rides `input action`,289`input tap --action`, and a sequence `action` event (`"as_event": true`), and the290opted-in result reports `viewport_event`. The default is unchanged. The matrix, for291one action and the key it is mapped to:292293| injection | `Input.is_action_pressed` | `_input` / `_unhandled_input` | `_gui_input` |294| --- | --- | --- | --- |295| `input action` (state route) | yes | no | no |296| `input action --as-event` | no | yes | yes (focused Control) |297| `input key` of the mapped key | no | yes | yes (focused Control) |298299Reach for it when a Control, a modal or another event-driven handler must react to300an ACTION rather than to the key it is bound to — otherwise a key or mouse event is301the plainer tool.302303The matrix describes eligible delivery under Godot's normal propagation and304consumption rules, not proof that every handler ran or a UI action succeeded.305Use the current harness bundled with gda. After updating gda, stop/start an existing306daemon session before using live commands; syncing the installed file does not307reload code in the running game. Mixed-version sessions are not supported.308309For a UI activation, use the gesture commands, not a lone event. Godot activates a310`Button` on the RELEASE, so a bare press never emits `pressed`; and a focused UI311does not advance when the press and the release land on the same process frame.312`gda input mouse-click` injects the whole gesture — the initial move, the press,313and the release, one per process frame — and `gda input tap --key K` /314`gda input tap --action N315316…(truncated)