Gekko

Writing commands

gko config schema command prints the JSON Schema of the frontmatter; see gko config schema.


File to command name

The path under commands/ is the command name. There is no registration step.

File Command
commands/classify.md gko classify
commands/commit-message.md gko commit-message
commands/git/review.md gko git review
commands/ticket/classify.md gko ticket classify

Intermediate levels are created automatically, and commands sharing a prefix merge under the same parent. Running an intermediate level on its own (gko git) is a usage error: the CLI parser prints that level's help on stderr and exits with 2.

$ gko git
Usage: gko git [OPTIONS] [COMMAND]

Commands:
  review  Review a diff

Options:
  -v, --verbose <LEVEL>  Diagnostic verbosity on stderr; stdout always carries the result only [default: warn] [possible values: error, warn, info]
  -h, --help             Print help

Eight names are reserved by the built-ins and rejected at load time: backend, config, model, doctor, describe, update, help and mcp. The reservation applies to the first segment only, so commands/git/describe.md is perfectly valid.


Anatomy of a command file

A command is a Markdown file: TOML frontmatter between --- fences, then the prompt as the body.

[!NOTE] The fence is ---, not +++. A file opening with +++ is rejected at load time with a message naming both delimiters.

---
description = "Translate input text"
model = "qwen-fast"

[args.language]
short = "l"
required = true
description = "Target language"

[input]
mode = "stdin_or_file"
---

Translate the following text into {{ args.language }}.

Preserve meaning and tone.

{{ input }}
cat README.md | gko translate --language french

Frontmatter reference

Key Type Default Notes
description string "" shown in gko --help
model string required must match a model id
[input] mode string "stdin" see Input modes
[args.<name>] table none see CLI arguments
[output] table text, no limit see Output contracts
[schemas] table none <id> = "<name or path>", for {{ schemas.<id> }} — see Schemas in the prompt
[partials] table none <id> = "<name or path>", for {{ partials.<id> }} — see Partials
system string none a system-role message sent before the examples and the body — see System prompt and examples
[[examples]] array of tables none fixed few-shot user/assistant turns — see System prompt and examples
[generation] table none overrides the model's own [generation], key by key — see Generation parameters

[!IMPORTANT] Unknown keys are rejected, not ignored — at the top level, under [input], under [args.*] and under [output]. A typo like moed = "file" would otherwise fall back to the default silently, so gko summarize README.md would read stdin instead of your file without a word of warning.


Input modes

[input]
mode = "stdin_or_file"
Mode Behaviour
stdin read standard input to EOF
file read the positional FILE argument; omitting it is an error
stdin_or_file use FILE when given, otherwise read stdin
binary like stdin_or_file, read as bytes and uploaded as-is — only to a transcriptions operation

Modes that accept a file get an optional positional FILE argument in their generated CLI.

A binary input is never text: its prompt cannot reference {{ input }} (rejected at load time), it only goes to a model whose operation speaks transcriptions (see Operation protocols), and such a command is not offered as an MCP tool. It shares the 64 MiB input cap.

gko transcribe memo.wav
cat ticket.md | gko classify      # stdin
gko classify ticket.md            # file

CLI arguments

Each [args.<name>] table becomes a real flag, in deterministic order.

[args.language]
short = "l"
required = true
description = "Target language"
Key Type Default Notes
short string none must be exactly one character, not -, h or v
required bool false
description string "" shown in the command's --help
type string, enum, integer, file string validates values before execution
values string array none required and non-empty only for enum; duplicates rejected
min, max integer none only for integer; inclusive bounds

The table key is the long flag: --language. Rejected at load time, each naming the file:

enum values, integer syntax and bounds are checked before anything runs (usage error, exit 2). For file, the CLI flag names a UTF-8 file whose content replaces {{ args.<name> }}; the file is read after preflight and before stdin. MCP callers pass the content directly.

Values become available to the prompt as {{ args.<name> }}.

