Output Contract

July 29, 2026 · View on GitHub

Status: stable as of v1.0.0. Field lists below were captured from a live Godot 4.7 editor and are protected by contract goldens. The stable markings are binding compatibility commitments for the v1 major line.

This document defines what consumers of the hera CLI — agents, scripts, CI pipelines, wrappers — may rely on: invocation shape, output streams, exit codes, error shapes, and per-command response fields. (hera-agent-godot is the transitional alias for the same binary; the contract is identical.)

What is public, what is internal

  • Public contract: the CLI — its arguments, stdout/stderr, and exit codes.
  • Internal: the localhost HTTP /rpc protocol between CLI and addon ({"tool", "params"}{"ok", "data", "error"}), the heartbeat files under ~/.hera-agent-godot/instances/, and port selection. CLI and addon ship and version together; the wire protocol may change between releases and is not a stable integration point. Build on the CLI, not on /rpc.

Stability tiers

TierMeaning
stableShape is frozen for v1.0.0. Documented fields keep their name and JSON type within a major version. New fields may be added at any time — consumers must ignore unknown fields.
experimentalShape may change in any release. Changes are called out in release notes but carry no compatibility promise.

Removing or renaming a stable field, or changing its JSON type, is a breaking change and requires a major version bump plus a deprecation cycle.

Versioning and deprecation policy

Hera follows semantic versioning from v1.0.0 onward. The CLI and addon ship as one versioned product and should be upgraded together.

  • Patch releases contain compatible fixes and documentation corrections.
  • Minor releases may add commands, flags, or response fields. Consumers of stable JSON must ignore unknown fields.
  • A stable command, flag, field, JSON type, output stream, or exit-code meaning is deprecated in a minor release before it can be removed or incompatibly changed. Removal happens no earlier than the next major release.
  • Experimental surfaces may change in a minor release. Their release notes must identify the change, but they do not carry the stable compatibility promise.
  • The internal localhost HTTP protocol and heartbeat files remain internal and may change without a public deprecation cycle.

See MIGRATING_TO_V1.md for the 0.x upgrade path.

Invocation

hera [--json|--ids] [--instance <pid>] [--timeout <ms>] <command> [args]
  • Global flags come before the command. --instance and --timeout accept both --flag N and --flag=N.
  • --timeout <ms> bounds each HTTP request (default 5000 ms); it does not bound a whole command — --wait polls send many requests. A timed-out request is a runtime failure (exit 1).
  • Unknown commands and malformed flags/arguments never reach the editor; they fail fast with exit code 2.
  • With opt-in shared-token auth enabled (SECURITY.md), a token mismatch is a runtime failure: unauthorized: ... on stderr, exit 1. The CLI picks the token up automatically from HERA_AGENT_GODOT_TOKEN or ~/.hera-agent-godot/token.

Output streams

  • stdout carries exactly one payload: the response data as compact JSON on a single line (default mode). Nothing else is written to stdout on the success path.
  • stderr carries human-readable diagnostics on failure, one line per problem, in the shape <command>: <message> (e.g. node: node not found: /nonexistent). Error message text is not part of the contract — do not parse it; branch on the exit code instead. Messages may include actionable hints (e.g. game: no game is running; start one with `hera run --current --wait` ).
  • version is the one exception: it prints a bare version string, not JSON.

Output modes

ModeBehavior
defaultCompact JSON, one line.
--jsonSame data, pretty-printed with 2-space indent.
--idsFor responses carrying a nodes array (scene tree, node find): node paths only, one per line. Anything else falls back to compact JSON.

JSON conventions

  • Encoding is UTF-8. Key order is not part of the contract.
  • Consumers must tolerate unknown fields (additive evolution is always allowed, even on stable commands).
  • Project-relative paths use res://; absolute paths use forward slashes on all platforms (e.g. C:/Users/...).
  • Node/resource property values are Godot-stringified (e.g. position "(0.0, 0.0)", booleans "true"), not typed JSON — this applies to node get, game node get, resource get, and game assert actual/expected values.

Exit codes

CodeMeaningExamples (verified)
0Success.status, scene tree, passing game assert
1Runtime failure or failed check: no live editor, tool returned an error, mutation guard refused, or a verdict command reported not-OK.node get /nonexistent, game tree with no game running, game ui audit with errors, game qa diagnose with issues
2Usage error: unknown command, missing/invalid flag argument, invalid --instance pid or --timeout value, malformed scenario file arguments.hera bogus, run --scene (missing value), --instance abc, --timeout abc

Verdict commands

Commands whose job is to pass or fail mirror the verdict in the exit code, but differ in where the detail goes:

  • game assert — pass: verdict JSON on stdout ({"prop","op","actual","expected"}), exit 0. Fail: the addon returns an error, so the CLI prints game: assert failed: ... to stderr and exits 1 (no stdout payload).
  • game qa --file — summary JSON on stdout ({"ok","steps","results"[,"requirements","requirements_covered","requirements_missing"]}), exit 0/1 following ok. A requirement with no successful covering step makes ok false.
  • game qa diagnose — report JSON on stdout ({"ok","checks":[{"name","ok",...}],"issues":[...]}), exit 0/1 following ok.
  • game ui audit — audit JSON on stdout ({"ok","strict","scope","controls","errors","warnings","findings","truncated"}), exit 0/1 following ok. Errors always make ok false; warnings do so only with --strict. Operation failures remain stderr-only.
  • smoke — progress/summary output, exit 0/1 by overall result.

Per-command contract

Tier markings are the Phase 7 proposal. "Key fields" lists top-level data fields; entries marked ✓ were captured live from a Godot 4.7 editor. Stable-command stdout is additionally pinned byte-for-byte by the golden contract tests (see Contract tests).

