Gekko

Output contracts


The stdout contract

stdout carries the command result and nothing else. Diagnostics, warnings and runtime information go to stderr, always.

gko summarize README.md > summary.txt    # the file contains the summary, nothing more
cat ticket.md | gko classify | jq .      # safe to pipe into a JSON tool

This holds on failure paths too: when a command fails, stdout is empty — zero bytes — and the error is on stderr. It holds for argument errors raised by the CLI parser itself, and for the built-ins, whose report is their result and therefore goes to stdout.

It also holds whatever --verbose says. Verbosity moves a threshold on the diagnostic stream only: --verbose info adds engine traces to stderr and changes stdout by not one byte. See Verbosity.


Declaring an output contract

[output]
format = "json"
schema = "schemas/classification.json"
Key Type Default Notes
format "text" | "json" "text"
schema name or path none JSON only; see Schema paths
max_lines integer none text only
allow_truncated boolean false accept an answer cut short by max_tokens; see Truncated answers
strip_reasoning boolean false remove a leading <think>...</think> block; see Reasoning models
extract JSON pointer none JSON only; stdout gets one value; see Extracting one value

Rejected at load time, naming the command file:

format = "json" without a schema is allowed: the response is then only checked for being well-formed JSON.

Schema paths

A schema is declared in one of three forms:

Form Example Resolves to
bare name schema = "classification" <scope root>/schemas/classification.json
relative path schema = "schemas/classification.json" <scope root>/schemas/classification.json
absolute path schema = "/srv/schemas/ticket.json" itself

A value without a / and without a .json suffix is a name; anything else is a path. A relative path resolves against the scope root of the command file, not against the current directory and not against the command file itself — schemas/ is a sibling of commands/. A command coming from /etc/gekko therefore looks in /etc/gekko/schemas/. The depth of the command path makes no difference: commands/git/review.md still resolves against the scope root.

Sending the schema to the model

When the command's backend declares structured_output = true (see Configuration), the output schema is sent with the request as an OpenAI response_format of type json_schema: a server that supports it constrains the model's answer to the schema, so the prompt does not need to describe the expected shape. The answer is validated against the schema afterwards either way.

Schemas in the prompt

A command can also paste schemas into its prompt. Declare them in a [schemas] table, one id per schema, in any of the three forms above, and reference them with {{ schemas.<id> }}:

[schemas]
ticket = "ticket"

[output]
format = "json"
schema = "ticket"
Answer with a JSON object matching this schema:

{{ schemas.ticket }}

A placeholder naming an id missing from [schemas] is rejected at load time, naming the command file. The placeholder renders the schema document as compact JSON.

Schemas are loaded only when the command actually runs — before its input is read and before the backend is contacted. A schema that is missing or malformed on a command nobody invokes does not break the rest of the CLI. Checking all of them is what gko doctor is for.


Text output

The response is trimmed of leading and trailing whitespace. Nothing is parsed, nothing is unwrapped.

[output]
format = "text"
max_lines = 1

If max_lines is declared and the response has more non-empty lines than that, the command fails with exit code 4. The output is never silently truncated — a response that does not meet the declared contract is a failure, not something to repair.


Truncated answers

A backend that stops generating because it hit max_tokens (its own default, or the model's declared [generation].max_tokens) reports it as finish_reason = "length". gko treats that as an execution failure by default — exit code 4 — exactly like a schema violation or an max_lines overrun: a cut-off answer did not honor the command's contract any less than a malformed one.

[output]
allow_truncated = true

Setting allow_truncated = true accepts the truncated answer as-is instead: it is finalized and written to stdout like any other answer, and the command exits 0.

Truncation never triggers the fallback retry: the fallback exists for a prompt an NPU-served model refuses as too long, not for an answer that ran out of max_tokens — the model answered, it just did not finish.

The behavior differs slightly with the terminal versus a pipe:


Reasoning models

Some backends inline the model's reasoning into content itself, wrapped in <think>...</think>. Left as-is, that breaks format = "json" (the reasoning is not valid JSON, or sits before the JSON body) and max_lines = 1 (the reasoning adds lines the contract did not expect).

[output]
format = "json"
schema = "classification"
strip_reasoning = true

strip_reasoning = true removes exactly one leading <think>...</think> block — whitespace-tolerant before the opening tag, but the closing tag is required: an unclosed block is treated as content, never guessed at. This runs before the rest of the pipeline (fence removal, JSON parsing, schema validation, or the text branch's trim/max_lines). A block found anywhere other than the very start — in particular, in the middle of the answer — is left untouched.

The removed text is logged at info by length only (stripped 412 characters of reasoning), never by content: reasoning can be long, and the diagnostic stream is not a transcript.

Streaming is disabled when strip_reasoning = true, even on a terminal that would otherwise stream a plain-text answer: printing tokens as they arrive would show the reasoning block before gko has a chance to strip it. The answer then arrives in one piece, as it does for a JSON output contract. gko describe reports the key, so the behavior is discoverable without reading the command file.

A server that reports reasoning in a separate reasoning_content field rather than inlining it into content needs no handling here: only content is read. strip_reasoning is for the inline <think> case specifically, and the tag is not configurable.


JSON output

[output]
format = "json"
schema = "schemas/ticket.json"

The pipeline is:

model response → strip Markdown fences → parse JSON → validate against schema → stdout

Fenced responses

Models very often wrap their JSON in a Markdown code fence. gko removes an opening fence at the start and its closing fence at the end, with or without a language tag, tolerating surrounding whitespace:

```json
{"category": "bug", "confidence": 0.91}
```

A fence appearing in the middle of the response is left alone — that is content, not wrapping.

Normalised output

What reaches stdout is the compact serialisation of the parsed value, so stdout is always valid JSON whatever the model wrapped around it:

cat ticket.md | gko classify | jq .category

Extracting one value

A shell pipeline usually wants one value, not a document. extract names it with a JSON pointer:

[output]
format = "json"
schema = "classification"
extract = "/category"
category=$(cat ticket.md | gko classify)   # hardware, not {"category":"hardware",...}

The schema is still validated on the whole document; only then is the pointed value written: a string bare, without quotes, anything else as compact JSON. A pointer the document does not resolve is an output failure (exit 4), naming the pointer.

extract shapes the CLI's stdout only. An MCP client still receives the whole document, which is what the tool's advertised output schema describes, and the expectations of gko config test address the whole document too.


JSON Schema validation

Schemas are ordinary JSON Schema documents:

{
  "type": "object",
  "required": ["category", "confidence"],
  "properties": {
    "category": { "type": "string" },
    "confidence": { "type": "number", "minimum": 0, "maximum": 1 }
  },
  "additionalProperties": false
}

Validation failures list every violation, not just the first one, so a prompt can be fixed in one pass instead of one error at a time.

A response that is not valid JSON, or that violates the schema, is an execution failure with exit code 4. gko does not retry, does not reformulate, and does not ask the model again: a program driving the CLI gets a stable contract rather than a best effort.


Exit codes

Code Kind Meaning
0 — success
1 I/O unreadable file, broken pipe
2 Configuration your files are wrong — the message names the file
3 Backend unreachable, or a non-2xx HTTP response
4 Output the model's answer violated the declared contract

The distinction between 2 and 4 is the useful one for a calling program: 2 means your configuration is broken, 4 means your configuration is fine and the model answered badly. A missing or malformed schema file is 2, because the fault is in the configuration, even though it is only discovered when the command runs.