Default values and repeated or boolean flags are not supported.


Prompt templating

Templating is minimal: there are no conditions, no loops and no expressions, and the only include is a partial inserted verbatim.

Placeholder Resolves to
{{ input }} the resolved input (stdin or file)
{{ args.name }} the value of a declared argument
{{ env.NAME }} an environment variable
{{ schemas.id }} a schema declared in [schemas], as JSON
{{ partials.id }} a text fragment declared in [partials], verbatim

Whitespace inside the braces is flexible: {{input}}, {{ input }} and {{ input }} are the same. Substitution is never re-applied to substituted content, so an argument value containing {{ input }} is passed through untouched.

An environment variable that is set but empty is legitimate and renders as an empty string. One that is unset is an error.

Partials

A fragment shared by several commands, such as a style guide, a glossary or a paragraph on JSON discipline, lives in its own file and is declared by each command that uses it:

[partials]
style = "style-guide"            # bare name: <scope root>/partials/style-guide.md
glossary = "shared/glossary.md"  # a path relative to the scope root, or an absolute one

{{ partials.style }} then inserts the file's text, in the body, in system or in an example. The rules are those of [schemas]:

gko describe lists a command's partials with their resolved paths, and --dry-run shows the rendered request with the partials inserted.

Two deliberate constraints

[!WARNING] A closed placeholder that is not recognised is an error at load time, never copied through verbatim. A misspelled {{ args.langauge }} would otherwise reach the model as literal text, and the model would answer something plausible. That is the most expensive failure mode available here, because it is invisible.

The consequence is that a prompt cannot contain {{ foo }} as literal text. An unclosed {{ is left alone, since nothing can distinguish intent from a typo there.

[!WARNING] An argument referenced by the prompt must be required = true. The prompt cannot be rendered without it, so declaring it optional is rejected at load time, naming the file — which also lets gko describe and gko doctor see the file is broken without running it.


System prompt and examples

---
description = "Classify a support ticket"
model = "qwen-fast"
system = "You are a deterministic classifier. Answer with JSON only."

[[examples]]
user = "ticket: printer on fire"
assistant = '{"category":"hardware","confidence":0.98}'
---
Classify: {{ input }}

system (optional string) and [[examples]] (optional array of { user, assistant } pairs) give the model a fixed system instruction and a few fixed demonstrations, which steer the shape of its answer more reliably than a single free-text prompt.

The request sent to the backend becomes, in this order: the system message (if declared), each example's user/assistant pair (in file order), then the rendered body as the final user message. A command declaring neither key sends a single user message.

Both system and every example field are templated with the same placeholders as the body ({{ args.* }}, {{ env.* }}, {{ schemas.* }}, {{ partials.* }}), with one exception: {{ input }} is rejected there at load time. The input is the user's own turn, rendered separately as the last message — referencing it from system or an example would not mean what it looks like it means.

An argument referenced only from system or an example is held to the same rule as one referenced from the body: it must be declared required = true (see above). An environment variable they reference is resolved at the same preflight step as the body's own placeholders — before the input is read.

system cannot be blank (empty after trimming), and every example needs both non-empty user and assistant fields; either is rejected at load time.

gko describe reports the raw system template (never resolved — describe documents the file, it does not run it) and the count of declared examples, never their content.


What is validated, and when

Everything knowable without the input is checked before the input is read.

resolve command → collect arguments → check placeholders resolve
                → read input → render prompt → call backend → apply output contract

In a pipeline, git diff | gko commit-message therefore fails on an unset environment variable before it consumes the diff.

Checked at load (exit 2) Checked before reading input (exit 2) Checked at runtime
frontmatter syntax, unknown keys declared arguments are present backend reachability (exit 3)
model exists, resolves to a backend operation {{ env.* }} variables are defined output contract (exit 4)
argument names, short letters
every placeholder is recognised and declared
reserved command names, verbose/-v collisions