# Gda

> 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.

- Skill: `aigengame/gda` (Agent Skill)
- Install (CLI): `npx skillmds@latest add aigengame/gda`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aigengame/gda/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: aigengame (https://skillmd.com/u/aigengame)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/aigengame/gda

---


`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

```json
{"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)
