.rotini.spec.yaml
The spec describes your CLI: its commands, their inputs and what they write. The document holds a top-level version and one root command (the program itself), and every sub-command below it has the same shape. env_prefix and schemas are valid only on the root command, and $schema is an optional key for editor tooling.
every key
# Every key the spec accepts, in one valid file. `rotini validate` checks it on every test run.
$schema: ./.rotini-schema.spec.json
version: 1.3.0
command:
name: deploy
display_name: acme deploy
summary: ship services to environments
description: Deploy builds, ships and rolls back services.
usage: deploy [flags] <command>
examples:
- deploy ship api --env prod
groups:
- name: Deploying
description: Commands that change what runs where.
- name: Output
header: deploy — internal tooling
footer: Use "deploy help <command>" for more about a command.
headings:
usage: "Usage:"
commands: "Commands:"
arguments: "Arguments:"
flags: "Flags:"
cascading: "Global Flags:"
environment: "Environment:"
configuration: "Configuration:"
stdin: "Stdin:"
output: "Output:"
examples: "Examples:"
topics: "Help Topics:"
topics:
- name: environment
summary: the environment variables deploy reads
generate: environment
- name: targets
summary: how targets are written
body: A target is a host and an optional port.
see_also: [kubectl, helm]
exit_status:
- code: 0
summary: success
- code: 1
summary: the deploy failed
- name: partial
code: 3
summary: some targets failed; stdout lists the ones that shipped
output: { type: array, items: { $ref: "#/schemas/Target" } }
retryable: true
docs_url: https://example.com/deploy/partial
env_prefix: DEPLOY
schemas:
Target:
type: object
properties:
host: { type: string, pattern: '^[a-z0-9.-]+$', pattern_message: must be a lowercase hostname }
port: { type: integer, minimum: 1, maximum: 65535 }
tags: { type: array, items: { type: string }, minItems: 1, maxItems: 8 }
description: Where a service is shipped.
required: [host]
Region:
type: string
enum: [eu, us, ap]
Rollout:
type: object
properties:
kind: { type: string, const: result }
version: { type: string }
description: How a rollout ended.
required: [kind, version]
flag_sets:
Paging:
summary: how long listings are paged
group: Output
flags:
- name: limit
identifiers: [--limit]
summary: at most this many rows
schema: { type: int, minimum: 1 }
- name: all
identifiers: [--all]
summary: every row
schema: { type: bool }
flag_groups:
- kind: mutually_exclusive
flags: [limit, all]
flag_dependencies:
- when: all
forbids: [limit]
flags:
- name: help
identifiers: [-h, --help]
summary: print help
cascading: true
short_circuit: true
schema: { type: bool }
- name: config
identifiers: [--config]
summary: a config file to read instead of discovering one
schema: { type: existingfile, config_source: project }
- name: profile
identifiers: [--profile]
summary: the profile of the user config file to use
cascading: true
schema: { type: string, variable: DEPLOY_PROFILE }
- name: verbose
identifiers: [-v, --verbose]
summary: more output (repeat for more)
group: Output
cascading: true
schema: { type: count }
env:
- name: token
summary: the API token
schema: { type: string, variable: DEPLOY_TOKEN, variable_file: DEPLOY_TOKEN_FILE, secret: true, required: false }
- name: labels
summary: extra labels, as DEPLOY_LABELS__<NAME>=value
schema: { type: map, nesting: __ }
config:
- name: region
summary: the default region
schema: { $ref: Region, key: defaults.region, default: eu, ignore_case: true }
- name: digest
summary: the pinned image digest
schema: { type: string, file: pinned, key: digest }
- name: cache
summary: where builds are cached; relative to the file that sets it
schema: { type: string, key: cache_dir, default: ~/.cache/deploy, expand: [home], relative_to: config }
config_files:
- name: project
format: yaml
discover: { strategy: walk-up, file: .deploy.yaml }
- name: user
discover: { strategy: xdg, app: deploy, file: config.yaml }
profiles: { under: profiles, select: profile, default: default }
- name: dotenv
path: .env
format: dotenv
as: env
- name: pinned
path: ./deploy.lock.yaml
schema:
type: object
properties:
digest: { type: string }
response_files: { prefix: "+" }
commands:
- name: ship
aliases: [s, push]
deprecated_identifiers: [push]
deprecated_identifiers_removed_in: { push: 2.0.0 }
summary: ship a service
group: Deploying
deprecated: use ship
effects: { kind: write, idempotent: true, open_world: true }
metadata: { owner: platform-team }
flags:
- name: env
identifiers: [-e, --env]
summary: the environment
description: The environment to ship to. Each one has its own cluster and credentials.
stability: beta
metadata: { audit: "true" }
schema: { type: string, enum: [dev, staging, prod], default: dev, default_text: the dev environment, repeatable: false, complete: { kind: none, message: "one of dev, staging or prod" } }
- name: target
identifiers: [--target]
summary: where to ship
schema: { $ref: Target, from: [file] }
- name: color
identifiers: [--color]
summary: color output
schema: { type: string, implicit_value: always, enum: [always, never, { value: auto, summary: color when stdout is a terminal, aliases: [tty] }] }
- name: cache
identifiers: [--cache]
summary: use the build cache
schema: { type: bool, negatable: true, default: true }
- name: tag
identifiers: [--tag]
summary: image tags
schema: { type: '[]string', separator: ',', minItems: 1, uniqueItems: true }
- name: set
identifiers: [--set]
summary: override values (a.b=c)
schema: { type: map, dotted_keys: true, properties: { replicas: { type: integer } } }
- name: timeout
identifiers: [--timeout]
summary: how long to wait
schema: { type: duration, minimum: 1s, maximum: 1h, default: 5m }
- name: when
identifiers: [--when]
summary: when to ship
schema: { type: date, layout: '2006-01-02', nullable: true }
- name: since
identifiers: [--since]
summary: ship changes made since this time
schema: { type: time, layout: ['2006-01-02 15:04', '2006-01-02'], relative: past }
- name: replicas
identifiers: [--replicas]
summary: how many replicas
schema: { type: int, exclusiveMinimum: 0, exclusiveMaximum: 100, multipleOf: 1 }
- name: mode
identifiers: [--mode]
summary: file mode for what it writes
schema: { type: uint32, base: prefixed, default: '0o644', maximum: 0o777 }
- name: id
identifiers: [--id]
summary: a request id
schema: { type: uuid.UUID, import: github.com/google/uuid }
- name: force
identifiers: [--force]
deprecated_identifiers: [--force]
summary: skip safety checks
hidden: true
deprecated: no longer needed
deprecated_since: 1.2.0
removed_in: 2.0.0
schema: { type: bool }
- name: dry-run
identifiers: [--dry-run]
summary: print instead of shipping
role: dry-run
schema: { type: bool }
- name: plan
identifiers: [--plan]
summary: write the plan to this file
schema: { type: string, complete: { kind: file, extensions: [json] } }
- name: replace
identifiers: [--replace]
summary: replace an existing plan file
role: force
schema: { type: bool }
- name: prune
identifiers: [--prune]
summary: also delete old releases
effects: { kind: destructive }
schema: { type: bool }
arguments:
- name: service
summary: the service to ship
schema: { type: string, required: true, minLength: 1, maxLength: 63, placeholder: SERVICE }
- name: extra
summary: extra build arguments
hidden: true
schema: { type: '[]string', glob: true }
flag_groups:
- kind: mutually_exclusive
flags: [dry-run, force]
flag_dependencies:
- when: plan
requires: [dry-run]
output:
type: object
properties:
service: { type: string }
url: { type: string }
filename: ship.go
- name: apply
summary: apply a manifest read from stdin
stdin:
schema:
type: object
properties:
service: { type: string }
required: true
format: yaml
- name: grep
summary: print the lines of a log that match a pattern
arguments:
- name: pattern
summary: the text to look for
schema: { type: string, required: true }
- name: files
summary: the logs to read (stdin when none is given)
schema: { type: '[]inputfile' }
stdin:
format: lines
stream: true
unless_argument: files
- name: logs
summary: stream a service's logs
flags:
- name: fields
identifiers: [--fields]
summary: write only these fields
role: fields
schema: { type: '[]string', separator: ',', values_from: output }
- name: format
identifiers: [--format]
summary: the output format
role: machine-output
role_value: json
agent: false
schema: { type: string, enum: [text, json], default: text }
output:
type: object
properties:
line: { type: string }
service: { type: string }
output_stream: true
handler:
import: github.com/example/deploy/internal/logs
convention: Logs
- name: rollout
summary: roll a service out, reporting progress as it goes
output:
oneOf:
- type: object
properties:
kind: { type: string, const: progress }
percent: { type: integer, minimum: 0, maximum: 100 }
required: [kind, percent]
- $ref: "#/schemas/Rollout"
output_stream: true
- name: exec
summary: run a command in a service
agent: false
arguments:
- name: argv
summary: the command and its arguments
schema: { type: '[]string' }
passthrough: true
- name: run
summary: run a one-off task in a service
flags:
- name: ipv4
identifiers: ['-4', --ipv4]
summary: connect over IPv4
schema: { type: bool }
arguments:
- name: service
summary: the service to run in
schema: { type: string, required: true }
- name: command
summary: the command to run, and its arguments
passthrough: true
schema: { type: '[]string', complete: { kind: executable } }
options_first: true
- name: help
summary: show help for a command
arguments:
- name: command
summary: the command to show help for
schema: { type: '[]string', complete: { kind: command } }
- name: status
summary: show what is deployed
help: |
status — show what is deployed.
This page is written by hand, verbatim.
man: |
.TH STATUS 1
markdown: |
# status
- name: history
hidden_aliases: [hist]
summary: list a service's releases
use: [Paging]
flags:
- name: format
identifiers: [--format]
hidden_identifiers: [--output-format]
summary: how to print the list
schema: { type: string, enum: [text, csv], default: text }
- name: delimiter
identifiers: [--delimiter]
summary: the csv field separator
schema: { type: string }
- name: since-release
identifiers: [--since-release]
summary: only releases after this one
schema: { type: string }
- name: after
identifiers: [--after]
summary: only releases after this one
deprecated: renamed
replaced_by: --since-release
schema: { type: string }
flag_dependencies:
- when: format
requires: [delimiter]
equals: [csv]
- requires: [limit]
unless: [all]
- name: releases
summary: list a service's releases
deprecated: renamed to history
replaced_by: history
plugins:
- name: audit
aliases: [au]
summary: run the audit plugin
timeout: 30s
plugin_discovery:
prefix: deploy-
hidden: false
plugin_path: ~/.deploy/plugins
multicall: { prefix: "dp-" }the JSON Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Rotini Spec Schema",
"description": "The spec describes your CLI: its commands, their inputs and what they write. The document holds a top-level `version` and one root `command` (the program itself), and every sub-command below it has the same shape. `env_prefix` and `schemas` are valid only on the root command, and `$schema` is an optional key for editor tooling.",
"type": "object",
"additionalProperties": false,
"required": [
"version",
"command"
],
"properties": {
"version": {
"type": "string",
"description": "The minimum rotini version this spec requires (X.Y.Z): the feature set it was written against, not an exact pin. Any rotini of the same major version at or beyond it accepts the document, so a patch or minor upgrade never requires an edit here. Two cases are errors: a rotini older than this, which may not know keys the spec uses, and a different major version. The check is skipped for a development build of rotini, which reports no release version (0.0.0, or none at all). This key, not the optional `$schema` URL, is what the check reads.",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["1.0.0"]
},
"$schema": {
"type": "string",
"description": "Optional URI identifying the rotini spec schema, for editor tooling only: rotini never fetches it, and the version check reads the top-level `version` key, not this. Any URI is accepted: a released schema (https://raw.githubusercontent.com/go-rotini/rotini/refs/tags/v1.3.0/schema-spec.json \u2014 note the 'v', matching the git tag), a path written into your project by the conf's `generate.schemas.spec.file`, or a fork's own URL. A relative path is resolved by your editor, not by rotini. `rotini init` seeds this key (`$schema: ./.rotini-schema.spec.json`), and it works in every format; YAML editors also accept a `# yaml-language-server: $schema=<path>` comment in its place."
},
"command": {
"$ref": "#/definitions/Command",
"description": "The CLI's root command (the binary itself): its name, doc-fields, inputs (flags/arguments/env/config/config_files/stdin) and sub-commands. The root must use 'name' (not '$ref'). The root-command-level keys `env_prefix` and `schemas` live here."
}
},
"definitions": {
"Command": {
"description": "A command node in the CLI command tree \u2014 the root command (under the document's 'command' key) and every sub-command share this recursive shape. Declared inline (with 'name') or composed from another spec file (with '$ref'). The root must use 'name' (not '$ref'). The two root-command-level keys (env_prefix, schemas) are accepted on this shape but are valid only on the root command \u2014 rotini validation rejects them on a sub-command.",
"type": "object",
"anyOf": [
{
"required": [
"name"
]
},
{
"required": [
"$ref"
]
}
],
"additionalProperties": false,
"properties": {
"env_prefix": {
"type": "string",
"pattern": "^[A-Z][A-Z0-9_]*[A-Z0-9]$|^[A-Z]$", "examples": ["APP", "MY_TOOL"],
"description": "Root only: a prefix for every environment-variable name rotini derives. Derived names are the UPPER_SNAKE forms of plain env inputs without 'variable:' (input 'home' \u2192 ACME_HOME), of nested env families without 'variable:' (the family's base name), and of flags' environment fallbacks (key 'server.port' \u2192 ACME_SERVER_PORT). A name set explicitly with 'variable:' is used exactly as written and is never prefixed. With a prefix declared, the unprefixed name does not bind: input 'home' reads ACME_HOME, not HOME. Write it in UPPER_SNAKE with no trailing underscore (rotini adds the '_'). The derived name is written into the generated field's `env:` tag when you generate, so the name is fixed in the code. Help lists env: inputs by name under its Environment section. Help also shows each flag's environment fallback, and its configuration key when the command reads configuration files, on a line under the flag.\n\nIn a composed CLI, a `$ref`'d child's env_prefix travels with its commands: a parent that declares none adopts the child's, a parent that declares one wins, and two children with different prefixes are rejected."
},
"schemas": {
"type": "object",
"description": "Document-level (root only): reusable named schema definitions. Referenced elsewhere by name, `$ref: <Name>`, or as a pointer, `$ref: \"#/schemas/<Name>\"`. Each may carry a `description`, which becomes the generated type's doc comment. Names must be PascalCase Go-exportable identifiers \u2014 each becomes a generated Go type in the cmd package (or in the models package, when the conf declares one), which other packages may import.",
"additionalProperties": {
"$ref": "#/definitions/Schema"
},
"propertyNames": {
"pattern": "^[A-Z][A-Za-z0-9_]*$", "examples": ["DB", "MountSpec"]
}
},
"name": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["deploy", "list-users"],
"description": "Command name used in routing. As the root command (the document itself) this is the binary name and must be set \u2014 the root cannot use '$ref'."
},
"$ref": {
"type": "string",
"description": "The spec file whose root command is mounted here as this sub-command. Not valid on the root command. Two forms are accepted:\n\n- A relative path, resolved against this spec file's directory (`$ref: ../db/.rotini.spec.yaml`).\n- `mod://<module>@<version>/<path>`, a spec inside another Go module (`$ref: mod://github.com/acme/db@v1.4.0/cmd/db/.rotini.spec.yaml`). The version is required, and <path> is the spec file's path inside that module. The module is read from the module cache with `go mod download`, so it is verified against go.sum; add it to go.mod first (`go get github.com/acme/db@v1.4.0`). A relative $ref inside that spec resolves within the same module and cannot leave it.\n\nGit and https URLs are not accepted. Either way, the mounted command uses the handlers of the composed spec's own generated package unless `handler:` names another.\n\nThe composed spec is the base, and the parent can adjust it where it is mounted:\n\n- Identity and presentation keys declared next to the $ref (name, aliases, summary, description, usage, header, footer, examples, headings, groups, help, man, markdown, exit_status, see_also, group, hidden, deprecated, deprecated_identifiers, filename, plugin_path, hidden_aliases, replaced_by, metadata) replace the child's, for that one mounted command only. The child's own sub-commands keep theirs, so a parent can tailor the child for its tree without forking it.\n- A 'commands:' list next to the $ref is added to the child's own sub-commands: its inline entries get their own handler files, and its $ref entries are mounted as further children.\n- `handler:` on a $ref command points it at a different handler package.\n- Keys the handler depends on (flags, arguments, env, config, config_files, stdin, flag_groups, flag_dependencies, use, output, plugins, plugin_discovery, passthrough, options_first) cannot be changed here: the mounted command runs the child's handler, built against the child's own inputs and output, so validation rejects them. Declare them in the child spec.\n\nThe child's own plugins, plugin_discovery, passthrough and options_first travel with it. `rotini validate` and `generate` on the parent also check every spec composed by a relative path as its own document, reporting problems at their position in that file. A spec composed with mod:// is not checked that way: it belongs to its own module, which validates it."
},
"aliases": {
"type": "array",
"description": "Additional names that invoke this command. Command aliases affect dispatch routing; use identifiers on flags for flag aliases. Sub-commands only: the root command is reached by invoking the binary (argv[0] is not a routing token), so rotini validation rejects aliases there.",
"items": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["ls", "rm"]
}
},
"deprecated_identifiers": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["remove"]
},
"description": "Aliases of this command that are deprecated (a subset of 'aliases'). When the command is invoked via one of these, rotini's Deprecations surfaces it for the handler to act on; invoking via the name or a non-listed alias is unaffected. Sub-commands only, like 'aliases' \u2014 rejected on the root by rotini validation."
},
"timeout": {
"type": "string",
"description": "Not supported on a local command and rejected by rotini validation: a timeout is a plugin-only, host-side bound on a dispatched binary, so it has no effect on local execution. Set it on a plugins[] entry's 'timeout' instead. (Recognized here only so validation can give that targeted error rather than a generic 'unknown property'.)",
"pattern": "^([0-9]+(\\.[0-9]+)?(ns|us|\u00b5s|ms|s|m|h))+$", "examples": ["30s", "1m30s"]
},
"flags": {
"type": "array",
"description": "Flag inputs for this command. Each applies only when this command is the one run, unless it is `cascading`.",
"items": {
"$ref": "#/definitions/FlagInput"
}
},
"arguments": {
"type": "array",
"description": "Positional argument inputs for this command",
"items": {
"$ref": "#/definitions/ArgumentInput"
}
},
"config_files": {
"type": "array",
"description": "Configuration files this command reads values from, each at a fixed 'path' or found with 'discover'. Files cascade: a command can read from the files declared on it and on every command above it, so a 'config' input here or on any sub-command may pin itself (schema 'file:') to a file declared here or on any ancestor. Only the files along the invoked command's path are loaded; files declared on other branches are never read. File names must be unique along a path (a collision is an error), and declaring the same physical file at two levels is a warning. When two files in scope define the same key, the one declared nearest the invoked command wins, and within one command's list the first declared wins, so list the more specific file (a project's) before the more general one (the user's). In a composed CLI, a `$ref`'d child's files travel with its commands, so `parent child cmd` reads what `child cmd` reads without the parent declaring anything.",
"items": {
"$ref": "#/definitions/ConfigurationFile"
}
},
"config": {
"type": "array",
"description": "Config-value inputs for this command, bound by key from an in-scope config_files source (declared on this command or any ancestor \u2014 see config_files).",
"items": {
"$ref": "#/definitions/ConfigInput"
}
},
"env": {
"type": "array",
"description": "Environment-variable inputs for this command",
"items": {
"$ref": "#/definitions/EnvInput"
}
},
"stdin": {
"$ref": "#/definitions/StdinSpec",
"description": "Declares expected stdin format and schema for this command"
},
"flag_groups": {
"type": "array",
"description": "Cross-flag presence rules validated at parse time (e.g. mutually exclusive output formats, a required-together credential pair). Groups that contradict each other or a flag dependency are rejected, as is a required flag in a mutually_exclusive group with no env or config fallback.",
"items": {
"$ref": "#/definitions/FlagGroup"
}
},
"flag_dependencies": {
"type": "array",
"description": "Conditional cross-flag requirements validated at parse time: when one flag is set, others become required (e.g. when --tls is set, --cert and --key are required).",
"items": {
"$ref": "#/definitions/FlagDependency"
}
},
"summary": {
"type": "string",
"description": "Short one-liner describing this command. It is shown next to the command in its parent's generated Commands list (so it applies even when a verbatim 'help' string is set), and it is also the NAME line of the man page, the opening line of the markdown page, and the lead of the command's own help page when no 'description' is set."
},
"description": {
"type": "string",
"description": "Long description block shown atop this command's generated help page. Ignored when 'help' (verbatim) is set."
},
"usage": {
"type": "string",
"description": "Usage-line override. When omitted, rotini derives `<command path> [flags] <command> <arguments>` from the command's shape. Ignored when 'help' is set."
},
"display_name": {
"type": "string",
"pattern": "^[^\\s](.*[^\\s])?$", "examples": ["kubectl ctx", "docker scan"],
"description": "Root only: the name the generated help, man and markdown pages show for the program, in place of the root's 'name'. For a plugin, which a host runs as `<host>-<name>` but the user types as `<host> <name>`: with name: kubectl-ctx and display_name: \"kubectl ctx\", every derived usage line reads `kubectl ctx use [flags] <name>`, the man page's SYNOPSIS `kubectl ctx use …` and the markdown title `# kubectl ctx use`. It may contain spaces. It changes presentation only: 'name' still matches the binary and names the generated page files and man pages (kubectl-ctx-use.1), completion scripts still register for 'name' (the shell completes the binary), and routing, handler names and Context.CommandPath() are unaffected. Text the author writes verbatim (usage, footer, examples, a verbatim help/man/markdown page) is not rewritten. A composed child's display_name is ignored; the composing parent's root decides."
},
"header": {
"type": "string",
"description": "Text rendered above the description block. Ignored when 'help' is set."
},
"footer": {
"type": "string",
"description": "Text rendered at the bottom of the page. Ignored when 'help' is set."
},
"headings": {
"$ref": "#/definitions/HelpHeadings",
"description": "Section heading overrides for the generated page; sane defaults fill any unset heading. Ignored when 'help' is set."
},
"examples": {
"type": "array",
"description": "Example command-line invocations, rendered one per line. Ignored when 'help' is set.",
"items": {
"type": "string"
}
},
"help": {
"type": "string",
"description": "Exact, verbatim help page for this command. When set, rotini writes it byte-for-byte (no rendering; terminal styling kept) and ignores the structured help fields (description/usage/header/footer/examples/headings); 'summary' is still used in the parent's Commands list. When unset, rotini generates the page from the structured fields."
},
"man": {
"type": "string",
"description": "Exact, verbatim man page for this command, written in roff, the markup the man program reads (the man feature's per-command escape, mirroring 'help'). When set, rotini writes it as given \u2014 byte-for-byte except that ANSI styling is removed, since a man page carries none \u2014 and ignores the structured doc-fields for the man page; when unset, the page is rendered as roff from those fields through the man template. Either way the page is named after the command path joined with '-' and lowercased, with the man section as its extension (deploy-status.1)."
},
"output": {
"$ref": "#/definitions/Schema",
"description": "The shape of what this command writes to stdout when it succeeds, as a schema. Rotini generates a typed '<Prefix>Output' Go type (when the shape is a '$ref' to a document-level schema, a new named type defined on that schema's type, such as 'type <Prefix>Output Task', so convert a value with <Prefix>Output(v)), documents the shape in an OUTPUT section of the help, man and markdown pages, and describes it in the output schema files and the contract document. It describes the shape only: it adds no flag and wires no format. How the output is written, and in which format, is the handler's own code — rtx.WriteOutput is an optional helper that writes json, yaml or toml, hands any other format to a renderer, and checks the value is this type. A command that writes a stream of items declares the shape of one item, and a stream whose items come in several kinds declares them with `oneOf`."
},
"hidden": {
"type": "boolean",
"default": false,
"description": "When true, the command is omitted from its parent's generated Commands list (it still dispatches on the command line)."
},
"group": {
"type": "string",
"description": "Group label for organizing this command under a heading in its parent's generated Commands list. Commands sharing a group are bucketed together, and ungrouped commands fall under the default Commands heading. Groups appear in the order their first member is declared, unless the parent declares `groups`: then the ungrouped commands come first, then the groups it lists, in its order, then any others. Presentation-only."
},
"groups": {
"type": "array",
"description": "Describes the group headings on this command's own pages: the groups its sub-commands' `group` and its flags' `group` name. Each entry gives a group a description, shown under its heading in help, man and markdown, and the list's order is the order the groups appear. A label used by both a sub-command and a flag shares one entry. Presentation only, and not the same as 'flag_groups', which validates combinations of flags. `rotini validate` warns about an entry no sub-command or flag uses.",
"items": {
"$ref": "#/definitions/HelpGroup"
}
},
"deprecated": {
"type": "string",
"description": "Deprecation message. The command is annotated as deprecated in its parent's generated Commands list, and invoking it is reported at run time by rotini.Deprecations with this message (a data feed for the handler; rotini itself prints nothing). With `deprecated_identifiers`, only those aliases report; without, every name does."
},
"deprecated_since": {
"type": "string",
"description": "The release that deprecated this command (X.Y.Z), shown beside the deprecation in help, man and markdown, written into the contract, and reported by rotini.Deprecations as Since. Needs `deprecated`.",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["1.4.0"]
},
"removed_in": {
"type": "string",
"description": "The release that will remove this command (X.Y.Z), later than `deprecated_since`. Shown like `deprecated_since` and reported as RemovedIn. `rotini validate --release <X.Y.Z>` (or the variable the conf's `validate.release_env` names) fails while the command is still declared at or past that release, so a planned removal is not forgotten. Needs `deprecated`.",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["2.0.0"]
},
"deprecated_identifiers_removed_in": {
"type": "object",
"description": "The release that removes each deprecated alias, by alias: `{remove: 2.0.0}` plans the removal of the alias `remove` while the command stays. Each key must be listed in `deprecated_identifiers`. `rotini validate --release` checks these like `removed_in`, and the contract carries them.",
"additionalProperties": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["2.0.0"]
}
},
"output_stream": {
"type": "boolean",
"default": false,
"description": "When true, the command writes a stream of items rather than one value, and `output:` declares one item. Write each item with rtx.WriteOutputItem: JSON is one compact value per line, YAML one document per item, each starting `---`. Help, man and markdown say so in the OUTPUT section, and the contract marks the command `stream: true`. Needs `output`. `rotini generate` warns when a handler calls WriteOutputItem on a command without it. When the items come in several kinds (progress updates, then a result), `output:` lists one schema per kind under `oneOf`, each tagged by the same required string property holding a `const` (`kind: {type: string, const: progress}`); see `oneOf`."
},
"filename": {
"type": "string",
"description": "Override the name of this command's generated handler-stub .go file (in the cli package). Defaults to a name derived from the command path ('<root>_<path>.go', every '-' written '_': config_get_contexts.go), reserved-name-escaped so a command named 'test'/'<GOOS>'/'<GOARCH>' does not collide with Go's filename rules. Must end in '.go', must not itself be a name Go reads specially ('_test.go', '_<GOOS>.go', '_<GOARCH>.go'), and must be unique among the commands generated into the same package. Renaming it orphans (and prunes) the previous stub file \u2014 move your handler code first.",
"pattern": "^[^/\\\\]+\\.go$", "examples": ["deploy.go"]
},
"commands": {
"type": "array",
"description": "Sub-commands of this command, declared inline or composed with $ref. On a $ref command these are added to the composed child's own sub-commands (see '$ref'); a name or alias that collides across the combined list is an error.",
"items": {
"$ref": "#/definitions/Command"
}
},
"plugin_path": {
"type": "string",
"description": "Extra directory to search for this command's plugin binaries, in addition to the host binary's own directory and PATH. A relative path is resolved against the run's directory; $VAR references and then a leading ~ are expanded when the program runs, and a directory that does not exist yet is simply empty. On a `$ref` node it overrides the composed child's own. It applies to BOTH kinds of plugin: the 'plugins' this spec declares and anything 'plugin_discovery' finds \u2014 they are the same binaries in the same place, so they are configured once here rather than per-mechanism. Without it, a declared plugin could only ever be installed next to the host binary or on PATH, which is the git/kubectl convention and not always the right one for a vendored or bundled plugin. Search order is fixed and the same for both: next to the host binary, then this directory, then PATH \u2014 so a plugin shipped beside the binary always wins over one found here, and a failure names the locations it actually searched."
},
"plugins": {
"type": "array",
"description": "Declared plugins: separate executables dispatched as first-class sub-commands of this command.",
"items": {
"$ref": "#/definitions/PluginSpec"
}
},
"plugin_discovery": {
"$ref": "#/definitions/PluginDiscovery",
"description": "Auto-expose external '<prefix>*' executables as plugin sub-commands of this command (kubectl/git/gh plugin discovery), in addition to any declared plugins. Presence enables discovery."
},
"exit_status": {
"type": "array",
"description": "Exit codes this command documents, rendered as an EXIT STATUS section in the man and markdown pages. The runtime does not check it, and sets no exit code of its own except two. A recorded error or a recovered panic exits 1 when no handler set a code, and the default signal handling exits 128+n on signal n (130 for Ctrl-C). So a command that documents `2: invalid input` here and only calls rtx.RecordError will exit 1. Set the code in the handler with rtx.Exit or rtx.HaltWithCode to make the program agree with this section. `rotini generate` warns when a command's handler sets an exit code (a literal or constant passed to rtx.HaltWithCode or rtx.Exit) that this list doesn't include, and `rotini validate` reports a code listed twice and warns about a code above 128, which a signal exit also uses. Code 0 needs no entry, and a command that prints its help when called without a sub-command exits 1, so list 1 for it. Ignored when 'man' (verbatim) is set.",
"items": {
"$ref": "#/definitions/ExitStatusEntry"
}
},
"see_also": {
"type": "array",
"description": "Cross-references rendered as a SEE ALSO section in the man page (e.g. related commands or man pages like 'rotini-generate(1)', or URLs). Ignored when 'man' (verbatim) is set.",
"items": {
"type": "string"
}
},
"passthrough": {
"type": "boolean",
"description": "When true, every token after this command's own name binds as a raw positional \u2014 no flag parsing, no unknown-flag errors, no '--' needed (the wrapper-CLI case: `mytool exec ls -la` forwards '-la' verbatim, and a literal '--' passes through too). Tokens BEFORE the command (ancestor flags) parse normally. A passthrough command declares no flags, no sub-commands, no declared plugins or discovery, and its last argument must be a variadic '[]string' \u2014 the receiver of the raw tokens (validation enforces all of this). Shell completion offers nothing past the boundary, falling back to file completion. To parse the command's own flags first and take the raw words only from one argument on, mark that argument `passthrough: true` instead."
},
"options_first": {
"type": "boolean",
"default": false,
"description": "When true, flags must come before this command's first argument: from that word on, every word is an argument, including flag-shaped words and a later `--`, as POSIX utilities parse (`ssh host -v` passes `-v` on). Words before the command's name (ancestor flags) and a `--` typed before the first argument work as usual, and a short-circuit flag such as `--help` after the first argument is an argument too. It applies when this command is the one invoked; sub-commands don't inherit it. A command whose last argument is a passthrough argument already stops at that argument. Can't be combined with `passthrough: true`, which parses no flags at all."
},
"response_files": {
"$ref": "#/definitions/ResponseFiles",
"description": "Root only: read response files. A word starting with the prefix (`@args.rsp`) is replaced by the file's lines, one argument per line, before the command line is parsed."
},
"markdown": {
"type": "string",
"description": "Exact, verbatim markdown reference page for this command (the markdown feature's per-command escape, mirroring 'help'/'man'). When set, rotini writes it as given \u2014 byte-for-byte except that ANSI styling is removed; when unset, the page is rendered from the structured doc-fields through the markdown template."
},
"handler": {
"$ref": "#/definitions/HandlerSource",
"description": "Use a handler from another Go package for this command instead of a generated handler file. Valid on any sub-command, not the root.\n\nOn a '$ref' command it replaces the default: a composed local or mod:// spec normally uses the handlers of its own generated package, and this points the command at a different package. On an inline command, the command's tree and typed inputs are still generated here, but its handler comes from the package and no handler file is written. It applies to this command only: an inline sub-command without its own 'handler:' still gets a generated handler file.\n\nThe package must export a constructor '<convention>() rotini.Handler' for each command it serves (the usual five-hook handler; hooks it does not implement default to no-ops), and the generated code calls 'pkg.<Convention>()'. The compiler enforces this, since rotini cannot type-check another package."
},
"flag_sets": {
"type": "object",
"description": "Root only: flags declared once, by set name, for commands to add with `use:`. A command that uses a set gets copies of its flags after its own, in `use` order, with its flag groups and dependencies; from then on they are ordinary flags of that command, for parsing, help, completion and the contract. The generated code has one struct per set, embedded in each using command's flags struct, so a handler reads `in.TaskrList.Flags.Format` whether the flag is its own or from a set.\n\nA set lives in the spec that declares it: a composed child uses its own sets, never its parent's.",
"propertyNames": {
"pattern": "^[A-Z][A-Za-z0-9]*$", "examples": ["Output", "Paging"]
},
"x-hint": "set names are PascalCase (Output, Paging), since each names a generated struct",
"additionalProperties": {
"$ref": "#/definitions/FlagSet"
}
},
"use": {
"type": "array",
"description": "The `flag_sets` this command adds, by name, in order. Each set's flags come after the command's own. A set name and a flag of the command (or of another set it uses) can't declare the same flag name, identifier or generated field name. Not valid next to `$ref`.",
"items": {
"type": "string",
"pattern": "^[A-Z][A-Za-z0-9]*$", "examples": ["Output"]
},
"uniqueItems": true
},
"hidden_aliases": {
"type": "array",
"description": "Names that invoke this command but are never listed: help, completion, suggestions and the program's other pages show only `name` and `aliases`. Use them for old spellings kept for scripts, or for shorthands not worth documenting. The help resolvers accept them as the command line does. Sub-commands only, like `aliases`; a hidden alias can't repeat the name, an alias, or a sibling's name. May be listed in `deprecated_identifiers`.",
"items": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["rm"]
},
"minItems": 1,
"uniqueItems": true
},
"replaced_by": {
"type": "string",
"minLength": 1,
"description": "With `deprecated`: the command to use instead, as its path below this spec's root command (`purge`, `remote add`), without the program's name. Help, man and markdown add `use <program> purge instead` to the deprecation note, written with the name the program runs as, and rotini.Deprecations reports it as ReplacedBy. `rotini validate` checks that the command exists and isn't itself deprecated.",
"examples": ["purge", "remote add"]
},
"multicall": {
"oneOf": [
{ "type": "boolean" },
{ "$ref": "#/definitions/Multicall" }
],
"description": "Root only: dispatch on the name the binary was invoked as, so one binary serves several names through links or copies. `true` is busybox style: a binary named after a top-level command (a symlink `ls` to the binary) runs it, as if the user had typed `<root> ls`. The object form sets a prefix to strip first, a prefix that answers shell completion, or both. The name is the base name the binary was run as, never a resolved symlink; on Windows a trailing .exe is dropped and names match without regard to case. A name that matches no command, the root's own included, runs the root as usual. Help pages still show the root's invocation (`busybox ls`)."
},
"stability": {
"type": "string",
"enum": ["experimental", "beta"],
"description": "How settled this command is. 'experimental': it may change or be removed in any release. 'beta': it may change in a minor release. Unset means stable. Help, man and markdown show (experimental) or (beta) beside it in the parent's command list, and a line on its own pages; the contract carries it. Its inputs, sub-commands, output and exit codes are as unstable as it is. `rotini diff` rates any change to an experimental item as safe, and a breaking change to a beta item as possibly breaking. Nothing changes at run time: to gate an experimental command, check in its handler."
},
"topics": {
"type": "array",
"description": "Root only: help topics, pages that aren't commands. `<program> help <topic>` shows one through the generated Help function (as the seeded help command does), the root's help lists them, the man and markdown features write a page for each, and `help <TAB>` completes their names along with the commands'. A topic's name can't be a root command's name or alias.",
"items": {
"$ref": "#/definitions/Topic"
}
},
"effects": {
"$ref": "#/definitions/Effects",
"description": "What running this command does, for the people and the programs (such as AI agents) that run it. Help, man and markdown show it as a line (`Effects: destructive, not idempotent`), and the contract, the generated Definition (CommandDef.Effects), tool exports, the agent skill page and permission snippets carry it. Unset says nothing, and a program reading the contract then assumes the worst: destructive, reaching outside the machine. Sub-commands don't inherit it. A flag's own `effects` raises it for a run that gives the flag. Not allowed on a `$ref` node: the child spec, whose handler runs, declares it. Nothing changes at run time."
},
"agent": {
"type": ["boolean", "null"],
"description": "Whether this command and its sub-commands are offered to AI agents: in tool exports, the agent skill page, llms.txt, permission snippets and the MCP companion. `false` leaves the subtree out, while it stays in help. `true` brings back a command left out by default: a hidden or deprecated one, one with sub-commands (which prints help when run bare), or a passthrough command (which would run whatever it is given). Unset follows those defaults. Nothing changes at run time."
},
"metadata": {
"type": "object",
"description": "Your own labels for this command, for your tools to read: string keys, each with a string value (`{owner: platform-team, tier: internal}`). Rotini never acts on them and shows them on no page (help, man, markdown, completion). It copies them as written into the generated Definition (CommandDef.Metadata, or Definition.Metadata on the root), where a handler reads them as rtx.Command().Metadata, and into the contract. `rotini diff` rates any change to them as safe. Sub-commands don't inherit them. Set next to a `$ref`, they replace the composed command's.",
"additionalProperties": {
"type": "string"
},
"propertyNames": {
"minLength": 1
}
}
}
},
"Multicall": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"prefix": {
"type": "string",
"minLength": 1,
"pattern": "^[^/\\\\]+$", "examples": ["acme-"],
"description": "Stripped from the invoked name before it is matched against the root's commands: with 'acme-', a binary run as acme-ls runs `ls`. A name without the prefix runs the root."
},
"complete": {
"type": "string",
"minLength": 1,
"pattern": "^[^/\\\\]+$", "examples": ["kubectl_complete-"],
"description": "An invoked name starting with this answers shell completion for the root instead of running it: the arguments are the words to complete, as kubectl passes them to a `kubectl_complete-<plugin>` executable. The answer is written in the format Program.WithCompletion sets, else rotini.PluginCompletion, the plugin hosts' format."
}
}
},
"HandlerSource": {
"type": "object",
"additionalProperties": false,
"required": [
"import",
"convention"
],
"description": "Where a command's handlers come from when they are not a generated stub: a Go package (handler delegation). The package's typed inputs live with it; this spec contributes only the command tree.",
"properties": {
"import": {
"type": "string",
"description": "Go import path of the handler package, in the same 'alias path' form an input type's 'import' uses (e.g. 'deployhandlers github.com/acme/clis/deploy/handlers'); identical imports are merged. A bare path takes its alias from the last path segment. The generated code calls '<alias>.<convention>()'.",
"pattern": "^[^\\s]+( [^\\s]+)?$", "examples": ["example.com/app/handlers", "h example.com/app/handlers"]
},
"convention": {
"type": "string",
"description": "Function-name prefix the package exports per command: codegen delegates this command to '<alias>.<convention>()' and each sub-command to '<alias>.<convention><SubPath>()', each returning a rotini.Handler. PascalCase Go-exportable identifier.",
"pattern": "^[A-Z][A-Za-z0-9_]*$", "examples": ["Deploy"]
}
}
},
"ResponseFiles": {
"type": "object",
"additionalProperties": false,
"required": [
"prefix"
],
"description": "Response files: arguments kept in a file and named on the command line. Before the first `--`, a word starting with the prefix names a file, read relative to the working directory, whose lines become arguments: one argument per line, kept exactly (spaces included), with a trailing carriage return removed. Empty and blank lines and lines starting with `#` are skipped. Words read from a file are not expanded again, and a `--` in a file ends expansion like one typed. To pass a word that starts with the prefix, double it: `@@x` is the argument `@x`. Words after `--` are never expanded. A file that can't be read is a usage error, even beside `--help`. Plugins and passthrough arguments receive the expanded words.",
"properties": {
"prefix": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"pattern": "^[^A-Za-z0-9\\s-]$",
"examples": ["@", "+"],
"description": "The one character that starts a response-file word, such as `@`. Not a letter, digit, `-` or space."
}
}
},
"PluginDiscovery": {
"type": "object",
"additionalProperties": false,
"description": "Auto-expose external '<prefix>*' executables as plugin sub-commands (kubectl/git/gh plugin style), alongside any declared plugins. Presence enables discovery; a discovered name that collides with a declared command or plugin is skipped.",
"properties": {
"prefix": {
"type": "string",
"description": "Executable-name prefix to discover. Default: the host binary name followed by '-' (e.g. 'acme-').",
"minLength": 1
},
"hidden": {
"type": "boolean",
"default": false,
"description": "When true, discovered plugins still dispatch but are omitted from completion listings."
}
}
},
"HelpHeadings": {
"type": "object",
"description": "Section heading overrides for generated help pages.",
"additionalProperties": false,
"properties": {
"usage": {
"type": "string",
"description": "Heading rendered above the usage section of the generated help page. Rendered verbatim \u2014 include any trailing ':' you want. Default: \"Usage:\"."
},
"commands": {
"type": "string",
"description": "Heading rendered above the commands section of the generated help page. Rendered verbatim \u2014 include any trailing ':' you want. Default: \"Commands:\"."
},
"arguments": {
"type": "string",
"description": "Heading rendered above the arguments section of the generated help page. Rendered verbatim \u2014 include any trailing ':' you want. Default: \"Arguments:\"."
},
"flags": {
"type": "string",
"description": "Heading rendered above the flags section of the generated help page. Rendered verbatim \u2014 include any trailing ':' you want. Default: \"Flags:\"."
},
"environment": {
"type": "string",
"description": "Heading rendered above the environment section of the generated help page. Rendered verbatim \u2014 include any trailing ':' you want. Default: \"Environment:\"."
},
"configuration": {
"type": "string",
"description": "Heading rendered above the configuration section of the generated help page. Rendered verbatim \u2014 include any trailing ':' you want. Default: \"Configuration:\"."
},
"cascading": {
"type": "string",
"description": "Heading rendered above the cascading-flags section on descendant commands' generated help pages. Rendered verbatim — include any trailing ':' you want. Default: \"Global Flags:\"."
},
"output": {
"type": "string",
"description": "Heading rendered above the Output section, which describes what the command writes when it declares `output:`. Rendered verbatim — include any trailing ':' you want. Default: \"Output:\"."
},
"stdin": {
"type": "string",
"description": "Heading rendered above the Stdin section, which describes what the command reads from standard input when it declares `stdin:`. Rendered verbatim \u2014 include any trailing ':' you want. Default: \"Stdin:\"."
},
"examples": {
"type": "string",
"description": "Heading rendered above the examples section of the generated help page. Rendered verbatim \u2014 include any trailing ':' you want. Default: \"Examples:\"."
},
"topics": {
"type": "string",
"description": "Heading rendered above the root page's list of help topics (`topics:`). Rendered verbatim \u2014 include any trailing ':' you want. Default: \"Help Topics:\"."
}
}
},
"BaseSchema": {
"type": "object",
"description": "Shared fields for Schema and InputSchema. JSON Schema Draft 7 cannot combine allOf inheritance with additionalProperties: false, so an unknown key inside a schema block is rejected by `rotini validate` itself rather than by the schema \u2014 with the same positioned \"unknown key\" message as anywhere else in the spec. A JSON Schema keyword rotini does not implement (format, title, \u2026) is rejected too, since it would otherwise do nothing. `description` is accepted on object schemas and their properties, where it becomes a Go doc comment.",
"properties": {
"$ref": {
"type": "string",
"pattern": "^(#/schemas/)?[A-Z][A-Za-z0-9_]*$", "examples": ["DB", "#/schemas/DB"],
"description": "Reference to a named schema in the root command's \"schemas\" map, by its name (`$ref: DB`) or as a JSON pointer (`$ref: '#/schemas/DB'`). The two mean the same; the pointer is what JSON Schema tooling reads. Resolved when you generate. A reference to a named scalar schema brings that schema's enum, pattern (and pattern_message), lengths and bounds with it, wherever the input leaves them unset.\n\nOn a flag, a named object schema makes the flag object-valued: its generated field is the schema's struct, and it takes any of:\n\n- JSON \u2014 `--db '{\"host\":\"h\",\"port\":5}'`\n- key=value pairs \u2014 `--db host=h,port=5`; dotted keys nest (`pool.max=9`), a repeated key appends to a list field, and quotes keep a comma (`host=\"a,b\"`)\n- a JSON or YAML file, with `from: [file]` \u2014 `--db @db.yaml`\n- one field per flag \u2014 `--db.host=h --db.port=5`\n\nOccurrences merge in the order they appear, a later key winning. Declared as `type: array, items: {$ref: \u2026}`, the flag is a list of objects and each occurrence is one element (per-field flags do not apply). A flag's environment fallback takes the same spellings (DB='{\"host\":\"h\"}'), and its configuration-file fallback is the object as a mapping (or a list of mappings).\n\nEvery spelling is validated against the named schema, with the same validator a stdin payload of that shape meets, and errors name the flag and the key (`--db: port: value must be >= 1`, `unknown key \"bogus\"`). A `default` is written as a mapping (a list of mappings for a list) and is checked against the schema by `rotini validate`; a supplied value replaces it rather than merging. Keys that shape a scalar value (enum, separator, ignore_case, implicit_value, negatable, dotted_keys, bounds, pattern) do not apply: an object's rules live in its schema. Arguments cannot be object-valued."
},
"type": {
"type": "string",
"description": "The type used to parse and store the value. Go type names (bool, int, float64, []string, duration, map) and JSON Schema names (boolean, integer, number, array, object) are equivalent. A bool accepts true/false, yes/no, on/off, y/n, t/f and 1/0, in any case, so `--cache=off` and CACHE=yes both work.\n\nValue types parse a kind of value and generate the matching Go field:\n\n- 'duration' \u2014 time.Duration; Go units plus 'd' days and 'w' weeks (7d, 2w3d)\n- 'time' / 'datetime' \u2014 time.Time, RFC 3339 (2026-09-29T14:00:00Z)\n- 'date' \u2014 time.Time, a calendar date (2026-09-29, that day's UTC midnight). All three time types take `layout:` for another format, Unix timestamps included, and `relative:` for times measured from now (2h, today)\n- 'url' \u2014 *url.URL; needs a scheme and host\n- 'email' \u2014 mail.Address; 'Name <a@b.c>' or a bare address\n- 'timezone' \u2014 *time.Location; an IANA name such as Europe/Berlin\n- 'mac' (net.HardwareAddr), 'ip' (netip.Addr), 'cidr' (netip.Prefix), 'hostport' (netip.AddrPort)\n- 'bytesize' \u2014 rotini.ByteSize; 512Mi, 10MB, 1.5GiB (an 'i' makes the unit binary)\n- 'hexbytes' \u2014 rotini.HexBytes; optional 0x\n- 'base64bytes' \u2014 rotini.Base64Bytes; standard or URL-safe, padded or not\n- 'regexp' \u2014 *regexp.Regexp, compiled when parsed; Go's RE2 syntax, which has no backreferences or lookaround\n- 'glob' \u2014 rotini.Glob, a path.Match pattern checked when parsed (`*`, `?`, `[a-z]`, `\\` escapes) with a Match method; `**` is not recursive. It keeps the pattern as a value; `glob: true` instead expands a pattern into file names\n- 'existingfile' / 'existingdir' \u2014 a plain string field, checked at parse time to exist and be that kind of thing, so a bad path is a usage error naming the flag the user typed. The check is existence and kind only: cleaning, following symlinks and creating a missing file are the handler's policy, and `~` and `$VAR` are expanded only when the input declares `expand`. A relative path is checked against the run's directory (Program.WithDir, by default the working directory) and bound as typed, so a handler opens it joined to rtx.Dir()\n- 'inputfile' \u2014 a plain string field, checked like 'existingfile', except that '-' means stdin: rotini.OpenInput(rtx, path) opens either (`cat a.txt - b.txt`). Only one '-' may be given per run, across every inputfile value. Unlike `from: [stdin]`, where '-' replaces the value with stdin's text, the value stays '-' and the handler reads the stream. Help adds '(- for stdin)'\n- 'outputfile' \u2014 a plain string field: a file to write, where '-' means stdout. Checked at parse time to be no directory and to sit in an existing directory; rotini.CreateOutput(rtx, path) writes it atomically and refuses to replace an existing file unless given rotini.Overwrite(true). Help adds '(- for stdout)'\n\nA value type works inside Go spellings too ('[]bytesize', 'map[string]duration'), and help shows the name as written ('--limit bytesize'), not the Go type. A lowercase name that is neither a Go builtin nor one of these is rejected as a typo, with a suggestion.\n\nShapes that change how a flag is written:\n\n- 'count' (flags only) \u2014 a presence counter: the flag takes no value, each occurrence increments the generated int field (-vvv \u2192 3, clustering included), an inline value (--verbose=3) is a parse error, and every value-shaped key (default, enum, constraints, from, key, \u2026) is rejected\n- '[]\u2026' / 'array' \u2014 repeatable (--tag a --tag b \u2192 a slice); 'items' declares the element type ('array' + items int \u2192 []int), defaulting to string\n- a map ('map[string]string', or 'map'/'object' \u2192 map[string]any) \u2014 repeatable too, taking 'key=value' pairs (--label k=v --label a=b; split on the first '='; the value coerced to the element type)\n\nAny other Go type (time.Time, uuid.UUID, your own): set 'import' to the backing package path. It must implement encoding.TextUnmarshaler \u2014 that method is its parser and validator, applied to each element of a list too. Rotini refuses a type without it at parse time with a loud error, never a silently zeroed field; validate cannot check it, since that would mean type-checking foreign packages.\n\nA NAMED OBJECT schema is different: a flag whose schema is `$ref: '#/schemas/DB'` (or `type: array, items: {$ref: '#/schemas/DB'}` for a list) takes a structured value \u2014 see `$ref`."
},
"import": {
"type": "string",
"description": "Optional Go import path backing 'type'. Set it when 'type' references a stdlib or third-party package whose name rotini does not already know (e.g. 'github.com/google/uuid' for uuid.UUID). Omit (or leave empty) for builtins and rotini's own type names (string, int, duration, url, ip, bytesize, \u2026) \u2014 codegen treats omitted/empty as 'no import'. The aliased form 'alias path' renames the import to avoid a clash (e.g. 'urlx github.com/me/url'). Codegen dedupes identical entries across the spec.",
"pattern": "^\\S+( \\S+)?$", "examples": ["github.com/google/uuid", "u github.com/google/uuid"]
},
"nullable": {
"type": "boolean",
"default": false,
"description": "Generate the field as a pointer (*T): nil means the input was not provided, distinguishable from its zero value. Defaults/values coerce through the pointer."
},
"enum": {
"type": "array",
"description": "Allowed values. The check applies to the final value, wherever it came from, so a flag value supplied by an environment variable or a config file is checked too. At least one member: an empty list would mean the same as no enum.\n\nMembers are strings, so quote a number (`enum: [\"1\", \"2\"]` on an int input). Each member is a value (`json`) or a value with a one-line summary saying what it means (`{value: yaml, summary: human-friendly}`); the two forms mix freely. Summaries are shown in help, man and markdown pages, offered as descriptions in shell completion, and carried in the contract's `enum_values`; JSON Schema output lists the values only. Summaries belong on an input's own schema: an output or stdin schema, and a named schema under `schemas`, take plain values.\n\nThe object form also takes `aliases` (other spellings accepted and bound as the value: `{value: yaml, aliases: [yml]}`), `hidden: true` (accepted, but left out of help's list, completion and error messages), and `deprecated` with a message, optionally `deprecated_since`, `removed_in` and `replaced_by` (another value). A deprecated value is still accepted and is left out of the same lists; `rotini.Deprecations` reports it when given on the command line, as it does `deprecated_aliases`, the aliases being retired. Help keeps its short list of the values to use; man and markdown pages list each value with its aliases and deprecation.",
"items": {
"x-hint": "must be a string (quote a number: \"1\") or an object ({value: \"1\", summary: one}) whose keys are value, summary, aliases, deprecated_aliases, hidden, deprecated, deprecated_since, removed_in and replaced_by",
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"additionalProperties": false,
"required": [
"value"
],
"properties": {
"value": {
"type": "string",
"description": "The value, as a user types it."
},
"summary": {
"type": "string",
"minLength": 1,
"description": "One line saying what the value means."
},
"aliases": {
"type": "array",
"items": {"type": "string", "minLength": 1},
"minItems": 1,
"uniqueItems": true,
"description": "Other spellings accepted for the value, each bound as the value itself. Never offered: help, completion and error messages list the value."
},
"deprecated_aliases": {
"type": "array",
"items": {"type": "string", "minLength": 1},
"minItems": 1,
"uniqueItems": true,
"description": "The aliases being retired: still accepted, and reported by rotini.Deprecations when given on the command line. Each must be one of `aliases`."
},
"hidden": {
"type": "boolean",
"description": "Accepted, but left out of help's list, completion and error messages."
},
"deprecated": {
"type": "string",
"minLength": 1,
"description": "The value's deprecation message. The value is still accepted, is left out of help's list, completion and error messages, and is reported by rotini.Deprecations when given on the command line."
},
"deprecated_since": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
"examples": ["1.4.0"],
"description": "With deprecated: the release that deprecated the value."
},
"removed_in": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
"examples": ["2.0.0"],
"description": "With deprecated: the release that will remove the value, after deprecated_since."
},
"replaced_by": {
"type": "string",
"minLength": 1,
"description": "With deprecated: the value to use instead, another value of this enum that is not deprecated."
}
}
}
]
},
"minItems": 1
},
"pattern": {
"type": "string",
"description": "Regular expression the value must match (string types only; for a list, each element). As in JSON Schema, the pattern matches anywhere in the value unless anchored: use ^\u2026$ for a full match. A failure shows the user the regex itself unless `pattern_message:` says it in words."
},
"pattern_message": {
"type": "string",
"minLength": 1,
"description": "With 'pattern' only: what the user is told when a value does not match, in place of the regex \u2014 which is written for the program, not the person typing. Phrase it to follow the input's name: `pattern_message: must be json, yaml, wide, name or custom-columns=<spec>` reports `-o must be json, yaml, wide, name or custom-columns=<spec> (got \"bogus\")`, where the default is `-o must match ^(json|yaml|\u2026)$ (got \"bogus\")`. Applies wherever the pattern is checked: an input on every channel it reads, each element of a list, a named schema an input refers to, and a property of an object-valued flag or a stdin payload.",
"examples": ["must be a DNS label: lowercase letters, digits and '-'"],
"$comment": "Enforced by lintPatternCompiles: requires pattern on the same schema."
},
"minimum": {
"type": [
"number",
"string",
"null"
],
"pattern": "^([0-9]*\\.?[0-9]+ ?[a-zA-Z\u00b5]+)+$",
"examples": ["30s", "1h30m", "512Mi"],
"x-hint": "a string bound is a duration (30s, 1h30m) or a size (512Mi, 10MB), for a duration or bytesize input; any other type takes a number",
"description": "Minimum allowed value (inclusive). Number types (the int, uint and float types and the integer and number aliases) take a number; a duration takes a duration (`minimum: 1s`) and a bytesize a size (`minimum: 1Mi`, or a number of bytes), read by the same parser as the value and printed back that way in errors (`must be >= 1s`). Rejected on any other type, where it would have no effect. For a repeatable input the bound applies to each element. minimum: 0 is a real, enforced bound. A number input with any bound (this, `maximum`, the exclusive bounds or `multipleOf`) rejects NaN and infinite values."
},
"maximum": {
"type": [
"number",
"string",
"null"
],
"pattern": "^([0-9]*\\.?[0-9]+ ?[a-zA-Z\u00b5]+)+$",
"examples": ["30s", "1h30m", "512Mi"],
"x-hint": "a string bound is a duration (30s, 1h30m) or a size (512Mi, 10MB), for a duration or bytesize input; any other type takes a number",
"description": "Maximum allowed value (inclusive). Same applicability rules as 'minimum' (numbers, durations and sizes, each in its own spelling; per-element for arrays; rejected elsewhere); maximum: 0 is a real, enforced bound. As with 'minimum', a bounded number rejects NaN and infinite values."
},
"exclusiveMinimum": {
"type": [
"number",
"string",
"null"
],
"pattern": "^([0-9]*\\.?[0-9]+ ?[a-zA-Z\u00b5]+)+$",
"examples": ["30s", "1h30m", "512Mi"],
"x-hint": "a string bound is a duration (30s, 1h30m) or a size (512Mi, 10MB), for a duration or bytesize input; any other type takes a number",
"description": "Exclusive lower bound: the value must be strictly greater. Same applicability rules as 'minimum' (numbers, durations and sizes; per-element for arrays; rejected elsewhere). exclusiveMinimum: 0 expresses \"positive\" exactly."
},
"exclusiveMaximum": {
"type": [
"number",
"string",
"null"
],
"pattern": "^([0-9]*\\.?[0-9]+ ?[a-zA-Z\u00b5]+)+$",
"examples": ["30s", "1h30m", "512Mi"],
"x-hint": "a string bound is a duration (30s, 1h30m) or a size (512Mi, 10MB), for a duration or bytesize input; any other type takes a number",
"description": "Exclusive upper bound: the value must be strictly less. Same applicability rules as 'maximum'."
},
"multipleOf": {
"type": [
"number",
"string",
"null"
],
"pattern": "^([0-9]*\\.?[0-9]+ ?[a-zA-Z\u00b5]+)+$",
"examples": ["30s", "1h30m", "512Mi"],
"x-hint": "a string bound is a duration (30s, 1h30m) or a size (512Mi, 10MB), for a duration or bytesize input; any other type takes a number",
"exclusiveMinimum": 0,
"description": "The value must be an integer multiple of this (JSON Schema semantics: the division yields an integer). Numbers, durations and sizes, in their own spelling (`multipleOf: 1s`); per-element for arrays; must be strictly positive."
},
"minLength": {
"type": "integer",
"minimum": 0,
"description": "Minimum string length in runes (string types only; for []string, each element) \u2014 rejected on non-string types."
},
"maxLength": {
"type": [
"integer",
"null"
],
"minimum": 0,
"description": "Maximum string length in runes (string types only; for []string, each element) \u2014 rejected on non-string types. maxLength: 0 is a real bound: only the empty string is accepted. A maxLength below minLength is rejected, since no value could pass."
},
"minItems": {
"type": "integer",
"minimum": 0,
"description": "Minimum number of values for a repeatable (array or map) input \u2014 rejected on scalar types. A list flag or variadic argument that is left out has zero values, so minItems also applies to it unless it has a default (add required for the message to say the input is missing). An env or config list is checked only when it is set."
},
"maxItems": {
"type": [
"integer",
"null"
],
"minimum": 0,
"description": "Maximum number of values for a repeatable (array or map) input \u2014 rejected on scalar types. maxItems: 0 is a real bound: no value is accepted. A maxItems below minItems is rejected, since no value could pass."
},
"properties": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Schema"
},
"description": "Property schemas for an object shape. Used by document shapes (output, stdin payloads, named schemas) \u2014 and, on a map-typed FLAG, the declared property names feed shell completion's key vocabulary (dotted paths when dotted_keys is set, offered up to the '=')."
},
"items": {
"$ref": "#/definitions/Schema",
"description": "Element schema for an array type: 'array' + items int generates []int, and items $ref a slice of the named type; with no items, elements are strings. Per-value constraints (enum, pattern, minimum/maximum, exclusiveMinimum/exclusiveMaximum, multipleOf, minLength/maxLength) apply to every element and may be written either here, JSON Schema style, or on the list itself. The two spellings mean the same thing, and declaring the same constraint in both places with different values is an error. minItems/maxItems belong on the list (they count elements) and are rejected here on flags, arguments, env and config. The TextUnmarshaler rule also applies to each element (see 'type')."
},
"uniqueItems": {
"type": "boolean",
"description": "On a list input (a list flag, a variadic argument, or a list env or config input): reject a list that holds the same value twice. Values compare as their type reads them, so `01` and `1` are the same int, `1h` and `60m` the same duration, `1Ki` and `1024` the same bytesize, and `::1` and `0:0:0:0:0:0:0:1` the same ip; a time compares as the instant it names, read with its layout; a list of objects compares whole objects; a case-insensitive enum compares its declared spellings. Checked on every channel and by CheckInputs, and a list default may not repeat a value. Rejected on scalar, map and count inputs, and inside items. On output, stdin and named schemas it is the JSON Schema keyword."
}
}
},
"Schema": {
"description": "JSON Schema-inspired type definition used for output/response and object property schemas. The 'required' field is a string array of required property names (JSON Schema object semantics). For input schemas where 'required' means 'must be provided', use InputSchema instead.",
"allOf": [
{
"$ref": "#/definitions/BaseSchema"
},
{
"type": "object",
"properties": {
"description": {
"type": "string",
"description": "What this shape or property is, in a sentence. Rotini writes it as the Go doc comment on the generated type or struct field (a named schema, an output or stdin shape, and each of their properties), so the code a handler reads explains itself. Documentation only: it changes no validation and no field. Accepted on object schemas and their properties; an input's own schema uses `summary:` on the input instead."
},
"required": {
"type": "array",
"x-hint": "on an object schema, required lists property names (required: [host]); true or false belongs on an input's own schema (flags, arguments, env, config, stdin)",
"description": "Required property names for object schemas (standard JSON Schema semantics).",
"items": {
"type": "string"
}
},
"const": {
"description": "The one value this property may hold (standard JSON Schema semantics), checked wherever the shape is: output checks, stdin payloads, config files and object flags. Its Go field keeps the declared `type`. In a stream whose items come in several kinds (`oneOf` on `output`), each kind names itself with a required string property holding a `const`, such as `kind: {type: string, const: progress}`."
},
"oneOf": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/Schema"
},
"description": "On a command's `output` with `output_stream: true` only: the stream's items come in several kinds, one schema per kind, each an object written inline or a `$ref` to a named object schema. Every kind declares the same property, its tag, as a required string with a `const` naming the kind (`kind: {type: string, const: progress}`), and no two kinds share a value. Nothing else may sit beside `oneOf`.\n\nThe generated <Prefix>Output is then an interface that only the kinds' types satisfy: a `$ref` kind is its named schema's type, and an inline kind gets <Prefix>Output<Value> (<Prefix>OutputProgress for `progress`). rtx.WriteOutputItem takes any kind, fills in an empty tag from the item's type, and refuses an item whose tag names another kind. rotini.DecodeOutput decodes each item into its kind's type by reading the tag. Help, man and markdown list each kind with its fields; the contract and the output schema files describe the stream as standard JSON Schema `oneOf`."
}
}
}
]
},
"InputSchema": {
"description": "Extended schema for input definitions (flags, arguments, env vars, config values, stdin). Inherits all BaseSchema fields and adds input-level metadata. The 'required' field here is a boolean indicating whether this input must be provided \u2014 unlike Schema where 'required' is a string array of property names.",
"allOf": [
{
"$ref": "#/definitions/BaseSchema"
},
{
"type": "object",
"properties": {
"required": {
"type": "boolean",
"x-hint": "on an input, required is true or false; a list of property names (required: [host]) belongs on an object schema under schemas",
"default": false,
"description": "When true, the input must be provided (or stdin must not be empty for stdin inputs). On the command line an empty value (`--name \"\"`, `--name=`, or `''` as an argument) is provided; add `minLength: 1` to refuse it. An env input set to the empty string is provided too, while a flag's environment fallback set to the empty string counts as unset. Rejected beside a `default`, which would always supply the value. Note: this is a boolean \u2014 unlike the string-array 'required' on Schema."
},
"default": {
"description": "The value an input takes when nothing supplies one. A scalar for a single value; a list for a repeatable input, each element added as one occurrence (as if the flag were repeated); a mapping for a map input, added as key=value pairs; and for an object-valued flag a mapping of the named schema's keys (a list of mappings for a list of objects). Write it the way a user would type the value (a duration as 30s, a date as 2026-09-29 or in the input's `layout:`). `rotini validate` checks it against the input's type, enum, bounds and schema, so a default that could never be accepted fails there rather than on every run that uses it. A supplied value replaces the default; the two never merge. `default_text:` changes only how help shows it."
},
"variable": {
"type": ["string", "array"],
"items": {"type": "string", "pattern": "^[A-Z_][A-Z0-9_]*$", "examples": ["GH_TOKEN"]},
"minItems": 1,
"uniqueItems": true,
"description": "The exact environment variable this input reads, instead of the name rotini would derive. It may be a list, first preferred: `variable: [GH_TOKEN, GITHUB_TOKEN]` reads the first one that is set, for a value other tools already know under more than one name. Help lists every name. For a flag, help shows them under the flag, in lookup order. A nested env input (`nesting:`) takes one name, since it is the prefix of a family of variables. Valid on env inputs, and on flags and arguments as their environment fallback; rejected on config inputs and stdin, which have no environment variable.\n\nA variable named here is never given the `env_prefix`: it is already exact, and prefixing it would silently make it a different variable.\n\nOn a flag or an argument it also turns on the fallback chain (command line, then environment, then config file, then default), keyed by the input's own name unless `key:` names one. So `--token` with `variable: GITHUB_TOKEN` reads that variable directly, with no need for a config `key:` whose UPPER_SNAKE form happens to match. Because the flag now has a key, a configuration file that defines that key supplies it too; declare `key:` to control what that key is.\n\nAn argument's fallback fills a position the command line left out (`deploy` reads `<env>` from `DEPLOY_ENV`). Positionals fill left to right, so a fallback applies only when every argument before it has a value, and an argument after one with a fallback needs a fallback or a default too. A variadic argument splits its environment value on its `separator`; without one the whole value is one item. Not on a passthrough argument, or the arguments of a passthrough command, whose words are taken as typed.",
"pattern": "^[A-Z_][A-Z0-9_]*$", "examples": ["GITHUB_TOKEN"]
},
"variable_file": {
"type": "string",
"pattern": "^[A-Z_][A-Z0-9_]*$",
"examples": ["APP_TOKEN_FILE"],
"description": "An environment variable holding the path of a file to read this input's value from, the convention Docker and Kubernetes secrets use: with `variable_file: APP_TOKEN_FILE`, `APP_TOKEN_FILE=/run/secrets/token` reads the value from that file. One trailing line ending is removed, a relative path is read from the run's working directory, and the file may be at most 1 MiB. Setting both the plain variable and this one is an error; an empty value counts as unset. Valid on env inputs and on flags that read an environment variable (as part of the flag's fallback); like `variable:`, the name is exact and never given the `env_prefix`. Help lists it after the plain names, and a secret input's file contents are redacted like any other value."
},
"file": {
"type": "string",
"description": "Config inputs only: reads this input from one named config_files entry only, never from the other files, so a key present in another file does not satisfy it (or its 'required'). Omit it to read through the declared order, where the first file with the key wins.",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["appconf"]
},
"key": {
"type": "string",
"description": "Dotted key path the value is read from (config inputs, and the config fallback of flags and arguments; e.g. 'server.port'). Segments of letters/digits/_/-, joined by dots; rotini resolves it through the configuration files (and SNAKE_UPPER of it names a flag's or argument's env fallback variable). Help shows the key under the flag or argument when the command reads configuration files.",
"pattern": "^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$", "examples": ["port", "server.port"]
},
"secret": {
"type": "boolean",
"default": false,
"description": "When true, this input's value is treated as a secret: redacted in provenance/error output by the default input reader. It does not prompt: a handler that wants to ask for the value interactively reads it without echo itself (golang.org/x/term's ReadPassword, for one); for non-interactive supply, pair secret with from: [file] (token file) or an env input. A secret flag or argument whose from: list leaves out value refuses a value typed on the command line, which would show in the process list and shell history; `rotini validate` warns about a secret flag that accepts one. Help, man and markdown pages leave out a secret input's default unless `default_text` sets what to show."
},
"ignore_case": {
"type": "boolean",
"default": false,
"description": "With 'enum' only: match a value against the enum without regard to case, so `--mode FAST` is accepted against [fast, slow]. The value is stored in its declared spelling (fast), so a handler compares against one form. Applies wherever the input reads a value: the command line, a flag's environment and config fallbacks, and env and config inputs.",
"$comment": "Enforced by lintIgnoreCase: requires enum."
},
"separator": {
"type": "string",
"minLength": 1,
"maxLength": 3,
"pattern": "^(?:[\\s\\S]|nul)$", "examples": [",", "nul"],
"description": "List and map flags, a variadic argument, and an env list or map input: split each value on this character, so `--tags a,b,c` is three tags and `--label a=1,b=2` two entries. The word `nul` splits on NUL bytes, as `find -print0` writes them; since command-line and environment values can't contain a NUL, it applies only to content rotini reads itself, so it is valid only on a list or map flag with `from: [file]` or `from: [stdin]`, whose file or piped content is split on NUL instead of lines, byte for byte. Splitting is CSV-style: an item in double quotes keeps the separator (`--tags '\"a,b\",c'`), leading spaces are trimmed, and an empty value (`--tags \"\"`) is an empty list. Repeating the flag still appends, so `--tags a,b --tags c` is three tags. Items are split before validation, so enum, item constraints and minItems/maxItems see each one. A flag's environment-variable fallback splits the same way (TAGS=a,b); a configuration file's list binds item by item whether or not a separator is declared. Without a separator, each occurrence is one value, used as is. An env list or map input splits its variable on commas (TAGS=a,b) unless it declares a separator (`separator: ':'` reads PATHS=a:b): a plain split that trims spaces, with no quoting. A map's separator splits pairs; each pair still splits on `=`. Not valid on config inputs: a configuration file writes a list as a list.",
"$comment": "Enforced by lintSeparator: list/map types on flags, arguments and env inputs only; not a double quote, a line break, or '=' on a map."
},
"implicit_value": {
"type": [
"string",
"number",
"boolean"
],
"description": "Flags only: the value a flag takes when it is given without one, which makes its value optional (the `--color[=when]` shape). With `implicit_value: always`, a bare `--color` means always, `--color=never` sets never, and a flag left out takes its `default` as usual. Because the value is optional it must be attached: in `--color never`, `never` is the next argument, not the flag's value (a short flag attaches too: `-cnever`, `-c=never`). Help shows the flag as `--color[=<type>]` with `(implicit: always)`. For a scalar flag that is not a bool (a bool already works this way, with true), and the value must satisfy the flag's type, enum and constraints.",
"$comment": "Enforced by lintImplicitValue (scope) and lintValuesParse (type)."
},
"default_text": {
"type": "string",
"minLength": 1,
"description": "How help, man and markdown pages show the default, in place of the value itself. Use it for a default that reads badly as written (`default_text: 'the number of CPUs'`, `default_text: '$HOME/.cache/app'`), or one the handler computes when the input is unset, so there is no literal to show. Display only: the value the input takes is still `default`, or nothing."
},
"layout": {
"type": ["string", "array"],
"minLength": 1,
"items": {"type": "string", "minLength": 1},
"minItems": 1,
"uniqueItems": true,
"x-hint": "a layout (2006-01-02) or a list of them, tried in order",
"examples": [
"2006-01-02",
"Jan 2 2006 15:04",
"unix",
["2006-01-02", "unix"]
],
"description": "Time inputs only (time, datetime, date, time.Time, and lists of them): how the value is written. A Go reference-time layout (the reference time Mon Jan 2 15:04:05 MST 2006 written the way yours is: `2006-01-02`, `02/01/2006`, `Jan 2 2006 15:04`), or `unix` (seconds since the epoch, fractions allowed) or `unixmilli` (milliseconds). A layout with no zone parses as UTC. Without it, `date` reads `2006-01-02` (that day's UTC midnight) and `time`/`datetime` read RFC 3339 (`2026-09-29T14:00:00Z`). Applies wherever the input reads a value, and a `default` must parse under it. It may be a list, tried in order: the first layout that parses wins, help shows the first, and an error names them all (`layout: [2006-01-02, unix]`). Put the most specific first, since a value that parses under an earlier layout never reaches a later one.",
"$comment": "Enforced by lintLayout (time types only; a real layout) and lintValuesParse (defaults)."
},
"negatable": {
"type": ["boolean", "string"],
"pattern": "^--[a-zA-Z][a-zA-Z0-9_-]*$",
"x-hint": "true, or the negated form as a long identifier (--plain)",
"examples": [true, "--plain"],
"default": false,
"description": "Bool flags only: also accept a `--no-<name>` form for every long identifier, which sets the flag false. `--color` with negatable declares `--no-color` too.\n\nUse it to turn something off for one run when a default, a config file or an environment variable already turned it on, which a plain bool flag cannot do. Short identifiers get no negated form.\n\nA declared identifier always wins over a derived negated one, so a flag you declare as `--no-cache` keeps that name. The negated form takes no value: `--no-color=true` is a parse error. The generated field is the same single bool either way, and help shows it as `--[no-]color`.\n\nA string names the one negated form instead: `negatable: --plain` on `--color` accepts `--plain` to set it false, and no `--no-color`. It is a long identifier that no flag on the command path declares, and help shows it beside the flag's own identifiers: `--color, --plain`.",
"$comment": "Enforced by lintNegatable: bool only, never count, and the long identifier is what the negated form derives from; a custom form collides with no identifier on the command path."
},
"dotted_keys": {
"type": "boolean",
"default": false,
"description": "Map-typed flags only, and only with 'any' values ('map'/'object' \u2192 map[string]any). When true, a '.'-separated key in a key=value pair assigns into nested maps, helm-style: --set image.tag=v2 \u2192 map[image][tag]=v2. Opt-in because '.' is a legal character in plain map keys \u2014 without it, --label a.b=c stores the literal key 'a.b'. Each assignment overwrites whatever is at its path (creating intermediate maps as needed), so later pairs win and --set a=1 --set a.b=2 leaves a nested map under 'a'. A value is read as its JSON spelling would be \u2014 true/false, null and JSON numbers are booleans, null and numbers; anything else is text \u2014 so --set replicas=3 stores the number 3, as a config file's replicas: 3 does. Declare 'properties' on the flag's schema to give shell completion the known key paths (offered up to the '=')."
},
"from": {
"type": "array",
"items": {
"type": "string",
"enum": [
"value",
"file",
"stdin"
]
},
"description": "Flags, and arguments before any variadic one: where the input's value may come from, besides the text on the command line.\n\n- 'file' \u2014 a value starting with '@' is replaced by the named file's contents (`--token @/run/secret`; pair with `secret: true` for a token file). To pass a literal value that starts with `@`, double it: `--to @@alice` gives `@alice`, and `@./@name` reads a file whose name starts with `@`\n- 'stdin' \u2014 a value of exactly '-' is replaced by what is piped on stdin (`-f -`); empty stdin is then a usage error, and stdin can be read once, so one input on a command path may take it: a from:stdin flag or argument, or the command's stdin: input\n- 'value' \u2014 a value typed on the command line. Literal values are always allowed, except on a secret input: with `secret: true`, a 'from' list without 'value' refuses a literal (`--token takes @file or -, not a value`), so the secret never shows in the process list or shell history. On other inputs listing it is documentation only. Any value that does not match an enabled marker stays literal\n\nText read from a file or stdin has one trailing line ending removed (leading and interior whitespace is kept), then goes through the normal type, enum and constraint checks: the flag's value is that text. On an object-valued flag the text is decoded as the object (JSON, or YAML when it spans lines); a structured payload for the whole command belongs in the command's stdin: input. On a list or map flag, the file's (or stdin's) lines are separate values; blank lines are skipped, and each line is split on the flag's `separator`.\n\nWithout 'from', '@' and '-' are ordinary characters. Defaults and environment and config fallbacks are always literal: the markers apply only to values given on the command line. On an argument they work the same way: `cat-config -` reads stdin, `cat-config @app.yaml` the file."
},
"config_source": {
"type": "string",
"description": "Flag and env inputs only: names a config_files entry whose file path this input supplies, so a user can point the program at a config file (`--config ./other.yaml`, or `ACME_CONFIG=./other.yaml` for a flag that declares `variable: ACME_CONFIG`). The command line and environment are read first, then the file they name is opened. The path comes from the flag if set on the command line, then the first variable that is set (the flag's own `variable:` names, then the env input's), then the flag's default, then the entry's own path or discover. A path supplied this way must exist: unlike a declared path, a missing file is then an error, because the user asked for it. A path given on the command line or in a variable has a leading `~` expanded to the home directory (`~user` too when the program wires path expansion, and kept as written otherwise) and is otherwise used as written, so `$VAR` stays literal; the flag's default is expanded like a declared path. An empty value on the command line is a usage error; an empty variable counts as unset. The flag's own value comes from the command line, its variable or its default, never from a configuration file. The input's type must be string. At most one flag and one env input may name the same entry. The file the user names is read in the format its extension implies unless the entry declares `format`, so an entry without one links every config format, and `rotini validate` warns about it; declaring `format` links only that format.",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["appconf"]
},
"nesting": {
"type": "string",
"minLength": 1,
"description": "Env inputs only, map-typed ('map'/'object' \u2192 map[string]any): the separator that collects a family of environment variables into this one nested input. The variable prefix is 'variable:' when set, else the UPPER_SNAKE form of the input's name. With name: http, variable: ACME_HTTP, nesting: \"__\", ACME_HTTP__TIMEOUT=30 and ACME_HTTP__RETRY__MAX=9 bind as http = {timeout: \"30\", retry: {max: \"9\"}} (segments lowercased; values are strings). 'required: true' is an error when no matching variables exist. 'default:' is rejected: set defaults in code or in a config file instead."
},
"complete": {
"$ref": "#/definitions/CompletionHint",
"description": "Shell-completion hint for this input's value: for the common case of a file or directory, between a fixed `enum` and a completer written in Go (FlagValueCompleter), plus an optional message to show when there is nothing to offer. Declare 'kind', 'message' or both.\n\nFlags and arguments only. Each generated completion script turns the hint into that shell's own path completion. A completer written in Go still wins when it answers; the hint is the fallback, which the completer can replace for one request with rtx.SetCompletionFallback."
},
"placeholder": {
"type": "string",
"minLength": 1,
"description": "Display name for this input's value in generated help, man pages and usage lines: `--file <PATH>` instead of the type, `<PATH>` instead of the argument's name. Presentation only: parsing, completion and the generated field are unchanged. Conventionally UPPERCASE or <angle-bracketed>."
},
"repeatable": {
"type": [
"boolean",
"null"
],
"description": "On a single-value flag: `repeatable: false` makes giving the flag more than once on the command line an error naming the first and last values, instead of the last value winning, which is the default. `-vv` and `--color --no-color` are repeats, and so is a cascading flag given at two levels of the command path; a sub-command that redeclares the flag has its own. Environment and configuration values are unaffected. Rejected on list, map, count and object flags, which repeat by nature, and on arguments, env and config inputs."
},
"relative": {
"type": "string",
"enum": ["past", "future", "both"],
"examples": ["past"],
"$comment": "Enforced by lintRelativeTime: time types only, and the default parses under it.",
"description": "Time inputs only (time, datetime, date, and lists of them): also accept a time measured from now. The input still takes absolute values, tried first, so declaring it never changes what one means.\n\n- 'past' \u2014 a time ago: `2h` or `-2h` (two hours ago), `3d`, `1w`; `+2h` is an error. The words `now`, `today` (midnight) and `yesterday`\n- 'future' \u2014 a time from now: `2h` or `+2h`; `-2h` is an error. The words `now`, `today` and `tomorrow`\n- 'both' \u2014 either, with a sign required (`-2h`, `+3d`), and all four words\n\nDurations take Go units plus `d` (24h) and `w`. Words are read in any case. On a date the result is the calendar date it falls on, and an offset must be whole days. Times are measured from the run's clock, read once per run, so every relative value in a run agrees; Program.WithClock sets it for tests, and rtx.Now() returns the same reading. `today` and the date a value falls on follow the clock's time zone (the process's, by default). A flag's or argument's `default` may be relative (`default: 24h`) and is measured at run time; an env or config input's default is absolute."
},
"base": {
"type": "string",
"enum": ["prefixed"],
"examples": ["prefixed"],
"$comment": "Enforced by lintIntegerBase: integer types only, and the default and enum parse under it.",
"description": "Integer inputs only (the int and uint types, integer, and lists of them): with `base: prefixed`, a value may also be written with a base prefix after an optional sign — `0x1F` or `0X1F` (hexadecimal), `0o17` (octal), `0b101` (binary) — and `_` may separate digits, as in Go literals (`1_000`, `0xFF_FF`). A value without a prefix is still decimal: `010` is ten, never octal. Misplaced underscores (`_1`, `1_`, `1__0`, `-_1`) are invalid, and a value out of the type's range is an error as usual.\n\nIt applies wherever the input reads text: the command line, a flag's or argument's environment and config fallbacks, env inputs, config values written as strings, and `default` and `enum` values. Bounds, enum and `uniqueItems` compare the number a value reads as, so `0x10` and `16` are the same. Without `base`, an integer is decimal from every source, so `0x10` is an invalid value. Help notes what the input accepts. `prefixed` is the only value."
},
"glob": {
"type": "boolean",
"default": false,
"description": "On a variadic argument of type string, existingfile, existingdir or inputfile: on Windows, where the shell passes patterns through unexpanded, rotini expands each word containing `*`, `?` or `[` into the matching paths, sorted, relative to the run's directory. A word naming an existing path is kept as written (`file[1].txt`), `-` is never expanded, and a pattern matching nothing is passed through without its escapes, so the path check reports it as a POSIX shell would. `[*]`, `[?]` and `[[]` stand for a literal `*`, `?` and `[`, and a word with no other pattern character is not expanded; ArgvOf writes these characters that way on Windows. Matching is case-sensitive (`*.TXT` does not match `a.txt`), `**` is not supported, and matches use `\\` separators. On other systems the shell has already expanded unquoted patterns, so nothing changes. Not the `glob` value type, which validates a pattern instead of expanding it."
},
"expand": {
"type": "array",
"items": {"type": "string", "enum": ["home", "env"]},
"uniqueItems": true,
"minItems": 1,
"examples": [["home", "env"]],
"$comment": "Enforced by lintExpand: string-shaped kinds only, not with enum or secret.",
"description": "Path inputs (string, existingfile, existingdir, inputfile, outputfile, and lists of them): expand what a shell would in values from every source (the command line, environment variables, configuration files, .env files, variable_file files and the declared default), before the value is checked or bound. Help shows the default as written.\n\n- 'home' \u2014 a value that is exactly `~`, or starts with `~/`, becomes the home directory plus the rest; `~name/...` becomes that user's home directory, read from the system's user database. A `~` anywhere else is kept.\n- 'env' \u2014 `$NAME` and `${NAME}` become that variable's value, from the run's environment (including .env files). An unset or empty variable is a usage error naming it, and `${NAME:-x}` and the other shell operators are errors. A `$` not followed by a name is kept (`C$`, `$1`).\n\n`~` is expanded first, then variables, in one pass: text a variable supplies is never expanded again. Each item of a list is expanded after the value is split on its `separator`. `%VAR%` is not expanded on Windows (cmd and PowerShell expand their own syntax), but `~\\` is. Write `$$` for a literal `$`. A project configuration file found by walk-up discovery can then read the user's environment into a path, so prefer `[home]` for inputs such a file sets. Not valid on secret inputs."
},
"relative_to": {
"type": "string",
"enum": ["config"],
"examples": ["config"],
"$comment": "Enforced by lintExpand: needs a configuration source.",
"description": "'config': a relative path read from a configuration file is resolved against that file's directory, so `cache: ./cache` in ~/.config/app/config.yaml means ~/.config/app/cache. Applied after `expand`. Absolute values, and values from the command line, environment variables, .env files and the default, are kept as they are and stay relative to the run's directory. Valid on config inputs, and on flags and arguments with a configuration fallback (`key:`)."
},
"values_from": {
"type": "string",
"pattern": "^output(\\.[A-Za-z_][A-Za-z0-9_-]*)*$",
"examples": ["output", "output.tasks"],
"$comment": "Enforced by lintValuesFrom: flags and arguments of a command that declares output, string or list-of-string types, no enum of their own, and a path that reaches an object with properties.",
"description": "Flags and arguments only: take the input's allowed values from the field names of the command's declared `output:`, for a `--json id,title` or `--sort-by title` flag. `output` is the output itself, or one item when it is an array (or the command writes a stream, `output_stream`); `output.tasks` is the `tasks` property of the output, and each further segment names a property below it. An array met on the way is stepped through to its items, so `values_from: output.tasks` on a `{tasks: [Task]}` output lists Task's fields. The place reached must be an object with `properties`, and its property names, sorted by name, become the input's enum: help, completion, validation and the contract list them like any enum, and renaming an output field changes the flag's values too. Only that object's own properties are listed, not nested ones. The input's type is `string` or a list of strings (`[]string`, with a `separator` for `--json id,title`), it declares no `enum` of its own, and it is not `cascading`. The handler applies the values with rotini.SelectFields and rotini.SortBy."
}
}
}
]
},
"FlagInput": {
"type": "object",
"required": [
"name"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["output", "dry-run"],
"description": "Logical name for the flag"
},
"schema": {
"$ref": "#/definitions/InputSchema",
"description": "Type definition and input-level metadata (type, required, default, enum, nullable, constraints)"
},
"summary": {
"type": "string",
"description": "Short one-liner shown next to this flag in the Flags section of generated help."
},
"hidden": {
"type": "boolean",
"default": false,
"description": "When true, the flag is omitted from generated help (it still parses on the command line)."
},
"group": {
"type": "string",
"description": "Group label that puts this flag under its own heading in generated help, the way a command's 'group' does in the Commands list: flags sharing a group appear together, and ungrouped flags fall under the default Flags heading. Groups appear in the order their first member is declared, unless the command declares `groups`: then the ungrouped flags come first, then the groups it lists, in its order, then any others.\n\nPresentation only: parsing, precedence and the generated field are unchanged. Use it on a command with many flags, so its help page is easy to scan. Not to be confused with 'flag_groups', which validates combinations of flags."
},
"cascading": {
"type": "boolean",
"default": false,
"description": "When true, this flag applies to its command and to every command below it: it may be written anywhere after its command's name, after its sub-commands' names too, and every descendant's generated help lists it (under the 'Global Flags' section). When false (the default), the flag applies only when its own command is the one run: given with any of its sub-commands, before or after their names, it is a usage error naming the command it belongs to, and descendants' help and completion leave it out. A flag is never accepted before its own command's name (one written before a sub-command's name belongs to an ancestor, which is how two commands may declare the same flag). Environment and configuration values are unaffected."
},
"short_circuit": {
"type": "boolean",
"default": false,
"description": "When true, setting this flag on the command line waives every declared requirement of the invoked command chain: required inputs, enums, bounds, patterns, flag groups and flag dependencies are not checked, and rtx.Inputs succeeds. Use it for flags that replace the command's normal run, such as --help, --version or --print-schema. Errors in reading the command line (an unknown flag or command, a value of the wrong type, too many arguments) are still reported, as is an environment value of the wrong type. A configuration file that can't be found, parsed or read into its inputs is skipped for that run, so --help works in a broken directory. Rotini takes no action of its own: your handler checks the flag and decides what to do. Only the command line sets it, never an environment variable, a configuration file or a default. Must be a bool, and can't be required, negatable, given a default of true, read from the environment or a configuration file (key, variable), or listed in a flag group or flag dependency."
},
"deprecated": {
"type": "string",
"description": "Deprecation message. The flag is annotated as deprecated in generated help, and using it is reported at run time by rotini.Deprecations with this message (a data feed for the handler; rotini itself prints nothing). With `deprecated_identifiers`, only those spellings report \u2014 the others are the ones to move to (`identifiers: [--db, --database]`, `deprecated_identifiers: [--database]`, `deprecated: use --db`); without, the whole flag is deprecated and every spelling reports."
},
"identifiers": {
"type": "array",
"description": "CLI flag identifiers (e.g., '--force', '-f'). When absent, '--<name>' is derived from the flag's name, with '_' written as '-' ('dry_run' \u2192 --dry-run). `rotini validate` warns about a flag with no long form and about a one-dash word of several letters ('-name'), which POSIX tools read as a bundle of short flags; it rejects such a word when the command's short flags spell it as a bundle. Reusing an identifier of a cascading ancestor flag is a warning, since after this command's name it means this flag; it is an error when this command's flags take every identifier of that flag, which then can't be given after this command's name at all. A flag of the same name that redeclares the switch is not reported.\n\nA single dash and one digit (`-4`) declares a digit option, on a bool or count flag (`identifiers: ['-4', --ipv4]`; quoted, since a bare -4 is a number in YAML). A word starting with that dash and digit is then the flag, on this command and every command below it, while other negative numbers still parse as numbers. `rotini validate` rejects it when an argument of this command or one below it takes negative numbers.",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^-{1,2}[a-zA-Z][a-zA-Z0-9_-]*$|^-[0-9]$", "examples": ["-o", "--output", "--dry-run"], "x-hint": "one or two dashes, then a letter, then letters, digits, - or _; or a single dash and one digit (-4), as in ssh -4. No dots: --db.host is how a user sets field host of an object flag --db (declare --db with $ref to a named object schema), so a declared --db.host would compete with it; to name a flag after a nested setting, write --db-host"
}
},
"deprecated_identifiers": {
"type": "array",
"items": {
"type": "string",
"pattern": "^-{1,2}[a-zA-Z][a-zA-Z0-9_-]*$|^-[0-9]$", "examples": ["--out"]
},
"description": "CLI tokens for this input that are deprecated \u2014 a subset of its identifiers (flags) or aliases (commands). When one of these is used on the command line, rotini's Deprecations surfaces it as a data point for the handler to act on (warn, emit telemetry, etc.); the framework itself does nothing. Tokens not listed here are unaffected. Pair it with `deprecated:` to give the report a message; `deprecated:` alone deprecates every spelling"
},
"deprecated_since": {
"type": "string",
"description": "The release that deprecated this flag (X.Y.Z), shown beside the deprecation in help, man and markdown, written into the contract, and reported by rotini.Deprecations as Since. Needs `deprecated`.",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["1.4.0"]
},
"removed_in": {
"type": "string",
"description": "The release that will remove this flag (X.Y.Z), later than `deprecated_since`. Shown like `deprecated_since` and reported as RemovedIn. `rotini validate --release <X.Y.Z>` (or the variable the conf's `validate.release_env` names) fails while the flag is still declared at or past that release. Needs `deprecated`.",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["2.0.0"]
},
"deprecated_identifiers_removed_in": {
"type": "object",
"description": "The release that removes each deprecated identifier, by identifier: `{--conf: 2.0.0}` plans the removal of the spelling `--conf` while the flag stays under its other identifiers. Each key must be listed in `deprecated_identifiers`. `rotini validate --release` checks these like `removed_in`, and the contract carries them.",
"additionalProperties": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["2.0.0"]
}
},
"role": {
"type": "string",
"enum": [
"force",
"chdir",
"fields",
"sort",
"dry-run",
"confirm",
"machine-output",
"page"
],
"description": "What this flag means to a program driving the CLI, such as an agent. 'force' marks the bool flag that lets the command replace existing output files (the handler passes it on, as in rotini.CreateOutput(rtx, path, rotini.Overwrite(in.Flags.Force))). 'fields' marks the list flag that chooses which output fields are written (`--json id,title`, applied with rotini.SelectFields), and 'sort' the string flag that names the field to sort by (`--sort-by title`, applied with rotini.SortBy); each takes its values from an `enum` or `values_from`. These three change nothing at run time. At most one flag per command has a given role.\n\n'chdir' is the one role the runtime acts on: a directory flag like git's `-C dir`, spelled with whatever identifiers the flag declares (`-C`, `-w, --workdir` or any other), on the root, cascading, read from the command line only (no default, env or config fallback, or `from:`), one per program. Rotini reads it before anything else, and resolves walk-up config discovery, relative paths, `@file` values, path checks and the directory plugins run in against it (Context.Dir returns it). It never changes the process's working directory, so a handler joins relative paths with rtx.Dir(). A relative value is resolved against the run's directory, and given more than once, the last wins.\n\n'dry-run' marks the bool flag that previews a run without changing anything, and 'confirm' the bool flag that answers yes for a command that would otherwise refuse or prompt. 'machine-output' marks the flag that makes the command write its declared `output` as JSON: a bool flag, or a string flag with an `enum` and `role_value` naming the value that selects JSON (`--format json`). 'page' marks the flag that selects a page of results. They change nothing at run time either; the contract, tool exports and the agent skill page read them, and a command has each at most once, counting the cascading flags it inherits."
},
"role_value": {
"type": "string",
"minLength": 1,
"examples": ["json"],
"description": "With `role: machine-output` on a string flag: the `enum` value that selects JSON output (`json` for `--format json`). Tool exports and the MCP companion add `--format=json` to the command line themselves and leave the flag out of the tool's parameters. Only on a machine-output flag with an enum that lists it."
},
"effects": {
"$ref": "#/definitions/Effects",
"description": "What giving this flag adds to its command's `effects`. A run's effect is the highest of its command's and every given flag's (read, then write, then destructive); it is idempotent only when every stated value says so, and reaches outside the machine when any does. A flag can only raise its command's effect, so its command must declare `effects` with a lower kind; mark a preview flag with `role: dry-run` instead. A cascading flag raises whichever command it is given to. Help, man and markdown show the kind beside the flag (`(destructive)`), and the contract and the generated FlagDef.Effects carry it."
},
"agent": {
"type": ["boolean", "null"],
"description": "Whether this flag is a tool parameter for AI agents (tool exports and the MCP companion). `false` leaves it out, while it stays in help. `true` brings back a flag left out by default: a hidden, deprecated, secret or short-circuit flag, or one that reads `from:` a file or stdin (which would let an agent read the host's files, or put a secret in the model's context). Unset follows those defaults."
},
"hidden_identifiers": {
"type": "array",
"description": "Identifiers the command line accepts for this flag but that are never listed: help, completion and suggestions show only `identifiers`. Use them for old spellings kept for scripts. A hidden long identifier of a negatable flag gets its `--no-` form too, unlisted as well. They follow the rules of `identifiers` (no clash on the command path), and may be listed in `deprecated_identifiers`.",
"items": {
"type": "string",
"pattern": "^-{1,2}[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["--out"]
},
"minItems": 1,
"uniqueItems": true
},
"replaced_by": {
"type": "string",
"minLength": 1,
"description": "With `deprecated`: the flag to use instead, as one of its identifiers (`--output`). It is a flag of this command or a cascading flag of an ancestor. Help, man and markdown add `use --output instead` to the deprecation note, and rotini.Deprecations reports it as ReplacedBy. `rotini validate` checks that the flag exists and isn't itself deprecated.",
"pattern": "^-{1,2}[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["--output"]
},
"description": {
"type": "string",
"minLength": 1,
"description": "Longer text about this flag, paragraphs separated by blank lines, shown under it in man and markdown pages and used as its description in the contract's parameters. Help shows `summary` only; an editable help template can show both, as .Description."
},
"stability": {
"type": "string",
"enum": ["experimental", "beta"],
"description": "How settled this flag is. 'experimental': it may change or be removed in any release. 'beta': it may change in a minor release. Unset means stable. Help, man and markdown show (experimental) or (beta) beside it, and the contract carries it. A flag is never more stable than its command: a flag of an experimental command is experimental too. `rotini diff` rates any change to an experimental item as safe, and a breaking change to a beta item as possibly breaking. Nothing changes at run time."
},
"metadata": {
"type": "object",
"description": "Your own labels for this flag, for your tools to read: string keys, each with a string value. Rotini never acts on them and shows them on no page. It copies them as written into the generated FlagDef.Metadata and the contract, and `rotini diff` rates any change to them as safe.",
"additionalProperties": {
"type": "string"
},
"propertyNames": {
"minLength": 1
}
}
}
},
"ArgumentInput": {
"type": "object",
"required": [
"name"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["source"],
"description": "Logical name for the argument"
},
"schema": {
"$ref": "#/definitions/InputSchema",
"description": "Type definition and input-level metadata (type, required, default, enum, nullable, constraints)"
},
"summary": {
"type": "string",
"description": "Short one-liner shown next to this argument in the Arguments section of generated help."
},
"hidden": {
"type": "boolean",
"default": false,
"description": "When true, the argument is omitted from generated help (it still parses on the command line)."
},
"deprecated": {
"type": "string",
"description": "Deprecation message. The argument is annotated as deprecated in generated help, and supplying it is reported at run time by rotini.Deprecations with this message (a data feed for the handler; rotini itself prints nothing)."
},
"deprecated_since": {
"type": "string",
"description": "The release that deprecated this argument (X.Y.Z), shown beside the deprecation in help, man and markdown, written into the contract, and reported by rotini.Deprecations as Since. Needs `deprecated`.",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["1.4.0"]
},
"removed_in": {
"type": "string",
"description": "The release that will remove this argument (X.Y.Z), later than `deprecated_since`. Shown like `deprecated_since` and reported as RemovedIn. `rotini validate --release <X.Y.Z>` fails while the argument is still declared at or past that release. Needs `deprecated`.",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["2.0.0"]
},
"passthrough": {
"type": "boolean",
"default": false,
"description": "When true, raw words start at this argument: flags before it parse as usual, and it and every word after it are taken as typed, flag-shaped words and `--` included (`app exec --region eu ls -la --help` gives the command's `--region` and passes `ls -la --help` on). A word starting with `-` that should be the first raw word goes after `--` (`app exec -- -x`). It must be the command's last argument, a variadic `[]string` without a separator, enum, pattern, bounds or `uniqueItems`, on a command with no sub-commands, plugins or command-level `passthrough`. A handler reads where a `--` was typed with rtx.DashIndex(). Shell completion offers no flags or sub-commands past the boundary; the argument's own completer, enum or hint still applies."
},
"description": {
"type": "string",
"minLength": 1,
"description": "Longer text about this argument, paragraphs separated by blank lines, shown under it in man and markdown pages and used as its description in the contract's parameters. Help shows `summary` only; an editable help template can show both, as .Description."
},
"stability": {
"type": "string",
"enum": ["experimental", "beta"],
"description": "How settled this argument is. 'experimental': it may change or be removed in any release. 'beta': it may change in a minor release. Unset means stable. Help, man and markdown show (experimental) or (beta) beside it, and the contract carries it. An argument is never more stable than its command. `rotini diff` rates any change to an experimental item as safe, and a breaking change to a beta item as possibly breaking. Nothing changes at run time."
},
"agent": {
"type": ["boolean", "null"],
"description": "Whether this argument is a tool parameter for AI agents (tool exports and the MCP companion). `false` leaves it out, while it stays in help. `true` brings back an argument left out by default: a hidden, deprecated or secret one, or one that reads `from:` a file or stdin. Unset follows those defaults. A required argument left out leaves its command out of the tool exports, with a warning."
},
"metadata": {
"type": "object",
"description": "Your own labels for this argument, for your tools to read: string keys, each with a string value. Rotini never acts on them and shows them on no page. It copies them as written into the generated ArgDef.Metadata and the contract, and `rotini diff` rates any change to them as safe.",
"additionalProperties": {
"type": "string"
},
"propertyNames": {
"minLength": 1
}
}
}
},
"ConfigInput": {
"type": "object",
"required": [
"name"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["endpoint"],
"description": "Logical name for this config value"
},
"summary": {
"type": "string",
"description": "Short one-liner shown next to this input in the generated Environment/Configuration help section."
},
"hidden": {
"type": "boolean",
"default": false,
"description": "When true, the input is omitted from generated help (it is still bound)."
},
"deprecated": {
"type": "string",
"description": "Deprecation message; the input is annotated as deprecated in generated help. (Run-time deprecation reporting covers what argv carries \u2014 commands, flags and arguments.)"
},
"deprecated_since": {
"type": "string",
"description": "The release that deprecated this input (X.Y.Z), shown beside the deprecation in help, man and markdown and written into the contract. Needs `deprecated`.",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["1.4.0"]
},
"removed_in": {
"type": "string",
"description": "The release that will remove this input (X.Y.Z), later than `deprecated_since`. Shown like `deprecated_since`. `rotini validate --release <X.Y.Z>` fails while the input is still declared at or past that release. Needs `deprecated`.",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["2.0.0"]
},
"schema": {
"$ref": "#/definitions/InputSchema",
"description": "Type definition and input-level metadata (required, default, file, key)"
},
"description": {
"type": "string",
"minLength": 1,
"description": "Longer text about this config value, paragraphs separated by blank lines, shown under it in man and markdown pages, in the contract and in the configuration file's JSON Schema. Help shows `summary` only; an editable help template can show both, as .Description."
},
"stability": {
"type": "string",
"enum": ["experimental", "beta"],
"description": "How settled this config value is. 'experimental': it may change or be removed in any release. 'beta': it may change in a minor release. Unset means stable. Help, man and markdown show (experimental) or (beta) beside it, and the contract carries it. An input is never more stable than its command. `rotini diff` rates any change to an experimental item as safe, and a breaking change to a beta item as possibly breaking. Nothing changes at run time."
},
"agent": {
"type": ["boolean", "null"],
"description": "Config values are never tool parameters: on an MCP server they are the server's own configuration. `false` leaves this value out of the agent skill page's list of configuration, and `true` lists it there when it is hidden. The contract carries it."
},
"metadata": {
"type": "object",
"description": "Your own labels for this config value, for your tools to read: string keys, each with a string value. Rotini never acts on them and shows them on no page. It copies them as written into the contract, and `rotini diff` rates any change to them as safe.",
"additionalProperties": {
"type": "string"
},
"propertyNames": {
"minLength": 1
}
}
}
},
"EnvInput": {
"type": "object",
"required": [
"name"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["region"],
"description": "Logical name for this env var input"
},
"summary": {
"type": "string",
"description": "Short one-liner shown next to this input in the generated Environment/Configuration help section."
},
"hidden": {
"type": "boolean",
"default": false,
"description": "When true, the input is omitted from generated help (it is still bound)."
},
"deprecated": {
"type": "string",
"description": "Deprecation message; the input is annotated as deprecated in generated help. (Run-time deprecation reporting covers what argv carries \u2014 commands, flags and arguments.)"
},
"deprecated_since": {
"type": "string",
"description": "The release that deprecated this input (X.Y.Z), shown beside the deprecation in help, man and markdown and written into the contract. Needs `deprecated`.",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["1.4.0"]
},
"removed_in": {
"type": "string",
"description": "The release that will remove this input (X.Y.Z), later than `deprecated_since`. Shown like `deprecated_since`. `rotini validate --release <X.Y.Z>` fails while the input is still declared at or past that release. Needs `deprecated`.",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["2.0.0"]
},
"schema": {
"$ref": "#/definitions/InputSchema",
"description": "Type definition and input-level metadata (required, default, variable)"
},
"description": {
"type": "string",
"minLength": 1,
"description": "Longer text about this environment variable, paragraphs separated by blank lines, shown under it in man and markdown pages and in the contract. Help shows `summary` only; an editable help template can show both, as .Description."
},
"stability": {
"type": "string",
"enum": ["experimental", "beta"],
"description": "How settled this environment variable is. 'experimental': it may change or be removed in any release. 'beta': it may change in a minor release. Unset means stable. Help, man and markdown show (experimental) or (beta) beside it, and the contract carries it. An input is never more stable than its command. `rotini diff` rates any change to an experimental item as safe, and a breaking change to a beta item as possibly breaking. Nothing changes at run time."
},
"agent": {
"type": ["boolean", "null"],
"description": "Environment variables are never tool parameters: on an MCP server they come from the server's own environment, where secrets belong. `false` leaves this variable out of the agent skill page's list of variables, and `true` lists it there when it is hidden. The contract carries it."
},
"metadata": {
"type": "object",
"description": "Your own labels for this environment variable, for your tools to read: string keys, each with a string value. Rotini never acts on them and shows them on no page. It copies them as written into the contract, and `rotini diff` rates any change to them as safe.",
"additionalProperties": {
"type": "string"
},
"propertyNames": {
"minLength": 1
}
}
}
},
"StdinSpec": {
"type": "object",
"additionalProperties": false,
"properties": {
"schema": {
"$ref": "#/definitions/InputSchema",
"description": "Type definition for stdin content. Set required: true in schema to error when stdin is empty. The document formats need a `type` or `$ref` here; the raw formats imply theirs, so `schema:` may be left out or hold only `required`."
},
"format": {
"type": "string",
"enum": [
"json",
"yaml",
"jsonc",
"toml",
"text",
"lines",
"jsonl",
"bytes"
],
"default": "json",
"description": "How the piped stdin payload is read, and the Go type of the generated Stdin field.\n\nThe four document formats (json, yaml, jsonc, toml) decode it into the generated <Prefix>Stdin type (`*<Prefix>Stdin`), validated against the declared schema. The default is json.\n\n'jsonl' reads JSON Lines: one JSON record per line, blank lines skipped, each checked against the schema, which describes one record. The field is `*[]<Prefix>Stdin`, or with `stream: true` an iterator, `iter.Seq2[<Prefix>Stdin, error]`. An error names the line it was found on.\n\nThe raw formats are for commands whose stdin is not a document, such as text filters: 'text' binds the whole payload as one string (`*string`), 'lines' binds its lines (`*[]string`, a trailing newline adds no empty element; with `stream: true`, `iter.Seq2[string, error]`), and 'bytes' binds the payload byte for byte (`*[]byte`), nothing trimmed. Their schema type is implied when left out ('string' for text and bytes, '[]string' for lines); none generates a <Prefix>Stdin type, because there is nothing to shape. Declaring stdin this way, rather than reading rtx.Stdin directly, puts it in the command's help, man and markdown pages and the contract.\n\nOne leading UTF-8 byte-order mark is removed before the payload is read, in every format but 'bytes'. A declared stdin that is not streamed is held in memory for the run."
},
"stream": {
"type": "boolean",
"default": false,
"description": "With 'lines' or 'jsonl' only: generate an iterator (`iter.Seq2[string, error]`, or `iter.Seq2[<Prefix>Stdin, error]`) instead of a slice, so a filter reads one line or record at a time in constant memory. Nothing is read until the handler ranges over it, and stdin is read once per run: a second range, or a second Inputs call, continues where the last stopped. The field is nil when stdin is not read (a terminal, a short-circuit flag such as --help, or `unless_argument` choosing the files), so check it before ranging. A read error, a record that fails its schema, or a cancelled run is yielded once as the error, and the iterator then stops. A line or record is limited only by memory. CheckInputs and InputReport.Validate carry a stream but don't check its items: they are checked as they are read."
},
"unless_argument": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$",
"examples": ["files"],
"description": "The name of an optional file argument of the same command (`inputfile` or `existingfile`, or a list of either): stdin is read only when that argument got no value, or its value is (or, for a list, contains) `-`. Otherwise the Stdin field stays nil and nothing is read. This is the `cat [FILE...]` and `jq FILTER [FILE]` shape. Without it, a declared stdin is read on every run, even when a file is given, which waits forever when a caller holds stdin open. With `required`, an empty stdin is an error only when no file is given."
},
"separator": {
"type": "string",
"enum": ["nul"],
"description": "With 'lines' only: split the payload on NUL bytes instead of newlines, as `find -print0` and `xargs -0` write it. Items are kept byte for byte (no `\\r` or space trimming, since a file name may contain either), and a trailing NUL adds no empty item. The default splits on newlines."
},
"metadata": {
"type": "object",
"description": "Your own labels for this command's stdin, for your tools to read: string keys, each with a string value. Rotini never acts on them and shows them on no page. It copies them as written into the contract, and `rotini diff` rates any change to them as safe.",
"additionalProperties": {
"type": "string"
},
"propertyNames": {
"minLength": 1
}
}
}
},
"FlagDependency": {
"type": "object",
"additionalProperties": false,
"description": "A conditional rule: when the 'when' flag is explicitly set on the command line (with one of the 'equals' values, if given), and none of the 'unless' flags is, every flag in 'requires' must also be set and no flag in 'forbids' may be. Every name is a flag's logical name; 'set' means explicitly provided (a default or env/config fallback neither triggers a rule nor satisfies one). Give 'when' or 'unless' (or both), and 'requires' or 'forbids' (or both).",
"properties": {
"when": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["tls"],
"description": "The flag whose presence triggers the rule."
},
"requires": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["cert"]
},
"description": "Flags that must also be set when the rule triggers."
},
"equals": {
"type": "array",
"minItems": 1,
"items": {
"type": [
"string",
"number",
"boolean"
]
},
"description": "With 'when': trigger only when the 'when' flag's value is one of these (`when: format`, `equals: [csv, tsv]`). The value compared is the one given on the command line, the last one if the flag is repeated, as the flag's enum stores it (so `ignore_case` and enum aliases match). For a bool flag, true or false (`--no-x` gives false). Valid on a single-value flag, not a list, map or count; each value must parse as the flag's type and be in its enum.",
"examples": [["csv", "tsv"]]
},
"unless": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["config"]
},
"description": "Flags any one of which, set on the command line, turns the rule off. Without 'when', the rule applies on every run unless one of these is set (`unless: [config]`, `requires: [token]`)."
},
"forbids": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["insecure"]
},
"description": "Flags that can't be set when the rule triggers (`when: tls`, `forbids: [insecure]`)."
}
}
},
"FlagGroup": {
"type": "object",
"required": [
"kind",
"flags"
],
"additionalProperties": false,
"description": "A constraint on which of this command's flags may (or must) be set together. 'flags' references flag logical names; 'set' means explicitly provided on the command line (a default or env/config fallback does not count).",
"properties": {
"kind": {
"type": "string",
"enum": [
"mutually_exclusive",
"required_together",
"one_of",
"at_least_one"
],
"description": "mutually_exclusive: at most one set. required_together: all or none. one_of: exactly one. at_least_one: one or more."
},
"flags": {
"type": "array",
"minItems": 2,
"items": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["json"]
},
"description": "The logical flag names the rule covers (at least two). Each must be a flag declared on the same command \u2014 validation rejects unknown names. \"Set\" means explicitly set on argv: defaults and env/config fallbacks neither trip nor satisfy a group."
}
}
},
"PluginSpec": {
"type": "object",
"required": [
"name"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["deploy"],
"description": "Name of the declared plugin. The dispatched binary is named <program>-<name>, and is searched for next to the host binary, then in the command's plugin_path, then on PATH. Inside a $ref-composed subtree <program> is the composed spec's own name, so one installed plugin serves both that spec's own binary and a parent that composes it."
},
"aliases": {
"type": "array",
"description": "Additional names that invoke this plugin.",
"items": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["dep"]
}
},
"summary": {
"type": "string",
"description": "Short one-liner shown next to this plugin in its parent's generated Commands list."
},
"timeout": {
"type": "string",
"description": "Host-side timeout for running the plugin. Uses Go duration format (e.g. \"10s\", \"1m30s\"). Empty or omitted means no timeout.",
"pattern": "^([0-9]+(\\.[0-9]+)?(ns|us|\u00b5s|ms|s|m|h))+$", "examples": ["10s", "1m30s"]
}
}
},
"ConfigurationFile": {
"type": "object",
"required": [
"name"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"description": "Logical name for the config file (e.g. 'app-config'). It anchors per-input pins (schema 'file:') and config_source claims, so it must be unique within its chain (this command and its ancestors) \u2014 a name collision in scope is an error.",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["appconf"]
},
"path": {
"type": "string",
"description": "File path. A leading `~` or `~user` and `$VAR` or `${VAR}` references (with the shell forms `${VAR:-x}`, `${VAR:?message}` and the like) are expanded from the run's environment, and a relative path is resolved against the run's directory. Exactly one of 'path' or 'discover' must be set.",
"minLength": 1
},
"format": {
"type": "string",
"enum": [
"json",
"yaml",
"toml",
"jsonc",
"dotenv"
],
"description": "The file's format. When omitted, the format is inferred from the file extension; declare it when the extension is missing or misleading. 'jsonc' is JSON with comments and trailing commas. 'dotenv' reads KEY=value lines whose keys are used as written: a config input reading one declares `key: API_ENDPOINT`, not a dotted path."
},
"schema": {
"$ref": "#/definitions/Schema",
"description": "Optional load-time validation: the loaded document is validated against this schema at bind time, before any value is read from it \u2014 a non-conforming file is a loud error naming the file and the violation (the same gate the stdin channel applies to its payload). Every value in a dotenv file is a string, so a schema for one uses string types; a TOML date or time is checked as its text (`2024-01-02T03:04:05Z`), as in YAML or JSON. The file that actually resolved \u2014 fixed path, discovered, or config_source-supplied \u2014 is the file validated; an absent file passes vacuously (absence is the per-input required's concern). Document-level named schemas resolve via \"$ref\": \"#/schemas/<Name>\". No typed struct is generated from this \u2014 typed access to config values is the config: inputs channel."
},
"discover": {
"$ref": "#/definitions/ConfigurationFileDiscover",
"description": "Locate this file at run time instead of a fixed 'path'. Exactly one of 'path' or 'discover' must be set."
},
"as": {
"type": "string",
"enum": ["config", "env"],
"default": "config",
"description": "What the file supplies. 'config' (the default): configuration values, read by config inputs and flag config fallbacks. 'env': environment variables, the `.env` shape: a `format: dotenv` file whose KEY=value lines are read as if they were set in the environment, by env inputs, flag environment fallbacks, `variable_file` and `config_source` variables. The real environment wins over the file, variable by variable, so the precedence is command line, environment, `.env` file, configuration files, default. Values are literal: `${OTHER}` is not expanded. Rotini's own variables (HOME, XDG_*, PATH) are never read from it. Requires `format: dotenv` (or a `.env` file name); a config input can't pin it with `file:`. Several `as: env` files in scope layer like config files: the nearest command's first, then in declaration order. Prefer a fixed `path: .env` to `walk-up` discovery, which could find a stray ~/.env."
},
"profiles": {
"$ref": "#/definitions/ConfigurationFileProfiles",
"description": "Named sets of settings in this file, one chosen per run by a declared input (`--profile prod`, `ACME_PROFILE=prod`). The keys under `<under>.<name>` are read as if they sat at the top of the file and win over the file's other top-level keys, which every profile shares. Merging is per key: a list is replaced, not joined, and a map's entries merge. Precedence is otherwise unchanged: command line, environment, configuration files (the selected profile, then the shared keys), default. Profiles that aren't selected are not read or checked, and the file's `schema` checks the shared keys merged with the selected profile, without the `<under>` key. Not allowed on a `format: dotenv` or `as: env` file."
}
}
},
"ConfigurationFileProfiles": {
"type": "object",
"additionalProperties": false,
"required": [
"under",
"select"
],
"description": "Where a configuration file keeps its named profiles and which input chooses one.",
"properties": {
"under": {
"type": "string",
"description": "The top-level key holding the named profiles: `profiles: {prod: {region: eu-west-1}}` is `under: profiles`. One key, not a dotted path. It must not be the first segment of a key a config input or fallback reads from this file.",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["profiles", "contexts"]
},
"select": {
"type": "string",
"description": "The name of the string input that chooses the profile: a flag with this name, an env input with this name, or both, declared on this command or an ancestor. The flag set on the command line wins, then the first set variable (the flag's own `variable:` names, then the env input's), then the default. The selector is read before any configuration file, so it can't come from one: the flag can't declare `key:` or `config_source`. An unknown profile chosen on the command line or in the environment is a usage error listing the defined ones; a defaulted profile that a file doesn't define just leaves the shared keys.",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "examples": ["profile"]
},
"default": {
"type": "string",
"description": "The profile used when neither the command line nor the environment chooses one. Declare the default in one place only: here, on the selector flag or on the selector env input. With no default and no selection, only the shared keys apply.",
"minLength": 1, "examples": ["default"]
}
}
},
"ConfigurationFileDiscover": {
"description": "A run-time location strategy for a configuration file, instead of a fixed 'path'. The first directory (in the strategy's order) containing 'file' wins; a file found nowhere is simply absent, the same as a missing fixed path. Among one command's config_files the first declared still wins, wherever each was found.",
"allOf": [
{
"type": "object",
"additionalProperties": false,
"required": [
"strategy",
"file"
],
"properties": {
"strategy": {
"type": "string",
"enum": [
"walk-up",
"xdg",
"native",
"xdg-system"
],
"description": "'walk-up': search from the run's directory (the working directory, unless a `role: chdir` flag or Program.WithDir sets another) upward, one parent at a time, until a directory containing 'file' is found or the root is reached. Use it for project-local config. On Windows the search stops at the drive root.\n\n'xdg': search $XDG_CONFIG_HOME/<app>, defaulting to ~/.config/<app>, on every platform, Windows and macOS included. Rotini does not substitute %APPDATA% or ~/Library/Application Support, so a CLI documented as reading ~/.config/<app> reads the same path everywhere, and a dotfiles repository works unchanged across machines. This is the portable choice. A relative $XDG_CONFIG_HOME is ignored, as the XDG spec requires: it must be absolute (with a drive letter on Windows).\n\n'native': search the platform's own config directory, <dir>/<app>: %AppData% on Windows, ~/Library/Application Support on macOS, and $XDG_CONFIG_HOME (default ~/.config) elsewhere, so on Linux it is the same directory as 'xdg'. A relative $XDG_CONFIG_HOME is ignored here too.\n\n'xdg-system': search each absolute directory in $XDG_CONFIG_DIRS, in order, defaulting to /etc/xdg, for <app>/<file>. On Windows it searches only when $XDG_CONFIG_DIRS is set. Declare it as its own config_files entry after the user's ('xdg' or 'native'): the first declared entry wins per key, so the user's file overrides the system one key by key.\n\n'xdg', 'native' and 'xdg-system' need 'app'."
},
"file": {
"type": "string",
"description": "The file name to look for in each searched directory (e.g. '.acme.toml', 'config.yaml'). It is a name alone: the strategy picks the directories, so `rotini validate` rejects a path (one with `/` or `\\`) and `.` or `..`.",
"minLength": 1
},
"app": {
"type": "string",
"description": "The application directory under the config root: the '<app>' in $XDG_CONFIG_HOME/<app> (or the native or system directory). Required by the 'xdg', 'native' and 'xdg-system' strategies and rejected by 'walk-up', which has no such directory. `rotini validate` enforces both, so its message can say which strategy needs it and what it is for.",
"pattern": "^[^/\\\\]+$", "examples": ["mytool"]
}
}
}
]
},
"ExitStatusEntry": {
"type": "object",
"required": [
"code"
],
"additionalProperties": false,
"properties": {
"code": {
"type": "integer",
"description": "The exit status code being documented (0-255 \u2014 the range a process can actually return).",
"minimum": 0,
"maximum": 255
},
"summary": {
"type": "string",
"description": "What this exit code means."
},
"output": {
"$ref": "#/definitions/Schema",
"description": "The shape stdout still carries when the command exits with this code, for an outcome that is not plain success but prints data anyway (`3: some tasks failed; stdout lists what succeeded`). Documented in the EXIT STATUS section and described in the output schema files and the contract document."
},
"name": {
"type": "string",
"description": "A snake_case name for the code, unique within the command. `rotini generate` writes a constant for it, `<Command>Exit<Name>` (`not_found` on `taskr get` gives TaskrGetExitNotFound), so a handler writes rtx.HaltWithCode(TaskrGetExitNotFound) instead of a bare number; the exit-code check reads the constant like the number. The man and markdown EXIT STATUS sections and the contract show the name too.",
"pattern": "^[a-z][a-z0-9_]*$", "examples": ["not_found"]
},
"retryable": {
"type": "boolean",
"default": false,
"description": "When true, running the same command again may succeed (a timeout, a busy lock). Carried into the contract for agents and other callers that decide whether to retry; rotini itself never retries."
},
"docs_url": {
"type": "string",
"pattern": "^https?://\\S+$",
"examples": ["https://example.com/errors/not-found"],
"description": "An absolute http or https link to more about this exit code, shown in the man and markdown EXIT STATUS sections and carried into the contract. Help and the default reporter don't show it; a reporter of your own can print it, from the generated Definition's ExitStatusDef.DocsURL."
}
}
},
"HelpGroup": {
"type": "object",
"description": "A group heading on a command's pages, with an optional description.",
"required": [
"name"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"description": "The group's label, as the sub-commands' or flags' `group` writes it.",
"minLength": 1
},
"description": {
"type": "string",
"description": "A paragraph shown under the group's heading in help, man and markdown."
}
}
},
"FlagSet": {
"type": "object",
"description": "Flags declared once under the root's `flag_sets`, for commands to add with `use:`.",
"required": [
"flags"
],
"additionalProperties": false,
"properties": {
"summary": {
"type": "string",
"description": "What the set is for, for the spec's readers; it isn't shown on any page."
},
"group": {
"type": "string",
"description": "The help group of every flag in the set that names no `group` of its own, so a set's flags are listed under one heading (`group: Output`)."
},
"flags": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/FlagInput"
},
"description": "The set's flags, declared as on a command."
},
"flag_groups": {
"type": "array",
"items": {
"$ref": "#/definitions/FlagGroup"
},
"description": "Flag groups over the set's own flags, added to each command that uses the set."
},
"flag_dependencies": {
"type": "array",
"items": {
"$ref": "#/definitions/FlagDependency"
},
"description": "Flag dependencies over the set's own flags, added to each command that uses the set."
}
}
},
"Topic": {
"type": "object",
"description": "A help topic: a page that isn't a command, with its text written here (`body`) or generated (`generate`).",
"required": [
"name",
"summary"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]*$",
"examples": ["environment", "filters"],
"description": "The word after `help` that shows the topic (`demo help filters`). Its man page is <program>-<name> in the man feature's section."
},
"summary": {
"type": "string",
"minLength": 1,
"description": "A one-line description, shown beside the name in the root's list of topics, in the man page's NAME line and at the top of the markdown page. The help page shows the body, or this summary when the topic has none."
},
"body": {
"type": "string",
"minLength": 1,
"description": "The topic's text, paragraphs separated by blank lines. Set exactly one of `body` and `generate`."
},
"generate": {
"type": "string",
"enum": ["environment"],
"description": "Build the topic's text from the spec. 'environment' lists every environment variable the program reads: env inputs, flag fallbacks, config_source and variable_file names, the completion switches the conf declares, and the XDG variables config discovery reads, each with the commands that read it. Set exactly one of `body` and `generate`."
}
}
},
"Effects": {
"type": "object",
"description": "What a command does when it runs, or what a flag adds to that. Programs that run the CLI for someone, such as AI agents and their permission rules, read it to decide what needs asking first.",
"required": [
"kind"
],
"additionalProperties": false,
"properties": {
"kind": {
"type": "string",
"enum": ["read", "write", "destructive"],
"description": "'read': changes nothing. 'write': creates or changes things, without destroying what was there. 'destructive': deletes or overwrites something that can't be got back."
},
"idempotent": {
"type": ["boolean", "null"],
"description": "Running it again with the same inputs changes nothing more. Unset says nothing."
},
"open_world": {
"type": ["boolean", "null"],
"description": "It reaches the network or other systems outside the machine. Unset says nothing, and a program reading the contract then assumes it does."
}
}
},
"CompletionHint": {
"type": "object",
"additionalProperties": false,
"anyOf": [
{ "required": ["kind"] },
{ "required": ["message"] }
],
"description": "A shell-completion hint for a flag's or argument's value: a `kind` of value to complete, a `message` to show when there is nothing to offer, or both.",
"properties": {
"kind": {
"type": "string",
"enum": [
"file",
"directory",
"none",
"command",
"executable",
"user",
"host",
"group"
],
"description": "'file': complete file paths (narrowed by 'extensions'). 'directory': complete directories only. 'none': complete nothing, which is not the same as declaring no hint. With no hint the shell applies its own default, and for bash and zsh that is file completion; 'none' turns it off, so for an opaque value (a container id, an API resource name) the shell does not offer the files in the current directory. 'command': complete command paths below the root, for a help command's argument: the words already given to this argument are the path, and the candidates are its visible sub-commands (`help remote <TAB>` offers remote's sub-commands). Arguments of type string or a list of strings only. 'executable': program names (bash offers every command name it knows; zsh, fish and PowerShell offer programs). 'user', 'group': local account and group names. 'host': host names the shell knows (/etc/hosts, ssh known hosts). Each shell's own completer supplies these four; PowerShell has no host completer, and offers users and groups on Windows only. Values of type string or a list of strings only."
},
"extensions": {
"type": "array",
"minItems": 1,
"description": "Narrows kind 'file' to these suffixes, written without a leading dot ('yaml', 'json'). Omitted, every file is offered. Rejected on the other kinds, which have no extensions to filter.",
"items": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9.+-]*$", "examples": ["yaml", "tar.gz"]
}
},
"message": {
"type": "string",
"minLength": 1,
"description": "A line the shell shows while this input's value is being completed and there is nothing to offer, such as `a service name from deploy.yaml`. One line. It shows only when the conf's completion feature sets `messages`, and in zsh and bash 4.4 or later; other shells skip it. A message a completer adds with rtx.AddCompletionMessage takes its place."
}
}
}
}
}Below, every key. rotini validate checks all of them before any code is generated, and
this list is rendered from the schema, so it always matches what the tool accepts.
Document
version
string · required
The minimum rotini version this spec requires (X.Y.Z): the feature set it was written against, not an exact pin. Any rotini of the same major version at or beyond it accepts the document, so a patch or minor upgrade never requires an edit here. Two cases are errors: a rotini older than this, which may not know keys the spec uses, and a different major version. The check is skipped for a development build of rotini, which reports no release version (0.0.0, or none at all). This key, not the optional $schema URL, is what the check reads.
command
Command · required
The CLI’s root command (the binary itself): its name, doc-fields, inputs (flags/arguments/env/config/config_files/stdin) and sub-commands. The root must use ’name’ (not ‘$ref’). The root-command-level keys env_prefix and schemas live here.
$schema
string
Optional URI identifying the rotini spec schema, for editor tooling only: rotini never fetches it, and the version check reads the top-level version key, not this. Any URI is accepted: a released schema (https://raw.githubusercontent.com/go-rotini/rotini/refs/tags/v1.3.0/schema-spec.json — note the ‘v’, matching the git tag), a path written into your project by the conf’s generate.schemas.spec.file, or a fork’s own URL. A relative path is resolved by your editor, not by rotini. rotini init seeds this key ($schema: ./.rotini-schema.spec.json), and it works in every format; YAML editors also accept a # yaml-language-server: $schema=<path> comment in its place.
Command
A command node in the CLI command tree — the root command (under the document’s ‘command’ key) and every sub-command share this recursive shape. Declared inline (with ’name’) or composed from another spec file (with ‘$ref’). The root must use ’name’ (not ‘$ref’). The two root-command-level keys (env_prefix, schemas) are accepted on this shape but are valid only on the root command — rotini validation rejects them on a sub-command.
Identity and visibility
name
string
Command name used in routing. As the root command (the document itself) this is the binary name and must be set — the root cannot use ‘$ref’.
aliases
array of string
Additional names that invoke this command. Command aliases affect dispatch routing; use identifiers on flags for flag aliases. Sub-commands only: the root command is reached by invoking the binary (argv[0] is not a routing token), so rotini validation rejects aliases there.
hidden
boolean · default false
When true, the command is omitted from its parent’s generated Commands list (it still dispatches on the command line).
deprecated
string
Deprecation message. The command is annotated as deprecated in its parent’s generated Commands list, and invoking it is reported at run time by rotini.Deprecations with this message (a data feed for the handler; rotini itself prints nothing). With deprecated_identifiers, only those aliases report; without, every name does.
deprecated_identifiers
array of string
Aliases of this command that are deprecated (a subset of ‘aliases’). When the command is invoked via one of these, rotini’s Deprecations surfaces it for the handler to act on; invoking via the name or a non-listed alias is unaffected. Sub-commands only, like ‘aliases’ — rejected on the root by rotini validation.
deprecated_since
string
The release that deprecated this command (X.Y.Z), shown beside the deprecation in help, man and markdown, written into the contract, and reported by rotini.Deprecations as Since. Needs deprecated.
removed_in
string
The release that will remove this command (X.Y.Z), later than deprecated_since. Shown like deprecated_since and reported as RemovedIn. rotini validate --release <X.Y.Z> (or the variable the conf’s validate.release_env names) fails while the command is still declared at or past that release, so a planned removal is not forgotten. Needs deprecated.
deprecated_identifiers_removed_in
object
The release that removes each deprecated alias, by alias: {remove: 2.0.0} plans the removal of the alias remove while the command stays. Each key must be listed in deprecated_identifiers. rotini validate --release checks these like removed_in, and the contract carries them.
hidden_aliases
array of string
Names that invoke this command but are never listed: help, completion, suggestions and the program’s other pages show only name and aliases. Use them for old spellings kept for scripts, or for shorthands not worth documenting. The help resolvers accept them as the command line does. Sub-commands only, like aliases; a hidden alias can’t repeat the name, an alias, or a sibling’s name. May be listed in deprecated_identifiers.
replaced_by
string
With deprecated: the command to use instead, as its path below this spec’s root command (purge, remote add), without the program’s name. Help, man and markdown add use <program> purge instead to the deprecation note, written with the name the program runs as, and rotini.Deprecations reports it as ReplacedBy. rotini validate checks that the command exists and isn’t itself deprecated.
stability
string · one of experimental, beta
How settled this command is. ’experimental’: it may change or be removed in any release. ‘beta’: it may change in a minor release. Unset means stable. Help, man and markdown show (experimental) or (beta) beside it in the parent’s command list, and a line on its own pages; the contract carries it. Its inputs, sub-commands, output and exit codes are as unstable as it is. rotini diff rates any change to an experimental item as safe, and a breaking change to a beta item as possibly breaking. Nothing changes at run time: to gate an experimental command, check in its handler.
metadata
object
Your own labels for this command, for your tools to read: string keys, each with a string value ({owner: platform-team, tier: internal}). Rotini never acts on them and shows them on no page (help, man, markdown, completion). It copies them as written into the generated Definition (CommandDef.Metadata, or Definition.Metadata on the root), where a handler reads them as rtx.Command().Metadata, and into the contract. rotini diff rates any change to them as safe. Sub-commands don’t inherit them. Set next to a $ref, they replace the composed command’s.
Inputs
flags
array of FlagInput
Flag inputs for this command. Each applies only when this command is the one run, unless it is cascading.
arguments
array of ArgumentInput
Positional argument inputs for this command
env
array of EnvInput
Environment-variable inputs for this command
config
array of ConfigInput
Config-value inputs for this command, bound by key from an in-scope config_files source (declared on this command or any ancestor — see config_files).
stdin
Declares expected stdin format and schema for this command
config_files
array of ConfigurationFile
Configuration files this command reads values from, each at a fixed ‘path’ or found with ‘discover’. Files cascade: a command can read from the files declared on it and on every command above it, so a ‘config’ input here or on any sub-command may pin itself (schema ‘file:’) to a file declared here or on any ancestor. Only the files along the invoked command’s path are loaded; files declared on other branches are never read. File names must be unique along a path (a collision is an error), and declaring the same physical file at two levels is a warning. When two files in scope define the same key, the one declared nearest the invoked command wins, and within one command’s list the first declared wins, so list the more specific file (a project’s) before the more general one (the user’s). In a composed CLI, a $ref’d child’s files travel with its commands, so parent child cmd reads what child cmd reads without the parent declaring anything.
env_prefix
string
Root only: a prefix for every environment-variable name rotini derives. Derived names are the UPPER_SNAKE forms of plain env inputs without ‘variable:’ (input ‘home’ → ACME_HOME), of nested env families without ‘variable:’ (the family’s base name), and of flags’ environment fallbacks (key ‘server.port’ → ACME_SERVER_PORT). A name set explicitly with ‘variable:’ is used exactly as written and is never prefixed. With a prefix declared, the unprefixed name does not bind: input ‘home’ reads ACME_HOME, not HOME. Write it in UPPER_SNAKE with no trailing underscore (rotini adds the ‘_’). The derived name is written into the generated field’s env: tag when you generate, so the name is fixed in the code. Help lists env: inputs by name under its Environment section. Help also shows each flag’s environment fallback, and its configuration key when the command reads configuration files, on a line under the flag.
In a composed CLI, a $ref’d child’s env_prefix travels with its commands: a parent that declares none adopts the child’s, a parent that declares one wins, and two children with different prefixes are rejected.
flag_groups
array of FlagGroup
Cross-flag presence rules validated at parse time (e.g. mutually exclusive output formats, a required-together credential pair). Groups that contradict each other or a flag dependency are rejected, as is a required flag in a mutually_exclusive group with no env or config fallback.
flag_dependencies
array of FlagDependency
Conditional cross-flag requirements validated at parse time: when one flag is set, others become required (e.g. when –tls is set, –cert and –key are required).
response_files
Root only: read response files. A word starting with the prefix (@args.rsp) is replaced by the file’s lines, one argument per line, before the command line is parsed.
flag_sets
object
Root only: flags declared once, by set name, for commands to add with use:. A command that uses a set gets copies of its flags after its own, in use order, with its flag groups and dependencies; from then on they are ordinary flags of that command, for parsing, help, completion and the contract. The generated code has one struct per set, embedded in each using command’s flags struct, so a handler reads in.TaskrList.Flags.Format whether the flag is its own or from a set.
A set lives in the spec that declares it: a composed child uses its own sets, never its parent’s.
use
array of string
The flag_sets this command adds, by name, in order. Each set’s flags come after the command’s own. A set name and a flag of the command (or of another set it uses) can’t declare the same flag name, identifier or generated field name. Not valid next to $ref.
Sub-commands and composition
commands
array of Command
Sub-commands of this command, declared inline or composed with $ref. On a $ref command these are added to the composed child’s own sub-commands (see ‘$ref’); a name or alias that collides across the combined list is an error.
$ref
string
The spec file whose root command is mounted here as this sub-command. Not valid on the root command. Two forms are accepted:
- A relative path, resolved against this spec file’s directory (
$ref: ../db/.rotini.spec.yaml). mod://<module>@<version>/<path>, a spec inside another Go module ($ref: mod://github.com/acme/db@v1.4.0/cmd/db/.rotini.spec.yaml). The version is required, and <path> is the spec file’s path inside that module. The module is read from the module cache withgo mod download, so it is verified against go.sum; add it to go.mod first (go get github.com/acme/db@v1.4.0). A relative $ref inside that spec resolves within the same module and cannot leave it.
Git and https URLs are not accepted. Either way, the mounted command uses the handlers of the composed spec’s own generated package unless handler: names another.
The composed spec is the base, and the parent can adjust it where it is mounted:
- Identity and presentation keys declared next to the $ref (name, aliases, summary, description, usage, header, footer, examples, headings, groups, help, man, markdown, exit_status, see_also, group, hidden, deprecated, deprecated_identifiers, filename, plugin_path, hidden_aliases, replaced_by, metadata) replace the child’s, for that one mounted command only. The child’s own sub-commands keep theirs, so a parent can tailor the child for its tree without forking it.
- A ‘commands:’ list next to the $ref is added to the child’s own sub-commands: its inline entries get their own handler files, and its $ref entries are mounted as further children.
handler:on a $ref command points it at a different handler package.- Keys the handler depends on (flags, arguments, env, config, config_files, stdin, flag_groups, flag_dependencies, use, output, plugins, plugin_discovery, passthrough, options_first) cannot be changed here: the mounted command runs the child’s handler, built against the child’s own inputs and output, so validation rejects them. Declare them in the child spec.
The child’s own plugins, plugin_discovery, passthrough and options_first travel with it. rotini validate and generate on the parent also check every spec composed by a relative path as its own document, reporting problems at their position in that file. A spec composed with mod:// is not checked that way: it belongs to its own module, which validates it.
handler
Use a handler from another Go package for this command instead of a generated handler file. Valid on any sub-command, not the root.
On a ‘$ref’ command it replaces the default: a composed local or mod:// spec normally uses the handlers of its own generated package, and this points the command at a different package. On an inline command, the command’s tree and typed inputs are still generated here, but its handler comes from the package and no handler file is written. It applies to this command only: an inline sub-command without its own ‘handler:’ still gets a generated handler file.
The package must export a constructor ‘<convention>() rotini.Handler’ for each command it serves (the usual five-hook handler; hooks it does not implement default to no-ops), and the generated code calls ‘pkg.<Convention>()’. The compiler enforces this, since rotini cannot type-check another package.
passthrough
boolean
When true, every token after this command’s own name binds as a raw positional — no flag parsing, no unknown-flag errors, no ‘–’ needed (the wrapper-CLI case: mytool exec ls -la forwards ‘-la’ verbatim, and a literal ‘–’ passes through too). Tokens BEFORE the command (ancestor flags) parse normally. A passthrough command declares no flags, no sub-commands, no declared plugins or discovery, and its last argument must be a variadic ‘[]string’ — the receiver of the raw tokens (validation enforces all of this). Shell completion offers nothing past the boundary, falling back to file completion. To parse the command’s own flags first and take the raw words only from one argument on, mark that argument passthrough: true instead.
options_first
boolean · default false
When true, flags must come before this command’s first argument: from that word on, every word is an argument, including flag-shaped words and a later --, as POSIX utilities parse (ssh host -v passes -v on). Words before the command’s name (ancestor flags) and a -- typed before the first argument work as usual, and a short-circuit flag such as --help after the first argument is an argument too. It applies when this command is the one invoked; sub-commands don’t inherit it. A command whose last argument is a passthrough argument already stops at that argument. Can’t be combined with passthrough: true, which parses no flags at all.
plugins
array of PluginSpec
Declared plugins: separate executables dispatched as first-class sub-commands of this command.
plugin_discovery
Auto-expose external ‘<prefix>*’ executables as plugin sub-commands of this command (kubectl/git/gh plugin discovery), in addition to any declared plugins. Presence enables discovery.
plugin_path
string
Extra directory to search for this command’s plugin binaries, in addition to the host binary’s own directory and PATH. A relative path is resolved against the run’s directory; $VAR references and then a leading ~ are expanded when the program runs, and a directory that does not exist yet is simply empty. On a $ref node it overrides the composed child’s own. It applies to BOTH kinds of plugin: the ‘plugins’ this spec declares and anything ‘plugin_discovery’ finds — they are the same binaries in the same place, so they are configured once here rather than per-mechanism. Without it, a declared plugin could only ever be installed next to the host binary or on PATH, which is the git/kubectl convention and not always the right one for a vendored or bundled plugin. Search order is fixed and the same for both: next to the host binary, then this directory, then PATH — so a plugin shipped beside the binary always wins over one found here, and a failure names the locations it actually searched.
timeout
string
Not supported on a local command and rejected by rotini validation: a timeout is a plugin-only, host-side bound on a dispatched binary, so it has no effect on local execution. Set it on a plugins[] entry’s ’timeout’ instead. (Recognized here only so validation can give that targeted error rather than a generic ‘unknown property’.)
multicall
boolean or Multicall
Root only: dispatch on the name the binary was invoked as, so one binary serves several names through links or copies. true is busybox style: a binary named after a top-level command (a symlink ls to the binary) runs it, as if the user had typed <root> ls. The object form sets a prefix to strip first, a prefix that answers shell completion, or both. The name is the base name the binary was run as, never a resolved symlink; on Windows a trailing .exe is dropped and names match without regard to case. A name that matches no command, the root’s own included, runs the root as usual. Help pages still show the root’s invocation (busybox ls).
Documentation
summary
string
Short one-liner describing this command. It is shown next to the command in its parent’s generated Commands list (so it applies even when a verbatim ‘help’ string is set), and it is also the NAME line of the man page, the opening line of the markdown page, and the lead of the command’s own help page when no ‘description’ is set.
description
string
Long description block shown atop this command’s generated help page. Ignored when ‘help’ (verbatim) is set.
usage
string
Usage-line override. When omitted, rotini derives <command path> [flags] <command> <arguments> from the command’s shape. Ignored when ‘help’ is set.
display_name
string
Root only: the name the generated help, man and markdown pages show for the program, in place of the root’s ’name’. For a plugin, which a host runs as <host>-<name> but the user types as <host> <name>: with name: kubectl-ctx and display_name: “kubectl ctx”, every derived usage line reads kubectl ctx use [flags] <name>, the man page’s SYNOPSIS kubectl ctx use … and the markdown title # kubectl ctx use. It may contain spaces. It changes presentation only: ’name’ still matches the binary and names the generated page files and man pages (kubectl-ctx-use.1), completion scripts still register for ’name’ (the shell completes the binary), and routing, handler names and Context.CommandPath() are unaffected. Text the author writes verbatim (usage, footer, examples, a verbatim help/man/markdown page) is not rewritten. A composed child’s display_name is ignored; the composing parent’s root decides.
examples
array of string
Example command-line invocations, rendered one per line. Ignored when ‘help’ is set.
exit_status
array of ExitStatusEntry
Exit codes this command documents, rendered as an EXIT STATUS section in the man and markdown pages. The runtime does not check it, and sets no exit code of its own except two. A recorded error or a recovered panic exits 1 when no handler set a code, and the default signal handling exits 128+n on signal n (130 for Ctrl-C). So a command that documents 2: invalid input here and only calls rtx.RecordError will exit 1. Set the code in the handler with rtx.Exit or rtx.HaltWithCode to make the program agree with this section. rotini generate warns when a command’s handler sets an exit code (a literal or constant passed to rtx.HaltWithCode or rtx.Exit) that this list doesn’t include, and rotini validate reports a code listed twice and warns about a code above 128, which a signal exit also uses. Code 0 needs no entry, and a command that prints its help when called without a sub-command exits 1, so list 1 for it. Ignored when ‘man’ (verbatim) is set.
see_also
array of string
Cross-references rendered as a SEE ALSO section in the man page (e.g. related commands or man pages like ‘rotini-generate(1)’, or URLs). Ignored when ‘man’ (verbatim) is set.
group
string
Group label for organizing this command under a heading in its parent’s generated Commands list. Commands sharing a group are bucketed together, and ungrouped commands fall under the default Commands heading. Groups appear in the order their first member is declared, unless the parent declares groups: then the ungrouped commands come first, then the groups it lists, in its order, then any others. Presentation-only.
groups
array of HelpGroup
Describes the group headings on this command’s own pages: the groups its sub-commands’ group and its flags’ group name. Each entry gives a group a description, shown under its heading in help, man and markdown, and the list’s order is the order the groups appear. A label used by both a sub-command and a flag shares one entry. Presentation only, and not the same as ‘flag_groups’, which validates combinations of flags. rotini validate warns about an entry no sub-command or flag uses.
header
string
Text rendered above the description block. Ignored when ‘help’ is set.
footer
string
Text rendered at the bottom of the page. Ignored when ‘help’ is set.
headings
Section heading overrides for the generated page; sane defaults fill any unset heading. Ignored when ‘help’ is set.
help
string
Exact, verbatim help page for this command. When set, rotini writes it byte-for-byte (no rendering; terminal styling kept) and ignores the structured help fields (description/usage/header/footer/examples/headings); ‘summary’ is still used in the parent’s Commands list. When unset, rotini generates the page from the structured fields.
man
string
Exact, verbatim man page for this command, written in roff, the markup the man program reads (the man feature’s per-command escape, mirroring ‘help’). When set, rotini writes it as given — byte-for-byte except that ANSI styling is removed, since a man page carries none — and ignores the structured doc-fields for the man page; when unset, the page is rendered as roff from those fields through the man template. Either way the page is named after the command path joined with ‘-’ and lowercased, with the man section as its extension (deploy-status.1).
markdown
string
Exact, verbatim markdown reference page for this command (the markdown feature’s per-command escape, mirroring ‘help’/‘man’). When set, rotini writes it as given — byte-for-byte except that ANSI styling is removed; when unset, the page is rendered from the structured doc-fields through the markdown template.
topics
array of Topic
Root only: help topics, pages that aren’t commands. <program> help <topic> shows one through the generated Help function (as the seeded help command does), the root’s help lists them, the man and markdown features write a page for each, and help <TAB> completes their names along with the commands’. A topic’s name can’t be a root command’s name or alias.
Output and shared types
output
The shape of what this command writes to stdout when it succeeds, as a schema. Rotini generates a typed ‘<Prefix>Output’ Go type (when the shape is a ‘$ref’ to a document-level schema, a new named type defined on that schema’s type, such as ’type <Prefix>Output Task’, so convert a value with <Prefix>Output(v)), documents the shape in an OUTPUT section of the help, man and markdown pages, and describes it in the output schema files and the contract document. It describes the shape only: it adds no flag and wires no format. How the output is written, and in which format, is the handler’s own code — rtx.WriteOutput is an optional helper that writes json, yaml or toml, hands any other format to a renderer, and checks the value is this type. A command that writes a stream of items declares the shape of one item, and a stream whose items come in several kinds declares them with oneOf.
output_stream
boolean · default false
When true, the command writes a stream of items rather than one value, and output: declares one item. Write each item with rtx.WriteOutputItem: JSON is one compact value per line, YAML one document per item, each starting ---. Help, man and markdown say so in the OUTPUT section, and the contract marks the command stream: true. Needs output. rotini generate warns when a handler calls WriteOutputItem on a command without it. When the items come in several kinds (progress updates, then a result), output: lists one schema per kind under oneOf, each tagged by the same required string property holding a const (kind: {type: string, const: progress}); see oneOf.
schemas
object
Document-level (root only): reusable named schema definitions. Referenced elsewhere by name, $ref: <Name>, or as a pointer, $ref: "#/schemas/<Name>". Each may carry a description, which becomes the generated type’s doc comment. Names must be PascalCase Go-exportable identifiers — each becomes a generated Go type in the cmd package (or in the models package, when the conf declares one), which other packages may import.
Generated code
filename
string
Override the name of this command’s generated handler-stub .go file (in the cli package). Defaults to a name derived from the command path (’<root><path>.go’, every ‘-’ written ‘’: config_get_contexts.go), reserved-name-escaped so a command named ’test’/’<GOOS>’/’<GOARCH>’ does not collide with Go’s filename rules. Must end in ‘.go’, must not itself be a name Go reads specially (’test.go’, ‘<GOOS>.go’, ‘_<GOARCH>.go’), and must be unique among the commands generated into the same package. Renaming it orphans (and prunes) the previous stub file — move your handler code first.
Agents
effects
What running this command does, for the people and the programs (such as AI agents) that run it. Help, man and markdown show it as a line (Effects: destructive, not idempotent), and the contract, the generated Definition (CommandDef.Effects), tool exports, the agent skill page and permission snippets carry it. Unset says nothing, and a program reading the contract then assumes the worst: destructive, reaching outside the machine. Sub-commands don’t inherit it. A flag’s own effects raises it for a run that gives the flag. Not allowed on a $ref node: the child spec, whose handler runs, declares it. Nothing changes at run time.
agent
boolean or null
Whether this command and its sub-commands are offered to AI agents: in tool exports, the agent skill page, llms.txt, permission snippets and the MCP companion. false leaves the subtree out, while it stays in help. true brings back a command left out by default: a hidden or deprecated one, one with sub-commands (which prints help when run bare), or a passthrough command (which would run whatever it is given). Unset follows those defaults. Nothing changes at run time.
FlagInput
name
string · required
Logical name for the flag
agent
boolean or null
Whether this flag is a tool parameter for AI agents (tool exports and the MCP companion). false leaves it out, while it stays in help. true brings back a flag left out by default: a hidden, deprecated, secret or short-circuit flag, or one that reads from: a file or stdin (which would let an agent read the host’s files, or put a secret in the model’s context). Unset follows those defaults.
cascading
boolean · default false
When true, this flag applies to its command and to every command below it: it may be written anywhere after its command’s name, after its sub-commands’ names too, and every descendant’s generated help lists it (under the ‘Global Flags’ section). When false (the default), the flag applies only when its own command is the one run: given with any of its sub-commands, before or after their names, it is a usage error naming the command it belongs to, and descendants’ help and completion leave it out. A flag is never accepted before its own command’s name (one written before a sub-command’s name belongs to an ancestor, which is how two commands may declare the same flag). Environment and configuration values are unaffected.
deprecated
string
Deprecation message. The flag is annotated as deprecated in generated help, and using it is reported at run time by rotini.Deprecations with this message (a data feed for the handler; rotini itself prints nothing). With deprecated_identifiers, only those spellings report — the others are the ones to move to (identifiers: [--db, --database], deprecated_identifiers: [--database], deprecated: use --db); without, the whole flag is deprecated and every spelling reports.
deprecated_identifiers
array of string
CLI tokens for this input that are deprecated — a subset of its identifiers (flags) or aliases (commands). When one of these is used on the command line, rotini’s Deprecations surfaces it as a data point for the handler to act on (warn, emit telemetry, etc.); the framework itself does nothing. Tokens not listed here are unaffected. Pair it with deprecated: to give the report a message; deprecated: alone deprecates every spelling
deprecated_identifiers_removed_in
object
The release that removes each deprecated identifier, by identifier: {--conf: 2.0.0} plans the removal of the spelling --conf while the flag stays under its other identifiers. Each key must be listed in deprecated_identifiers. rotini validate --release checks these like removed_in, and the contract carries them.
deprecated_since
string
The release that deprecated this flag (X.Y.Z), shown beside the deprecation in help, man and markdown, written into the contract, and reported by rotini.Deprecations as Since. Needs deprecated.
description
string
Longer text about this flag, paragraphs separated by blank lines, shown under it in man and markdown pages and used as its description in the contract’s parameters. Help shows summary only; an editable help template can show both, as .Description.
effects
What giving this flag adds to its command’s effects. A run’s effect is the highest of its command’s and every given flag’s (read, then write, then destructive); it is idempotent only when every stated value says so, and reaches outside the machine when any does. A flag can only raise its command’s effect, so its command must declare effects with a lower kind; mark a preview flag with role: dry-run instead. A cascading flag raises whichever command it is given to. Help, man and markdown show the kind beside the flag ((destructive)), and the contract and the generated FlagDef.Effects carry it.
group
string
Group label that puts this flag under its own heading in generated help, the way a command’s ‘group’ does in the Commands list: flags sharing a group appear together, and ungrouped flags fall under the default Flags heading. Groups appear in the order their first member is declared, unless the command declares groups: then the ungrouped flags come first, then the groups it lists, in its order, then any others.
Presentation only: parsing, precedence and the generated field are unchanged. Use it on a command with many flags, so its help page is easy to scan. Not to be confused with ‘flag_groups’, which validates combinations of flags.
hidden
boolean · default false
When true, the flag is omitted from generated help (it still parses on the command line).
hidden_identifiers
array of string
Identifiers the command line accepts for this flag but that are never listed: help, completion and suggestions show only identifiers. Use them for old spellings kept for scripts. A hidden long identifier of a negatable flag gets its --no- form too, unlisted as well. They follow the rules of identifiers (no clash on the command path), and may be listed in deprecated_identifiers.
identifiers
array of string
CLI flag identifiers (e.g., ‘–force’, ‘-f’). When absent, ‘–<name>’ is derived from the flag’s name, with ‘_’ written as ‘-’ (‘dry_run’ → –dry-run). rotini validate warns about a flag with no long form and about a one-dash word of several letters (’-name’), which POSIX tools read as a bundle of short flags; it rejects such a word when the command’s short flags spell it as a bundle. Reusing an identifier of a cascading ancestor flag is a warning, since after this command’s name it means this flag; it is an error when this command’s flags take every identifier of that flag, which then can’t be given after this command’s name at all. A flag of the same name that redeclares the switch is not reported.
A single dash and one digit (-4) declares a digit option, on a bool or count flag (identifiers: ['-4', --ipv4]; quoted, since a bare -4 is a number in YAML). A word starting with that dash and digit is then the flag, on this command and every command below it, while other negative numbers still parse as numbers. rotini validate rejects it when an argument of this command or one below it takes negative numbers.
metadata
object
Your own labels for this flag, for your tools to read: string keys, each with a string value. Rotini never acts on them and shows them on no page. It copies them as written into the generated FlagDef.Metadata and the contract, and rotini diff rates any change to them as safe.
removed_in
string
The release that will remove this flag (X.Y.Z), later than deprecated_since. Shown like deprecated_since and reported as RemovedIn. rotini validate --release <X.Y.Z> (or the variable the conf’s validate.release_env names) fails while the flag is still declared at or past that release. Needs deprecated.
replaced_by
string
With deprecated: the flag to use instead, as one of its identifiers (--output). It is a flag of this command or a cascading flag of an ancestor. Help, man and markdown add use --output instead to the deprecation note, and rotini.Deprecations reports it as ReplacedBy. rotini validate checks that the flag exists and isn’t itself deprecated.
role
string · one of force, chdir, fields, sort, dry-run, confirm, machine-output, page
What this flag means to a program driving the CLI, such as an agent. ‘force’ marks the bool flag that lets the command replace existing output files (the handler passes it on, as in rotini.CreateOutput(rtx, path, rotini.Overwrite(in.Flags.Force))). ‘fields’ marks the list flag that chooses which output fields are written (--json id,title, applied with rotini.SelectFields), and ‘sort’ the string flag that names the field to sort by (--sort-by title, applied with rotini.SortBy); each takes its values from an enum or values_from. These three change nothing at run time. At most one flag per command has a given role.
‘chdir’ is the one role the runtime acts on: a directory flag like git’s -C dir, spelled with whatever identifiers the flag declares (-C, -w, --workdir or any other), on the root, cascading, read from the command line only (no default, env or config fallback, or from:), one per program. Rotini reads it before anything else, and resolves walk-up config discovery, relative paths, @file values, path checks and the directory plugins run in against it (Context.Dir returns it). It never changes the process’s working directory, so a handler joins relative paths with rtx.Dir(). A relative value is resolved against the run’s directory, and given more than once, the last wins.
‘dry-run’ marks the bool flag that previews a run without changing anything, and ‘confirm’ the bool flag that answers yes for a command that would otherwise refuse or prompt. ‘machine-output’ marks the flag that makes the command write its declared output as JSON: a bool flag, or a string flag with an enum and role_value naming the value that selects JSON (--format json). ‘page’ marks the flag that selects a page of results. They change nothing at run time either; the contract, tool exports and the agent skill page read them, and a command has each at most once, counting the cascading flags it inherits.
role_value
string
With role: machine-output on a string flag: the enum value that selects JSON output (json for --format json). Tool exports and the MCP companion add --format=json to the command line themselves and leave the flag out of the tool’s parameters. Only on a machine-output flag with an enum that lists it.
schema
Type definition and input-level metadata (type, required, default, enum, nullable, constraints)
short_circuit
boolean · default false
When true, setting this flag on the command line waives every declared requirement of the invoked command chain: required inputs, enums, bounds, patterns, flag groups and flag dependencies are not checked, and rtx.Inputs succeeds. Use it for flags that replace the command’s normal run, such as –help, –version or –print-schema. Errors in reading the command line (an unknown flag or command, a value of the wrong type, too many arguments) are still reported, as is an environment value of the wrong type. A configuration file that can’t be found, parsed or read into its inputs is skipped for that run, so –help works in a broken directory. Rotini takes no action of its own: your handler checks the flag and decides what to do. Only the command line sets it, never an environment variable, a configuration file or a default. Must be a bool, and can’t be required, negatable, given a default of true, read from the environment or a configuration file (key, variable), or listed in a flag group or flag dependency.
stability
string · one of experimental, beta
How settled this flag is. ’experimental’: it may change or be removed in any release. ‘beta’: it may change in a minor release. Unset means stable. Help, man and markdown show (experimental) or (beta) beside it, and the contract carries it. A flag is never more stable than its command: a flag of an experimental command is experimental too. rotini diff rates any change to an experimental item as safe, and a breaking change to a beta item as possibly breaking. Nothing changes at run time.
summary
string
Short one-liner shown next to this flag in the Flags section of generated help.
ArgumentInput
name
string · required
Logical name for the argument
agent
boolean or null
Whether this argument is a tool parameter for AI agents (tool exports and the MCP companion). false leaves it out, while it stays in help. true brings back an argument left out by default: a hidden, deprecated or secret one, or one that reads from: a file or stdin. Unset follows those defaults. A required argument left out leaves its command out of the tool exports, with a warning.
deprecated
string
Deprecation message. The argument is annotated as deprecated in generated help, and supplying it is reported at run time by rotini.Deprecations with this message (a data feed for the handler; rotini itself prints nothing).
deprecated_since
string
The release that deprecated this argument (X.Y.Z), shown beside the deprecation in help, man and markdown, written into the contract, and reported by rotini.Deprecations as Since. Needs deprecated.
description
string
Longer text about this argument, paragraphs separated by blank lines, shown under it in man and markdown pages and used as its description in the contract’s parameters. Help shows summary only; an editable help template can show both, as .Description.
hidden
boolean · default false
When true, the argument is omitted from generated help (it still parses on the command line).
metadata
object
Your own labels for this argument, for your tools to read: string keys, each with a string value. Rotini never acts on them and shows them on no page. It copies them as written into the generated ArgDef.Metadata and the contract, and rotini diff rates any change to them as safe.
passthrough
boolean · default false
When true, raw words start at this argument: flags before it parse as usual, and it and every word after it are taken as typed, flag-shaped words and -- included (app exec --region eu ls -la --help gives the command’s --region and passes ls -la --help on). A word starting with - that should be the first raw word goes after -- (app exec -- -x). It must be the command’s last argument, a variadic []string without a separator, enum, pattern, bounds or uniqueItems, on a command with no sub-commands, plugins or command-level passthrough. A handler reads where a -- was typed with rtx.DashIndex(). Shell completion offers no flags or sub-commands past the boundary; the argument’s own completer, enum or hint still applies.
removed_in
string
The release that will remove this argument (X.Y.Z), later than deprecated_since. Shown like deprecated_since and reported as RemovedIn. rotini validate --release <X.Y.Z> fails while the argument is still declared at or past that release. Needs deprecated.
schema
Type definition and input-level metadata (type, required, default, enum, nullable, constraints)
stability
string · one of experimental, beta
How settled this argument is. ’experimental’: it may change or be removed in any release. ‘beta’: it may change in a minor release. Unset means stable. Help, man and markdown show (experimental) or (beta) beside it, and the contract carries it. An argument is never more stable than its command. rotini diff rates any change to an experimental item as safe, and a breaking change to a beta item as possibly breaking. Nothing changes at run time.
summary
string
Short one-liner shown next to this argument in the Arguments section of generated help.
EnvInput
name
string · required
Logical name for this env var input
agent
boolean or null
Environment variables are never tool parameters: on an MCP server they come from the server’s own environment, where secrets belong. false leaves this variable out of the agent skill page’s list of variables, and true lists it there when it is hidden. The contract carries it.
deprecated
string
Deprecation message; the input is annotated as deprecated in generated help. (Run-time deprecation reporting covers what argv carries — commands, flags and arguments.)
deprecated_since
string
The release that deprecated this input (X.Y.Z), shown beside the deprecation in help, man and markdown and written into the contract. Needs deprecated.
description
string
Longer text about this environment variable, paragraphs separated by blank lines, shown under it in man and markdown pages and in the contract. Help shows summary only; an editable help template can show both, as .Description.
hidden
boolean · default false
When true, the input is omitted from generated help (it is still bound).
metadata
object
Your own labels for this environment variable, for your tools to read: string keys, each with a string value. Rotini never acts on them and shows them on no page. It copies them as written into the contract, and rotini diff rates any change to them as safe.
removed_in
string
The release that will remove this input (X.Y.Z), later than deprecated_since. Shown like deprecated_since. rotini validate --release <X.Y.Z> fails while the input is still declared at or past that release. Needs deprecated.
schema
Type definition and input-level metadata (required, default, variable)
stability
string · one of experimental, beta
How settled this environment variable is. ’experimental’: it may change or be removed in any release. ‘beta’: it may change in a minor release. Unset means stable. Help, man and markdown show (experimental) or (beta) beside it, and the contract carries it. An input is never more stable than its command. rotini diff rates any change to an experimental item as safe, and a breaking change to a beta item as possibly breaking. Nothing changes at run time.
summary
string
Short one-liner shown next to this input in the generated Environment/Configuration help section.
ConfigInput
name
string · required
Logical name for this config value
agent
boolean or null
Config values are never tool parameters: on an MCP server they are the server’s own configuration. false leaves this value out of the agent skill page’s list of configuration, and true lists it there when it is hidden. The contract carries it.
deprecated
string
Deprecation message; the input is annotated as deprecated in generated help. (Run-time deprecation reporting covers what argv carries — commands, flags and arguments.)
deprecated_since
string
The release that deprecated this input (X.Y.Z), shown beside the deprecation in help, man and markdown and written into the contract. Needs deprecated.
description
string
Longer text about this config value, paragraphs separated by blank lines, shown under it in man and markdown pages, in the contract and in the configuration file’s JSON Schema. Help shows summary only; an editable help template can show both, as .Description.
hidden
boolean · default false
When true, the input is omitted from generated help (it is still bound).
metadata
object
Your own labels for this config value, for your tools to read: string keys, each with a string value. Rotini never acts on them and shows them on no page. It copies them as written into the contract, and rotini diff rates any change to them as safe.
removed_in
string
The release that will remove this input (X.Y.Z), later than deprecated_since. Shown like deprecated_since. rotini validate --release <X.Y.Z> fails while the input is still declared at or past that release. Needs deprecated.
schema
Type definition and input-level metadata (required, default, file, key)
stability
string · one of experimental, beta
How settled this config value is. ’experimental’: it may change or be removed in any release. ‘beta’: it may change in a minor release. Unset means stable. Help, man and markdown show (experimental) or (beta) beside it, and the contract carries it. An input is never more stable than its command. rotini diff rates any change to an experimental item as safe, and a breaking change to a beta item as possibly breaking. Nothing changes at run time.
summary
string
Short one-liner shown next to this input in the generated Environment/Configuration help section.
StdinSpec
format
string · one of json, yaml, jsonc, toml, text, lines, jsonl, bytes · default json
How the piped stdin payload is read, and the Go type of the generated Stdin field.
The four document formats (json, yaml, jsonc, toml) decode it into the generated <Prefix>Stdin type (*<Prefix>Stdin), validated against the declared schema. The default is json.
‘jsonl’ reads JSON Lines: one JSON record per line, blank lines skipped, each checked against the schema, which describes one record. The field is *[]<Prefix>Stdin, or with stream: true an iterator, iter.Seq2[<Prefix>Stdin, error]. An error names the line it was found on.
The raw formats are for commands whose stdin is not a document, such as text filters: ’text’ binds the whole payload as one string (*string), ’lines’ binds its lines (*[]string, a trailing newline adds no empty element; with stream: true, iter.Seq2[string, error]), and ‘bytes’ binds the payload byte for byte (*[]byte), nothing trimmed. Their schema type is implied when left out (‘string’ for text and bytes, ‘[]string’ for lines); none generates a <Prefix>Stdin type, because there is nothing to shape. Declaring stdin this way, rather than reading rtx.Stdin directly, puts it in the command’s help, man and markdown pages and the contract.
One leading UTF-8 byte-order mark is removed before the payload is read, in every format but ‘bytes’. A declared stdin that is not streamed is held in memory for the run.
metadata
object
Your own labels for this command’s stdin, for your tools to read: string keys, each with a string value. Rotini never acts on them and shows them on no page. It copies them as written into the contract, and rotini diff rates any change to them as safe.
schema
Type definition for stdin content. Set required: true in schema to error when stdin is empty. The document formats need a type or $ref here; the raw formats imply theirs, so schema: may be left out or hold only required.
separator
string · one of nul
With ’lines’ only: split the payload on NUL bytes instead of newlines, as find -print0 and xargs -0 write it. Items are kept byte for byte (no \r or space trimming, since a file name may contain either), and a trailing NUL adds no empty item. The default splits on newlines.
stream
boolean · default false
With ’lines’ or ‘jsonl’ only: generate an iterator (iter.Seq2[string, error], or iter.Seq2[<Prefix>Stdin, error]) instead of a slice, so a filter reads one line or record at a time in constant memory. Nothing is read until the handler ranges over it, and stdin is read once per run: a second range, or a second Inputs call, continues where the last stopped. The field is nil when stdin is not read (a terminal, a short-circuit flag such as –help, or unless_argument choosing the files), so check it before ranging. A read error, a record that fails its schema, or a cancelled run is yielded once as the error, and the iterator then stops. A line or record is limited only by memory. CheckInputs and InputReport.Validate carry a stream but don’t check its items: they are checked as they are read.
unless_argument
string
The name of an optional file argument of the same command (inputfile or existingfile, or a list of either): stdin is read only when that argument got no value, or its value is (or, for a list, contains) -. Otherwise the Stdin field stays nil and nothing is read. This is the cat [FILE...] and jq FILTER [FILE] shape. Without it, a declared stdin is read on every run, even when a file is given, which waits forever when a caller holds stdin open. With required, an empty stdin is an error only when no file is given.
ConfigurationFile
name
string · required
Logical name for the config file (e.g. ‘app-config’). It anchors per-input pins (schema ‘file:’) and config_source claims, so it must be unique within its chain (this command and its ancestors) — a name collision in scope is an error.
as
string · one of config, env · default config
What the file supplies. ‘config’ (the default): configuration values, read by config inputs and flag config fallbacks. ’env’: environment variables, the .env shape: a format: dotenv file whose KEY=value lines are read as if they were set in the environment, by env inputs, flag environment fallbacks, variable_file and config_source variables. The real environment wins over the file, variable by variable, so the precedence is command line, environment, .env file, configuration files, default. Values are literal: ${OTHER} is not expanded. Rotini’s own variables (HOME, XDG_*, PATH) are never read from it. Requires format: dotenv (or a .env file name); a config input can’t pin it with file:. Several as: env files in scope layer like config files: the nearest command’s first, then in declaration order. Prefer a fixed path: .env to walk-up discovery, which could find a stray ~/.env.
discover
Locate this file at run time instead of a fixed ‘path’. Exactly one of ‘path’ or ‘discover’ must be set.
format
string · one of json, yaml, toml, jsonc, dotenv
The file’s format. When omitted, the format is inferred from the file extension; declare it when the extension is missing or misleading. ‘jsonc’ is JSON with comments and trailing commas. ‘dotenv’ reads KEY=value lines whose keys are used as written: a config input reading one declares key: API_ENDPOINT, not a dotted path.
path
string
File path. A leading ~ or ~user and $VAR or ${VAR} references (with the shell forms ${VAR:-x}, ${VAR:?message} and the like) are expanded from the run’s environment, and a relative path is resolved against the run’s directory. Exactly one of ‘path’ or ‘discover’ must be set.
profiles
Named sets of settings in this file, one chosen per run by a declared input (--profile prod, ACME_PROFILE=prod). The keys under <under>.<name> are read as if they sat at the top of the file and win over the file’s other top-level keys, which every profile shares. Merging is per key: a list is replaced, not joined, and a map’s entries merge. Precedence is otherwise unchanged: command line, environment, configuration files (the selected profile, then the shared keys), default. Profiles that aren’t selected are not read or checked, and the file’s schema checks the shared keys merged with the selected profile, without the <under> key. Not allowed on a format: dotenv or as: env file.
schema
Optional load-time validation: the loaded document is validated against this schema at bind time, before any value is read from it — a non-conforming file is a loud error naming the file and the violation (the same gate the stdin channel applies to its payload). Every value in a dotenv file is a string, so a schema for one uses string types; a TOML date or time is checked as its text (2024-01-02T03:04:05Z), as in YAML or JSON. The file that actually resolved — fixed path, discovered, or config_source-supplied — is the file validated; an absent file passes vacuously (absence is the per-input required’s concern). Document-level named schemas resolve via “$ref”: “#/schemas/<Name>”. No typed struct is generated from this — typed access to config values is the config: inputs channel.
FlagGroup
A constraint on which of this command’s flags may (or must) be set together. ‘flags’ references flag logical names; ‘set’ means explicitly provided on the command line (a default or env/config fallback does not count).
kind
string · required · one of mutually_exclusive, required_together, one_of, at_least_one
mutually_exclusive: at most one set. required_together: all or none. one_of: exactly one. at_least_one: one or more.
flags
array of string · required
The logical flag names the rule covers (at least two). Each must be a flag declared on the same command — validation rejects unknown names. “Set” means explicitly set on argv: defaults and env/config fallbacks neither trip nor satisfy a group.
FlagDependency
A conditional rule: when the ‘when’ flag is explicitly set on the command line (with one of the ’equals’ values, if given), and none of the ‘unless’ flags is, every flag in ‘requires’ must also be set and no flag in ‘forbids’ may be. Every name is a flag’s logical name; ‘set’ means explicitly provided (a default or env/config fallback neither triggers a rule nor satisfies one). Give ‘when’ or ‘unless’ (or both), and ‘requires’ or ‘forbids’ (or both).
equals
array of string or number or boolean
With ‘when’: trigger only when the ‘when’ flag’s value is one of these (when: format, equals: [csv, tsv]). The value compared is the one given on the command line, the last one if the flag is repeated, as the flag’s enum stores it (so ignore_case and enum aliases match). For a bool flag, true or false (--no-x gives false). Valid on a single-value flag, not a list, map or count; each value must parse as the flag’s type and be in its enum.
forbids
array of string
Flags that can’t be set when the rule triggers (when: tls, forbids: [insecure]).
requires
array of string
Flags that must also be set when the rule triggers.
unless
array of string
Flags any one of which, set on the command line, turns the rule off. Without ‘when’, the rule applies on every run unless one of these is set (unless: [config], requires: [token]).
when
string
The flag whose presence triggers the rule.
ResponseFiles
Response files: arguments kept in a file and named on the command line. Before the first --, a word starting with the prefix names a file, read relative to the working directory, whose lines become arguments: one argument per line, kept exactly (spaces included), with a trailing carriage return removed. Empty and blank lines and lines starting with # are skipped. Words read from a file are not expanded again, and a -- in a file ends expansion like one typed. To pass a word that starts with the prefix, double it: @@x is the argument @x. Words after -- are never expanded. A file that can’t be read is a usage error, even beside --help. Plugins and passthrough arguments receive the expanded words.
prefix
string · required
The one character that starts a response-file word, such as @. Not a letter, digit, - or space.
HandlerSource
Where a command’s handlers come from when they are not a generated stub: a Go package (handler delegation). The package’s typed inputs live with it; this spec contributes only the command tree.
import
string · required
Go import path of the handler package, in the same ‘alias path’ form an input type’s ‘import’ uses (e.g. ‘deployhandlers github.com/acme/clis/deploy/handlers’); identical imports are merged. A bare path takes its alias from the last path segment. The generated code calls ‘<alias>.<convention>()’.
convention
string · required
Function-name prefix the package exports per command: codegen delegates this command to ‘<alias>.<convention>()’ and each sub-command to ‘<alias>.<convention><SubPath>()’, each returning a rotini.Handler. PascalCase Go-exportable identifier.
PluginSpec
name
string · required
Name of the declared plugin. The dispatched binary is named <program>-<name>, and is searched for next to the host binary, then in the command’s plugin_path, then on PATH. Inside a $ref-composed subtree <program> is the composed spec’s own name, so one installed plugin serves both that spec’s own binary and a parent that composes it.
aliases
array of string
Additional names that invoke this plugin.
summary
string
Short one-liner shown next to this plugin in its parent’s generated Commands list.
timeout
string
Host-side timeout for running the plugin. Uses Go duration format (e.g. “10s”, “1m30s”). Empty or omitted means no timeout.
PluginDiscovery
Auto-expose external ‘<prefix>*’ executables as plugin sub-commands (kubectl/git/gh plugin style), alongside any declared plugins. Presence enables discovery; a discovered name that collides with a declared command or plugin is skipped.
hidden
boolean · default false
When true, discovered plugins still dispatch but are omitted from completion listings.
prefix
string
Executable-name prefix to discover. Default: the host binary name followed by ‘-’ (e.g. ‘acme-’).
ExitStatusEntry
code
integer · required
The exit status code being documented (0-255 — the range a process can actually return).
docs_url
string
An absolute http or https link to more about this exit code, shown in the man and markdown EXIT STATUS sections and carried into the contract. Help and the default reporter don’t show it; a reporter of your own can print it, from the generated Definition’s ExitStatusDef.DocsURL.
name
string
A snake_case name for the code, unique within the command. rotini generate writes a constant for it, <Command>Exit<Name> (not_found on taskr get gives TaskrGetExitNotFound), so a handler writes rtx.HaltWithCode(TaskrGetExitNotFound) instead of a bare number; the exit-code check reads the constant like the number. The man and markdown EXIT STATUS sections and the contract show the name too.
output
The shape stdout still carries when the command exits with this code, for an outcome that is not plain success but prints data anyway (3: some tasks failed; stdout lists what succeeded). Documented in the EXIT STATUS section and described in the output schema files and the contract document.
retryable
boolean · default false
When true, running the same command again may succeed (a timeout, a busy lock). Carried into the contract for agents and other callers that decide whether to retry; rotini itself never retries.
summary
string
What this exit code means.
HelpGroup
A group heading on a command’s pages, with an optional description.
name
string · required
The group’s label, as the sub-commands’ or flags’ group writes it.
description
string
A paragraph shown under the group’s heading in help, man and markdown.
HelpHeadings
Section heading overrides for generated help pages.
arguments
string
Heading rendered above the arguments section of the generated help page. Rendered verbatim — include any trailing ‘:’ you want. Default: “Arguments:”.
cascading
string
Heading rendered above the cascading-flags section on descendant commands’ generated help pages. Rendered verbatim — include any trailing ‘:’ you want. Default: “Global Flags:”.
commands
string
Heading rendered above the commands section of the generated help page. Rendered verbatim — include any trailing ‘:’ you want. Default: “Commands:”.
configuration
string
Heading rendered above the configuration section of the generated help page. Rendered verbatim — include any trailing ‘:’ you want. Default: “Configuration:”.
environment
string
Heading rendered above the environment section of the generated help page. Rendered verbatim — include any trailing ‘:’ you want. Default: “Environment:”.
examples
string
Heading rendered above the examples section of the generated help page. Rendered verbatim — include any trailing ‘:’ you want. Default: “Examples:”.
flags
string
Heading rendered above the flags section of the generated help page. Rendered verbatim — include any trailing ‘:’ you want. Default: “Flags:”.
output
string
Heading rendered above the Output section, which describes what the command writes when it declares output:. Rendered verbatim — include any trailing ‘:’ you want. Default: “Output:”.
stdin
string
Heading rendered above the Stdin section, which describes what the command reads from standard input when it declares stdin:. Rendered verbatim — include any trailing ‘:’ you want. Default: “Stdin:”.
topics
string
Heading rendered above the root page’s list of help topics (topics:). Rendered verbatim — include any trailing ‘:’ you want. Default: “Help Topics:”.
usage
string
Heading rendered above the usage section of the generated help page. Rendered verbatim — include any trailing ‘:’ you want. Default: “Usage:”.
Topic
A help topic: a page that isn’t a command, with its text written here (body) or generated (generate).
name
string · required
The word after help that shows the topic (demo help filters). Its man page is <program>-<name> in the man feature’s section.
summary
string · required
A one-line description, shown beside the name in the root’s list of topics, in the man page’s NAME line and at the top of the markdown page. The help page shows the body, or this summary when the topic has none.
body
string
The topic’s text, paragraphs separated by blank lines. Set exactly one of body and generate.
generate
string · one of environment
Build the topic’s text from the spec. ’environment’ lists every environment variable the program reads: env inputs, flag fallbacks, config_source and variable_file names, the completion switches the conf declares, and the XDG variables config discovery reads, each with the commands that read it. Set exactly one of body and generate.
Schema
JSON Schema-inspired type definition used for output/response and object property schemas. The ‘required’ field is a string array of required property names (JSON Schema object semantics). For input schemas where ‘required’ means ‘must be provided’, use InputSchema instead.
const
The one value this property may hold (standard JSON Schema semantics), checked wherever the shape is: output checks, stdin payloads, config files and object flags. Its Go field keeps the declared type. In a stream whose items come in several kinds (oneOf on output), each kind names itself with a required string property holding a const, such as kind: {type: string, const: progress}.
description
string
What this shape or property is, in a sentence. Rotini writes it as the Go doc comment on the generated type or struct field (a named schema, an output or stdin shape, and each of their properties), so the code a handler reads explains itself. Documentation only: it changes no validation and no field. Accepted on object schemas and their properties; an input’s own schema uses summary: on the input instead.
oneOf
array of Schema
On a command’s output with output_stream: true only: the stream’s items come in several kinds, one schema per kind, each an object written inline or a $ref to a named object schema. Every kind declares the same property, its tag, as a required string with a const naming the kind (kind: {type: string, const: progress}), and no two kinds share a value. Nothing else may sit beside oneOf.
The generated <Prefix>Output is then an interface that only the kinds’ types satisfy: a $ref kind is its named schema’s type, and an inline kind gets <Prefix>Output<Value> (<Prefix>OutputProgress for progress). rtx.WriteOutputItem takes any kind, fills in an empty tag from the item’s type, and refuses an item whose tag names another kind. rotini.DecodeOutput decodes each item into its kind’s type by reading the tag. Help, man and markdown list each kind with its fields; the contract and the output schema files describe the stream as standard JSON Schema oneOf.
required
array of string
Required property names for object schemas (standard JSON Schema semantics).
Effects
What a command does when it runs, or what a flag adds to that. Programs that run the CLI for someone, such as AI agents and their permission rules, read it to decide what needs asking first.
kind
string · required · one of read, write, destructive
‘read’: changes nothing. ‘write’: creates or changes things, without destroying what was there. ‘destructive’: deletes or overwrites something that can’t be got back.
idempotent
boolean or null
Running it again with the same inputs changes nothing more. Unset says nothing.
open_world
boolean or null
It reaches the network or other systems outside the machine. Unset says nothing, and a program reading the contract then assumes it does.
InputSchema
Extended schema for input definitions (flags, arguments, env vars, config values, stdin). Inherits all BaseSchema fields and adds input-level metadata. The ‘required’ field here is a boolean indicating whether this input must be provided — unlike Schema where ‘required’ is a string array of property names.
Which keys each kind of input accepts
An input’s schema: block takes the keys below, but not every key means something on every
kind of input — a flag’s negatable has no meaning for an environment variable, and
rotini validate rejects it there. This table is produced by validating each key on each
kind of input, so it is what the validator actually accepts. stdin: takes a JSON Schema
document instead; see StdinSpec.
| Key | flag | argument | env | config |
|---|---|---|---|---|
$ref | ✓ | ✓ | ✓ | ✓ |
base | ✓ | ✓ | ✓ | ✓ |
complete | ✓ | ✓ | — | — |
config_source | ✓ | — | ✓ | — |
default | ✓ | ✓ | ✓ | ✓ |
default_text | ✓ | ✓ | ✓ | ✓ |
dotted_keys | ✓ | — | — | — |
enum | ✓ | ✓ | ✓ | ✓ |
exclusiveMaximum | ✓ | ✓ | ✓ | ✓ |
exclusiveMinimum | ✓ | ✓ | ✓ | ✓ |
expand | ✓ | ✓ | ✓ | ✓ |
file | — | — | — | ✓ |
from | ✓ | ✓ | — | — |
glob | — | ✓ | — | — |
ignore_case | ✓ | ✓ | ✓ | ✓ |
implicit_value | ✓ | — | — | — |
import | ✓ | ✓ | ✓ | ✓ |
items | ✓ | ✓ | ✓ | ✓ |
key | ✓ | ✓ | — | ✓ |
layout | ✓ | ✓ | ✓ | ✓ |
maxItems | ✓ | ✓ | ✓ | ✓ |
maxLength | ✓ | ✓ | ✓ | ✓ |
maximum | ✓ | ✓ | ✓ | ✓ |
minItems | ✓ | ✓ | ✓ | ✓ |
minLength | ✓ | ✓ | ✓ | ✓ |
minimum | ✓ | ✓ | ✓ | ✓ |
multipleOf | ✓ | ✓ | ✓ | ✓ |
negatable | ✓ | — | — | — |
nesting | — | — | ✓ | — |
nullable | ✓ | ✓ | ✓ | ✓ |
pattern | ✓ | ✓ | ✓ | ✓ |
pattern_message | ✓ | ✓ | ✓ | ✓ |
placeholder | ✓ | ✓ | ✓ | ✓ |
properties | ✓ | — | — | — |
relative | ✓ | ✓ | ✓ | ✓ |
relative_to | ✓ | ✓ | — | ✓ |
repeatable | ✓ | — | — | — |
required | ✓ | ✓ | ✓ | ✓ |
secret | ✓ | ✓ | ✓ | ✓ |
separator | ✓ | ✓ | ✓ | — |
type | ✓ | ✓ | ✓ | ✓ |
uniqueItems | ✓ | ✓ | ✓ | ✓ |
values_from | ✓ | ✓ | — | — |
variable | ✓ | ✓ | ✓ | — |
variable_file | ✓ | ✓ | ✓ | — |
base
string · one of prefixed
Integer inputs only (the int and uint types, integer, and lists of them): with base: prefixed, a value may also be written with a base prefix after an optional sign — 0x1F or 0X1F (hexadecimal), 0o17 (octal), 0b101 (binary) — and _ may separate digits, as in Go literals (1_000, 0xFF_FF). A value without a prefix is still decimal: 010 is ten, never octal. Misplaced underscores (_1, 1_, 1__0, -_1) are invalid, and a value out of the type’s range is an error as usual.
It applies wherever the input reads text: the command line, a flag’s or argument’s environment and config fallbacks, env inputs, config values written as strings, and default and enum values. Bounds, enum and uniqueItems compare the number a value reads as, so 0x10 and 16 are the same. Without base, an integer is decimal from every source, so 0x10 is an invalid value. Help notes what the input accepts. prefixed is the only value.
complete
Shell-completion hint for this input’s value: for the common case of a file or directory, between a fixed enum and a completer written in Go (FlagValueCompleter), plus an optional message to show when there is nothing to offer. Declare ‘kind’, ‘message’ or both.
Flags and arguments only. Each generated completion script turns the hint into that shell’s own path completion. A completer written in Go still wins when it answers; the hint is the fallback, which the completer can replace for one request with rtx.SetCompletionFallback.
config_source
string
Flag and env inputs only: names a config_files entry whose file path this input supplies, so a user can point the program at a config file (--config ./other.yaml, or ACME_CONFIG=./other.yaml for a flag that declares variable: ACME_CONFIG). The command line and environment are read first, then the file they name is opened. The path comes from the flag if set on the command line, then the first variable that is set (the flag’s own variable: names, then the env input’s), then the flag’s default, then the entry’s own path or discover. A path supplied this way must exist: unlike a declared path, a missing file is then an error, because the user asked for it. A path given on the command line or in a variable has a leading ~ expanded to the home directory (~user too when the program wires path expansion, and kept as written otherwise) and is otherwise used as written, so $VAR stays literal; the flag’s default is expanded like a declared path. An empty value on the command line is a usage error; an empty variable counts as unset. The flag’s own value comes from the command line, its variable or its default, never from a configuration file. The input’s type must be string. At most one flag and one env input may name the same entry. The file the user names is read in the format its extension implies unless the entry declares format, so an entry without one links every config format, and rotini validate warns about it; declaring format links only that format.
default
The value an input takes when nothing supplies one. A scalar for a single value; a list for a repeatable input, each element added as one occurrence (as if the flag were repeated); a mapping for a map input, added as key=value pairs; and for an object-valued flag a mapping of the named schema’s keys (a list of mappings for a list of objects). Write it the way a user would type the value (a duration as 30s, a date as 2026-09-29 or in the input’s layout:). rotini validate checks it against the input’s type, enum, bounds and schema, so a default that could never be accepted fails there rather than on every run that uses it. A supplied value replaces the default; the two never merge. default_text: changes only how help shows it.
default_text
string
How help, man and markdown pages show the default, in place of the value itself. Use it for a default that reads badly as written (default_text: 'the number of CPUs', default_text: '$HOME/.cache/app'), or one the handler computes when the input is unset, so there is no literal to show. Display only: the value the input takes is still default, or nothing.
dotted_keys
boolean · default false
Map-typed flags only, and only with ‘any’ values (‘map’/‘object’ → map[string]any). When true, a ‘.’-separated key in a key=value pair assigns into nested maps, helm-style: –set image.tag=v2 → map[image][tag]=v2. Opt-in because ‘.’ is a legal character in plain map keys — without it, –label a.b=c stores the literal key ‘a.b’. Each assignment overwrites whatever is at its path (creating intermediate maps as needed), so later pairs win and –set a=1 –set a.b=2 leaves a nested map under ‘a’. A value is read as its JSON spelling would be — true/false, null and JSON numbers are booleans, null and numbers; anything else is text — so –set replicas=3 stores the number 3, as a config file’s replicas: 3 does. Declare ‘properties’ on the flag’s schema to give shell completion the known key paths (offered up to the ‘=’).
expand
array of string
Path inputs (string, existingfile, existingdir, inputfile, outputfile, and lists of them): expand what a shell would in values from every source (the command line, environment variables, configuration files, .env files, variable_file files and the declared default), before the value is checked or bound. Help shows the default as written.
- ‘home’ — a value that is exactly
~, or starts with~/, becomes the home directory plus the rest;~name/...becomes that user’s home directory, read from the system’s user database. A~anywhere else is kept. - ’env’ —
$NAMEand${NAME}become that variable’s value, from the run’s environment (including .env files). An unset or empty variable is a usage error naming it, and${NAME:-x}and the other shell operators are errors. A$not followed by a name is kept (C$,$1).
~ is expanded first, then variables, in one pass: text a variable supplies is never expanded again. Each item of a list is expanded after the value is split on its separator. %VAR% is not expanded on Windows (cmd and PowerShell expand their own syntax), but ~\ is. Write $$ for a literal $. A project configuration file found by walk-up discovery can then read the user’s environment into a path, so prefer [home] for inputs such a file sets. Not valid on secret inputs.
file
string
Config inputs only: reads this input from one named config_files entry only, never from the other files, so a key present in another file does not satisfy it (or its ‘required’). Omit it to read through the declared order, where the first file with the key wins.
from
array of string
Flags, and arguments before any variadic one: where the input’s value may come from, besides the text on the command line.
- ‘file’ — a value starting with ‘@’ is replaced by the named file’s contents (
--token @/run/secret; pair withsecret: truefor a token file). To pass a literal value that starts with@, double it:--to @@alicegives@alice, and@./@namereads a file whose name starts with@ - ‘stdin’ — a value of exactly ‘-’ is replaced by what is piped on stdin (
-f -); empty stdin is then a usage error, and stdin can be read once, so one input on a command path may take it: a from:stdin flag or argument, or the command’s stdin: input - ‘value’ — a value typed on the command line. Literal values are always allowed, except on a secret input: with
secret: true, a ‘from’ list without ‘value’ refuses a literal (--token takes @file or -, not a value), so the secret never shows in the process list or shell history. On other inputs listing it is documentation only. Any value that does not match an enabled marker stays literal
Text read from a file or stdin has one trailing line ending removed (leading and interior whitespace is kept), then goes through the normal type, enum and constraint checks: the flag’s value is that text. On an object-valued flag the text is decoded as the object (JSON, or YAML when it spans lines); a structured payload for the whole command belongs in the command’s stdin: input. On a list or map flag, the file’s (or stdin’s) lines are separate values; blank lines are skipped, and each line is split on the flag’s separator.
Without ‘from’, ‘@’ and ‘-’ are ordinary characters. Defaults and environment and config fallbacks are always literal: the markers apply only to values given on the command line. On an argument they work the same way: cat-config - reads stdin, cat-config @app.yaml the file.
glob
boolean · default false
On a variadic argument of type string, existingfile, existingdir or inputfile: on Windows, where the shell passes patterns through unexpanded, rotini expands each word containing *, ? or [ into the matching paths, sorted, relative to the run’s directory. A word naming an existing path is kept as written (file[1].txt), - is never expanded, and a pattern matching nothing is passed through without its escapes, so the path check reports it as a POSIX shell would. [*], [?] and [[] stand for a literal *, ? and [, and a word with no other pattern character is not expanded; ArgvOf writes these characters that way on Windows. Matching is case-sensitive (*.TXT does not match a.txt), ** is not supported, and matches use \ separators. On other systems the shell has already expanded unquoted patterns, so nothing changes. Not the glob value type, which validates a pattern instead of expanding it.
ignore_case
boolean · default false
With ’enum’ only: match a value against the enum without regard to case, so --mode FAST is accepted against [fast, slow]. The value is stored in its declared spelling (fast), so a handler compares against one form. Applies wherever the input reads a value: the command line, a flag’s environment and config fallbacks, and env and config inputs.
implicit_value
string or number or boolean
Flags only: the value a flag takes when it is given without one, which makes its value optional (the --color[=when] shape). With implicit_value: always, a bare --color means always, --color=never sets never, and a flag left out takes its default as usual. Because the value is optional it must be attached: in --color never, never is the next argument, not the flag’s value (a short flag attaches too: -cnever, -c=never). Help shows the flag as --color[=<type>] with (implicit: always). For a scalar flag that is not a bool (a bool already works this way, with true), and the value must satisfy the flag’s type, enum and constraints.
key
string
Dotted key path the value is read from (config inputs, and the config fallback of flags and arguments; e.g. ‘server.port’). Segments of letters/digits/_/-, joined by dots; rotini resolves it through the configuration files (and SNAKE_UPPER of it names a flag’s or argument’s env fallback variable). Help shows the key under the flag or argument when the command reads configuration files.
layout
string or array
Time inputs only (time, datetime, date, time.Time, and lists of them): how the value is written. A Go reference-time layout (the reference time Mon Jan 2 15:04:05 MST 2006 written the way yours is: 2006-01-02, 02/01/2006, Jan 2 2006 15:04), or unix (seconds since the epoch, fractions allowed) or unixmilli (milliseconds). A layout with no zone parses as UTC. Without it, date reads 2006-01-02 (that day’s UTC midnight) and time/datetime read RFC 3339 (2026-09-29T14:00:00Z). Applies wherever the input reads a value, and a default must parse under it. It may be a list, tried in order: the first layout that parses wins, help shows the first, and an error names them all (layout: [2006-01-02, unix]). Put the most specific first, since a value that parses under an earlier layout never reaches a later one.
negatable
boolean or string · default false
Bool flags only: also accept a --no-<name> form for every long identifier, which sets the flag false. --color with negatable declares --no-color too.
Use it to turn something off for one run when a default, a config file or an environment variable already turned it on, which a plain bool flag cannot do. Short identifiers get no negated form.
A declared identifier always wins over a derived negated one, so a flag you declare as --no-cache keeps that name. The negated form takes no value: --no-color=true is a parse error. The generated field is the same single bool either way, and help shows it as --[no-]color.
A string names the one negated form instead: negatable: --plain on --color accepts --plain to set it false, and no --no-color. It is a long identifier that no flag on the command path declares, and help shows it beside the flag’s own identifiers: --color, --plain.
nesting
string
Env inputs only, map-typed (‘map’/‘object’ → map[string]any): the separator that collects a family of environment variables into this one nested input. The variable prefix is ‘variable:’ when set, else the UPPER_SNAKE form of the input’s name. With name: http, variable: ACME_HTTP, nesting: “__”, ACME_HTTP__TIMEOUT=30 and ACME_HTTP__RETRY__MAX=9 bind as http = {timeout: “30”, retry: {max: “9”}} (segments lowercased; values are strings). ‘required: true’ is an error when no matching variables exist. ‘default:’ is rejected: set defaults in code or in a config file instead.
placeholder
string
Display name for this input’s value in generated help, man pages and usage lines: --file <PATH> instead of the type, <PATH> instead of the argument’s name. Presentation only: parsing, completion and the generated field are unchanged. Conventionally UPPERCASE or <angle-bracketed>.
relative
string · one of past, future, both
Time inputs only (time, datetime, date, and lists of them): also accept a time measured from now. The input still takes absolute values, tried first, so declaring it never changes what one means.
- ‘past’ — a time ago:
2hor-2h(two hours ago),3d,1w;+2his an error. The wordsnow,today(midnight) andyesterday - ‘future’ — a time from now:
2hor+2h;-2his an error. The wordsnow,todayandtomorrow - ‘both’ — either, with a sign required (
-2h,+3d), and all four words
Durations take Go units plus d (24h) and w. Words are read in any case. On a date the result is the calendar date it falls on, and an offset must be whole days. Times are measured from the run’s clock, read once per run, so every relative value in a run agrees; Program.WithClock sets it for tests, and rtx.Now() returns the same reading. today and the date a value falls on follow the clock’s time zone (the process’s, by default). A flag’s or argument’s default may be relative (default: 24h) and is measured at run time; an env or config input’s default is absolute.
relative_to
string · one of config
‘config’: a relative path read from a configuration file is resolved against that file’s directory, so cache: ./cache in ~/.config/app/config.yaml means ~/.config/app/cache. Applied after expand. Absolute values, and values from the command line, environment variables, .env files and the default, are kept as they are and stay relative to the run’s directory. Valid on config inputs, and on flags and arguments with a configuration fallback (key:).
repeatable
boolean or null
On a single-value flag: repeatable: false makes giving the flag more than once on the command line an error naming the first and last values, instead of the last value winning, which is the default. -vv and --color --no-color are repeats, and so is a cascading flag given at two levels of the command path; a sub-command that redeclares the flag has its own. Environment and configuration values are unaffected. Rejected on list, map, count and object flags, which repeat by nature, and on arguments, env and config inputs.
required
boolean · default false
When true, the input must be provided (or stdin must not be empty for stdin inputs). On the command line an empty value (--name "", --name=, or '' as an argument) is provided; add minLength: 1 to refuse it. An env input set to the empty string is provided too, while a flag’s environment fallback set to the empty string counts as unset. Rejected beside a default, which would always supply the value. Note: this is a boolean — unlike the string-array ‘required’ on Schema.
secret
boolean · default false
When true, this input’s value is treated as a secret: redacted in provenance/error output by the default input reader. It does not prompt: a handler that wants to ask for the value interactively reads it without echo itself (golang.org/x/term’s ReadPassword, for one); for non-interactive supply, pair secret with from: [file] (token file) or an env input. A secret flag or argument whose from: list leaves out value refuses a value typed on the command line, which would show in the process list and shell history; rotini validate warns about a secret flag that accepts one. Help, man and markdown pages leave out a secret input’s default unless default_text sets what to show.
separator
string
List and map flags, a variadic argument, and an env list or map input: split each value on this character, so --tags a,b,c is three tags and --label a=1,b=2 two entries. The word nul splits on NUL bytes, as find -print0 writes them; since command-line and environment values can’t contain a NUL, it applies only to content rotini reads itself, so it is valid only on a list or map flag with from: [file] or from: [stdin], whose file or piped content is split on NUL instead of lines, byte for byte. Splitting is CSV-style: an item in double quotes keeps the separator (--tags '"a,b",c'), leading spaces are trimmed, and an empty value (--tags "") is an empty list. Repeating the flag still appends, so --tags a,b --tags c is three tags. Items are split before validation, so enum, item constraints and minItems/maxItems see each one. A flag’s environment-variable fallback splits the same way (TAGS=a,b); a configuration file’s list binds item by item whether or not a separator is declared. Without a separator, each occurrence is one value, used as is. An env list or map input splits its variable on commas (TAGS=a,b) unless it declares a separator (separator: ':' reads PATHS=a:b): a plain split that trims spaces, with no quoting. A map’s separator splits pairs; each pair still splits on =. Not valid on config inputs: a configuration file writes a list as a list.
values_from
string
Flags and arguments only: take the input’s allowed values from the field names of the command’s declared output:, for a --json id,title or --sort-by title flag. output is the output itself, or one item when it is an array (or the command writes a stream, output_stream); output.tasks is the tasks property of the output, and each further segment names a property below it. An array met on the way is stepped through to its items, so values_from: output.tasks on a {tasks: [Task]} output lists Task’s fields. The place reached must be an object with properties, and its property names, sorted by name, become the input’s enum: help, completion, validation and the contract list them like any enum, and renaming an output field changes the flag’s values too. Only that object’s own properties are listed, not nested ones. The input’s type is string or a list of strings ([]string, with a separator for --json id,title), it declares no enum of its own, and it is not cascading. The handler applies the values with rotini.SelectFields and rotini.SortBy.
variable
string or array
The exact environment variable this input reads, instead of the name rotini would derive. It may be a list, first preferred: variable: [GH_TOKEN, GITHUB_TOKEN] reads the first one that is set, for a value other tools already know under more than one name. Help lists every name. For a flag, help shows them under the flag, in lookup order. A nested env input (nesting:) takes one name, since it is the prefix of a family of variables. Valid on env inputs, and on flags and arguments as their environment fallback; rejected on config inputs and stdin, which have no environment variable.
A variable named here is never given the env_prefix: it is already exact, and prefixing it would silently make it a different variable.
On a flag or an argument it also turns on the fallback chain (command line, then environment, then config file, then default), keyed by the input’s own name unless key: names one. So --token with variable: GITHUB_TOKEN reads that variable directly, with no need for a config key: whose UPPER_SNAKE form happens to match. Because the flag now has a key, a configuration file that defines that key supplies it too; declare key: to control what that key is.
An argument’s fallback fills a position the command line left out (deploy reads <env> from DEPLOY_ENV). Positionals fill left to right, so a fallback applies only when every argument before it has a value, and an argument after one with a fallback needs a fallback or a default too. A variadic argument splits its environment value on its separator; without one the whole value is one item. Not on a passthrough argument, or the arguments of a passthrough command, whose words are taken as typed.
variable_file
string
An environment variable holding the path of a file to read this input’s value from, the convention Docker and Kubernetes secrets use: with variable_file: APP_TOKEN_FILE, APP_TOKEN_FILE=/run/secrets/token reads the value from that file. One trailing line ending is removed, a relative path is read from the run’s working directory, and the file may be at most 1 MiB. Setting both the plain variable and this one is an error; an empty value counts as unset. Valid on env inputs and on flags that read an environment variable (as part of the flag’s fallback); like variable:, the name is exact and never given the env_prefix. Help lists it after the plain names, and a secret input’s file contents are redacted like any other value.
ConfigurationFileDiscover
A run-time location strategy for a configuration file, instead of a fixed ‘path’. The first directory (in the strategy’s order) containing ‘file’ wins; a file found nowhere is simply absent, the same as a missing fixed path. Among one command’s config_files the first declared still wins, wherever each was found.
strategy
string · required · one of walk-up, xdg, native, xdg-system
‘walk-up’: search from the run’s directory (the working directory, unless a role: chdir flag or Program.WithDir sets another) upward, one parent at a time, until a directory containing ‘file’ is found or the root is reached. Use it for project-local config. On Windows the search stops at the drive root.
‘xdg’: search $XDG_CONFIG_HOME/<app>, defaulting to ~/.config/<app>, on every platform, Windows and macOS included. Rotini does not substitute %APPDATA% or ~/Library/Application Support, so a CLI documented as reading ~/.config/<app> reads the same path everywhere, and a dotfiles repository works unchanged across machines. This is the portable choice. A relative $XDG_CONFIG_HOME is ignored, as the XDG spec requires: it must be absolute (with a drive letter on Windows).
’native’: search the platform’s own config directory, <dir>/<app>: %AppData% on Windows, ~/Library/Application Support on macOS, and $XDG_CONFIG_HOME (default ~/.config) elsewhere, so on Linux it is the same directory as ‘xdg’. A relative $XDG_CONFIG_HOME is ignored here too.
‘xdg-system’: search each absolute directory in $XDG_CONFIG_DIRS, in order, defaulting to /etc/xdg, for <app>/<file>. On Windows it searches only when $XDG_CONFIG_DIRS is set. Declare it as its own config_files entry after the user’s (‘xdg’ or ’native’): the first declared entry wins per key, so the user’s file overrides the system one key by key.
‘xdg’, ’native’ and ‘xdg-system’ need ‘app’.
file
string · required
The file name to look for in each searched directory (e.g. ‘.acme.toml’, ‘config.yaml’). It is a name alone: the strategy picks the directories, so rotini validate rejects a path (one with / or \) and . or ...
app
string
The application directory under the config root: the ‘<app>’ in $XDG_CONFIG_HOME/<app> (or the native or system directory). Required by the ‘xdg’, ’native’ and ‘xdg-system’ strategies and rejected by ‘walk-up’, which has no such directory. rotini validate enforces both, so its message can say which strategy needs it and what it is for.
ConfigurationFileProfiles
Where a configuration file keeps its named profiles and which input chooses one.
under
string · required
The top-level key holding the named profiles: profiles: {prod: {region: eu-west-1}} is under: profiles. One key, not a dotted path. It must not be the first segment of a key a config input or fallback reads from this file.
select
string · required
The name of the string input that chooses the profile: a flag with this name, an env input with this name, or both, declared on this command or an ancestor. The flag set on the command line wins, then the first set variable (the flag’s own variable: names, then the env input’s), then the default. The selector is read before any configuration file, so it can’t come from one: the flag can’t declare key: or config_source. An unknown profile chosen on the command line or in the environment is a usage error listing the defined ones; a defaulted profile that a file doesn’t define just leaves the shared keys.
default
string
The profile used when neither the command line nor the environment chooses one. Declare the default in one place only: here, on the selector flag or on the selector env input. With no default and no selection, only the shared keys apply.
BaseSchema
Shared fields for Schema and InputSchema. JSON Schema Draft 7 cannot combine allOf inheritance with additionalProperties: false, so an unknown key inside a schema block is rejected by rotini validate itself rather than by the schema — with the same positioned “unknown key” message as anywhere else in the spec. A JSON Schema keyword rotini does not implement (format, title, …) is rejected too, since it would otherwise do nothing. description is accepted on object schemas and their properties, where it becomes a Go doc comment.
$ref
string
Reference to a named schema in the root command’s “schemas” map, by its name ($ref: DB) or as a JSON pointer ($ref: '#/schemas/DB'). The two mean the same; the pointer is what JSON Schema tooling reads. Resolved when you generate. A reference to a named scalar schema brings that schema’s enum, pattern (and pattern_message), lengths and bounds with it, wherever the input leaves them unset.
On a flag, a named object schema makes the flag object-valued: its generated field is the schema’s struct, and it takes any of:
- JSON —
--db '{"host":"h","port":5}' - key=value pairs —
--db host=h,port=5; dotted keys nest (pool.max=9), a repeated key appends to a list field, and quotes keep a comma (host="a,b") - a JSON or YAML file, with
from: [file]—--db @db.yaml - one field per flag —
--db.host=h --db.port=5
Occurrences merge in the order they appear, a later key winning. Declared as type: array, items: {$ref: …}, the flag is a list of objects and each occurrence is one element (per-field flags do not apply). A flag’s environment fallback takes the same spellings (DB=’{“host”:“h”}’), and its configuration-file fallback is the object as a mapping (or a list of mappings).
Every spelling is validated against the named schema, with the same validator a stdin payload of that shape meets, and errors name the flag and the key (--db: port: value must be >= 1, unknown key "bogus"). A default is written as a mapping (a list of mappings for a list) and is checked against the schema by rotini validate; a supplied value replaces it rather than merging. Keys that shape a scalar value (enum, separator, ignore_case, implicit_value, negatable, dotted_keys, bounds, pattern) do not apply: an object’s rules live in its schema. Arguments cannot be object-valued.
enum
array of string or object
Allowed values. The check applies to the final value, wherever it came from, so a flag value supplied by an environment variable or a config file is checked too. At least one member: an empty list would mean the same as no enum.
Members are strings, so quote a number (enum: ["1", "2"] on an int input). Each member is a value (json) or a value with a one-line summary saying what it means ({value: yaml, summary: human-friendly}); the two forms mix freely. Summaries are shown in help, man and markdown pages, offered as descriptions in shell completion, and carried in the contract’s enum_values; JSON Schema output lists the values only. Summaries belong on an input’s own schema: an output or stdin schema, and a named schema under schemas, take plain values.
The object form also takes aliases (other spellings accepted and bound as the value: {value: yaml, aliases: [yml]}), hidden: true (accepted, but left out of help’s list, completion and error messages), and deprecated with a message, optionally deprecated_since, removed_in and replaced_by (another value). A deprecated value is still accepted and is left out of the same lists; rotini.Deprecations reports it when given on the command line, as it does deprecated_aliases, the aliases being retired. Help keeps its short list of the values to use; man and markdown pages list each value with its aliases and deprecation.
exclusiveMaximum
number or string or null
Exclusive upper bound: the value must be strictly less. Same applicability rules as ‘maximum’.
exclusiveMinimum
number or string or null
Exclusive lower bound: the value must be strictly greater. Same applicability rules as ‘minimum’ (numbers, durations and sizes; per-element for arrays; rejected elsewhere). exclusiveMinimum: 0 expresses “positive” exactly.
import
string
Optional Go import path backing ’type’. Set it when ’type’ references a stdlib or third-party package whose name rotini does not already know (e.g. ‘github.com/google/uuid’ for uuid.UUID). Omit (or leave empty) for builtins and rotini’s own type names (string, int, duration, url, ip, bytesize, …) — codegen treats omitted/empty as ’no import’. The aliased form ‘alias path’ renames the import to avoid a clash (e.g. ‘urlx github.com/me/url’). Codegen dedupes identical entries across the spec.
items
Element schema for an array type: ‘array’ + items int generates []int, and items $ref a slice of the named type; with no items, elements are strings. Per-value constraints (enum, pattern, minimum/maximum, exclusiveMinimum/exclusiveMaximum, multipleOf, minLength/maxLength) apply to every element and may be written either here, JSON Schema style, or on the list itself. The two spellings mean the same thing, and declaring the same constraint in both places with different values is an error. minItems/maxItems belong on the list (they count elements) and are rejected here on flags, arguments, env and config. The TextUnmarshaler rule also applies to each element (see ’type’).
maxItems
integer or null
Maximum number of values for a repeatable (array or map) input — rejected on scalar types. maxItems: 0 is a real bound: no value is accepted. A maxItems below minItems is rejected, since no value could pass.
maxLength
integer or null
Maximum string length in runes (string types only; for []string, each element) — rejected on non-string types. maxLength: 0 is a real bound: only the empty string is accepted. A maxLength below minLength is rejected, since no value could pass.
maximum
number or string or null
Maximum allowed value (inclusive). Same applicability rules as ‘minimum’ (numbers, durations and sizes, each in its own spelling; per-element for arrays; rejected elsewhere); maximum: 0 is a real, enforced bound. As with ‘minimum’, a bounded number rejects NaN and infinite values.
minItems
integer
Minimum number of values for a repeatable (array or map) input — rejected on scalar types. A list flag or variadic argument that is left out has zero values, so minItems also applies to it unless it has a default (add required for the message to say the input is missing). An env or config list is checked only when it is set.
minLength
integer
Minimum string length in runes (string types only; for []string, each element) — rejected on non-string types.
minimum
number or string or null
Minimum allowed value (inclusive). Number types (the int, uint and float types and the integer and number aliases) take a number; a duration takes a duration (minimum: 1s) and a bytesize a size (minimum: 1Mi, or a number of bytes), read by the same parser as the value and printed back that way in errors (must be >= 1s). Rejected on any other type, where it would have no effect. For a repeatable input the bound applies to each element. minimum: 0 is a real, enforced bound. A number input with any bound (this, maximum, the exclusive bounds or multipleOf) rejects NaN and infinite values.
multipleOf
number or string or null
The value must be an integer multiple of this (JSON Schema semantics: the division yields an integer). Numbers, durations and sizes, in their own spelling (multipleOf: 1s); per-element for arrays; must be strictly positive.
nullable
boolean · default false
Generate the field as a pointer (*T): nil means the input was not provided, distinguishable from its zero value. Defaults/values coerce through the pointer.
pattern
string
Regular expression the value must match (string types only; for a list, each element). As in JSON Schema, the pattern matches anywhere in the value unless anchored: use ^…$ for a full match. A failure shows the user the regex itself unless pattern_message: says it in words.
pattern_message
string
With ‘pattern’ only: what the user is told when a value does not match, in place of the regex — which is written for the program, not the person typing. Phrase it to follow the input’s name: pattern_message: must be json, yaml, wide, name or custom-columns=<spec> reports -o must be json, yaml, wide, name or custom-columns=<spec> (got "bogus"), where the default is -o must match ^(json|yaml|…)$ (got "bogus"). Applies wherever the pattern is checked: an input on every channel it reads, each element of a list, a named schema an input refers to, and a property of an object-valued flag or a stdin payload.
properties
object
Property schemas for an object shape. Used by document shapes (output, stdin payloads, named schemas) — and, on a map-typed FLAG, the declared property names feed shell completion’s key vocabulary (dotted paths when dotted_keys is set, offered up to the ‘=’).
type
string
The type used to parse and store the value. Go type names (bool, int, float64, []string, duration, map) and JSON Schema names (boolean, integer, number, array, object) are equivalent. A bool accepts true/false, yes/no, on/off, y/n, t/f and 1/0, in any case, so --cache=off and CACHE=yes both work.
Value types parse a kind of value and generate the matching Go field:
- ‘duration’ — time.Duration; Go units plus ’d’ days and ‘w’ weeks (7d, 2w3d)
- ’time’ / ‘datetime’ — time.Time, RFC 3339 (2026-09-29T14:00:00Z)
- ‘date’ — time.Time, a calendar date (2026-09-29, that day’s UTC midnight). All three time types take
layout:for another format, Unix timestamps included, andrelative:for times measured from now (2h, today) - ‘url’ — *url.URL; needs a scheme and host
- ’email’ — mail.Address; ‘Name <a@b.c>’ or a bare address
- ’timezone’ — *time.Location; an IANA name such as Europe/Berlin
- ‘mac’ (net.HardwareAddr), ‘ip’ (netip.Addr), ‘cidr’ (netip.Prefix), ‘hostport’ (netip.AddrPort)
- ‘bytesize’ — rotini.ByteSize; 512Mi, 10MB, 1.5GiB (an ‘i’ makes the unit binary)
- ‘hexbytes’ — rotini.HexBytes; optional 0x
- ‘base64bytes’ — rotini.Base64Bytes; standard or URL-safe, padded or not
- ‘regexp’ — *regexp.Regexp, compiled when parsed; Go’s RE2 syntax, which has no backreferences or lookaround
- ‘glob’ — rotini.Glob, a path.Match pattern checked when parsed (
*,?,[a-z],\escapes) with a Match method;**is not recursive. It keeps the pattern as a value;glob: trueinstead expands a pattern into file names - ’existingfile’ / ’existingdir’ — a plain string field, checked at parse time to exist and be that kind of thing, so a bad path is a usage error naming the flag the user typed. The check is existence and kind only: cleaning, following symlinks and creating a missing file are the handler’s policy, and
~and$VARare expanded only when the input declaresexpand. A relative path is checked against the run’s directory (Program.WithDir, by default the working directory) and bound as typed, so a handler opens it joined to rtx.Dir() - ‘inputfile’ — a plain string field, checked like ’existingfile’, except that ‘-’ means stdin: rotini.OpenInput(rtx, path) opens either (
cat a.txt - b.txt). Only one ‘-’ may be given per run, across every inputfile value. Unlikefrom: [stdin], where ‘-’ replaces the value with stdin’s text, the value stays ‘-’ and the handler reads the stream. Help adds ‘(- for stdin)’ - ‘outputfile’ — a plain string field: a file to write, where ‘-’ means stdout. Checked at parse time to be no directory and to sit in an existing directory; rotini.CreateOutput(rtx, path) writes it atomically and refuses to replace an existing file unless given rotini.Overwrite(true). Help adds ‘(- for stdout)’
A value type works inside Go spellings too (’[]bytesize’, ‘map[string]duration’), and help shows the name as written (’–limit bytesize’), not the Go type. A lowercase name that is neither a Go builtin nor one of these is rejected as a typo, with a suggestion.
Shapes that change how a flag is written:
- ‘count’ (flags only) — a presence counter: the flag takes no value, each occurrence increments the generated int field (-vvv → 3, clustering included), an inline value (–verbose=3) is a parse error, and every value-shaped key (default, enum, constraints, from, key, …) is rejected
- ‘[]…’ / ‘array’ — repeatable (–tag a –tag b → a slice); ‘items’ declares the element type (‘array’ + items int → []int), defaulting to string
- a map (‘map[string]string’, or ‘map’/‘object’ → map[string]any) — repeatable too, taking ‘key=value’ pairs (–label k=v –label a=b; split on the first ‘=’; the value coerced to the element type)
Any other Go type (time.Time, uuid.UUID, your own): set ‘import’ to the backing package path. It must implement encoding.TextUnmarshaler — that method is its parser and validator, applied to each element of a list too. Rotini refuses a type without it at parse time with a loud error, never a silently zeroed field; validate cannot check it, since that would mean type-checking foreign packages.
A NAMED OBJECT schema is different: a flag whose schema is $ref: '#/schemas/DB' (or type: array, items: {$ref: '#/schemas/DB'} for a list) takes a structured value — see $ref.
uniqueItems
boolean
On a list input (a list flag, a variadic argument, or a list env or config input): reject a list that holds the same value twice. Values compare as their type reads them, so 01 and 1 are the same int, 1h and 60m the same duration, 1Ki and 1024 the same bytesize, and ::1 and 0:0:0:0:0:0:0:1 the same ip; a time compares as the instant it names, read with its layout; a list of objects compares whole objects; a case-insensitive enum compares its declared spellings. Checked on every channel and by CheckInputs, and a list default may not repeat a value. Rejected on scalar, map and count inputs, and inside items. On output, stdin and named schemas it is the JSON Schema keyword.
CompletionHint
A shell-completion hint for a flag’s or argument’s value: a kind of value to complete, a message to show when there is nothing to offer, or both.
extensions
array of string
Narrows kind ‘file’ to these suffixes, written without a leading dot (‘yaml’, ‘json’). Omitted, every file is offered. Rejected on the other kinds, which have no extensions to filter.
kind
string · one of file, directory, none, command, executable, user, host, group
‘file’: complete file paths (narrowed by ’extensions’). ‘directory’: complete directories only. ’none’: complete nothing, which is not the same as declaring no hint. With no hint the shell applies its own default, and for bash and zsh that is file completion; ’none’ turns it off, so for an opaque value (a container id, an API resource name) the shell does not offer the files in the current directory. ‘command’: complete command paths below the root, for a help command’s argument: the words already given to this argument are the path, and the candidates are its visible sub-commands (help remote <TAB> offers remote’s sub-commands). Arguments of type string or a list of strings only. ’executable’: program names (bash offers every command name it knows; zsh, fish and PowerShell offer programs). ‘user’, ‘group’: local account and group names. ‘host’: host names the shell knows (/etc/hosts, ssh known hosts). Each shell’s own completer supplies these four; PowerShell has no host completer, and offers users and groups on Windows only. Values of type string or a list of strings only.
message
string
A line the shell shows while this input’s value is being completed and there is nothing to offer, such as a service name from deploy.yaml. One line. It shows only when the conf’s completion feature sets messages, and in zsh and bash 4.4 or later; other shells skip it. A message a completer adds with rtx.AddCompletionMessage takes its place.
FlagSet
Flags declared once under the root’s flag_sets, for commands to add with use:.
flags
array of FlagInput · required
The set’s flags, declared as on a command.
flag_dependencies
array of FlagDependency
Flag dependencies over the set’s own flags, added to each command that uses the set.
flag_groups
array of FlagGroup
Flag groups over the set’s own flags, added to each command that uses the set.
group
string
The help group of every flag in the set that names no group of its own, so a set’s flags are listed under one heading (group: Output).
summary
string
What the set is for, for the spec’s readers; it isn’t shown on any page.
Multicall
complete
string
An invoked name starting with this answers shell completion for the root instead of running it: the arguments are the words to complete, as kubectl passes them to a kubectl_complete-<plugin> executable. The answer is written in the format Program.WithCompletion sets, else rotini.PluginCompletion, the plugin hosts’ format.
prefix
string
Stripped from the invoked name before it is matched against the root’s commands: with ‘acme-’, a binary run as acme-ls runs ls. A name without the prefix runs the root.