Core & discovery

CommandTierKey fields
statusstablepid, project_name, project_path, godot_version, scene. (game_feel_mode, game_feel_ui_mode are experimental fields inside a stable response.)
instancesstablecount, instances[] of {pid, port, project_path, godot_version, scene, ts}
versionstablebare string (linker-injected; dev for source builds)
run / stopstable✓ state shape {playing, scene}

Editor reads

CommandTierKey fields
scene treestablescene, count, truncated, nodes[] of {name, path, type}
scene liststablecurrent, open[]
editor statestablecurrent_scene, current_script{found, path}, main_scene, open_scenes[], playing, playing_scene, project_name, project_path, selected[]
editor selectedstableselection list with scene-relative paths
node findstablecount, truncated, nodes[] of {name, path, type}
node getstablename, path, type, properties{} (stringified values)
signal liststablenode, count, truncated, signals[] of {name, args[], connections[]} (+ external_connections when editor-internal targets exist)
screenshot diffexperimentalbefore, after, width, height, threshold, total_pixels, changed_pixels, changed_ratio, max_delta, identical, and changed_bounds{x,y,width,height} when anything changed. Computed locally; max_delta is reported even when it is under the threshold
theme get / theme setexperimental✓ (get) path, types[], items{<type>{colors{}, constants{}, font_sizes{}}}; (set) path, type, applied{}, undoable:false. Colour values are printed to 6 decimals so a value round-trips what the caller wrote rather than float32 noise
project infostablename, path, current_scene, files{all, scene, script, resource, asset, shader, other}, godot{...}
project list-filesstablefile list with compact type tags
classdb info/methods/properties/signals/constants/enums/inheritsstable✓ (info) class, parent, can_instantiate, is_node, is_resource
resource getstableclass, name, editor-visible properties
resource uidstablepath, uid, uid_path, sidecar, sidecar_exists
resource liststableresource entries with class + path
outputstableavailable, log_path, type, total, lines[]
diagnosticsstableavailable, clean, file_logging_enabled, log_path, total_lines, error_count, errors[], warning_count, warnings[]. available is false whenever the log cannot be read, and clean is false there too since cleanliness cannot be asserted without a readable log. file_logging_enabled is the effective value (get_setting_with_override), because file logging defaults to true on desktop through the .pc feature tag while the untagged default is false
script current / script inspectexperimentalcompact script metadata (class name, extends, functions, signals, exports, line count)

Editor mutations

All enforce the single-editor guard unless --instance is passed; guard refusal is exit 1.

CommandTierNotes
node add / node set / node removestableundoable; node add may include an experimental agent_hint field when Game Feel Mode is on
signal connect / signal disconnectstableundoable, CONNECT_PERSIST
scene open / scene savestable
evalstablestringified expression result
batchstablesequential results array; --continue keeps going past failures
node instance / node set-resource / node attach-script / node detach-scriptexperimentalattach-script responses include dependency diagnostics whose shape may evolve
scene create / scene save-as / scene reloadexperimentalpersistent filesystem changes
editor select / editor clear-selectionexperimentaleditor-state mutation only
script open / script createexperimental
project mkdir / project scan / project reimport / project set-main-sceneexperimentalpersistent project changes
resource set / resource create / resource resave / resource update-uids / resource export-mesh-libraryexperimentalpersistent filesystem changes
screenshotstable (base)✓-adjacent base fields path, width, height; the --analyze metrics block is experimental

Runtime (game) surface

Requires a play session plus the HeraGameInspector autoload; not undoable.

CommandTierKey fields
game treestablescene + compact node list
game node get/set/callstableget mirrors node get (stringified values); call returns a stringified result
game assertstable✓ pass: {prop, op, actual, expected}; fail: stderr + exit 1
game instancesexperimentalinstances[] with pid, scene, heartbeat age
game ui treeexperimentalControl entries; fields selectable via --fields
game ui auditexperimentalok, strict, scope, controls, errors, warnings, structured findings[], truncated
game click / game input / game input-logexperimentalinput injection + diagnostic log (v0.7 surface)
game screenshotexperimentalcapture path; --analyze metrics evolve with QA guidance
game qa discoverexperimentalcallable qa_* helpers
game qa diagnoseexperimentalok, checks[] of {name, ok, ...}, issues[]
game qa --fileexperimentalok, steps, results[], requirements* (verdict semantics above)

Guidance & content

CommandTierNotes
guidance ui / guidance game-feelexperimental✓ (ui) mode, setting, instruction, checklist[]. Guidance text is content, not contract — it changes freely; only the envelope fields are candidates for stabilization.
game_feel [topic]experimentaltopic index / topic body; knowledge-base content evolves freely
smokeexperimentalcheck-by-check progress; exit code is the contract, output shape is not

Known gaps (tracked in ROADMAP Phase 7)

  • Detailed field-level schemas (types, optionality) for every subcommand are still to be pinned; this draft freezes names of the fields listed above.

Contract tests

cmd/contract_golden_test.go pins this contract in CI: it runs the real CLI end-to-end (argv → discovery → HTTP → stdout/exit code) against a mock editor serving fixture responses, and byte-compares stdout with golden files under cmd/testdata/contract/. Stable read commands use responses captured from a live Godot 4.7 editor; exit-code and stderr-shape semantics above are asserted directly. After an intentional contract change, regenerate with go test ./cmd -run TestContract -update and list the change in release notes.

See COMMANDS.md for flags and semantics, ARCHITECTURE.md for the request lifecycle, and ROADMAP.md for the standardization arc.