❯ rotini --documentation█

.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

StdinSpec

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

ResponseFiles

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

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

HandlerSource

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

PluginDiscovery

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.

string

Text rendered above the description block. Ignored when ‘help’ is set.

string

Text rendered at the bottom of the page. Ignored when ‘help’ is set.

headings

HelpHeadings

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

Schema

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

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

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

InputSchema

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

InputSchema

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

InputSchema

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

InputSchema

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

InputSchema

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

ConfigurationFileDiscover

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

ConfigurationFileProfiles

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

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

Schema

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.

Keyflagargumentenvconfig
$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

CompletionHint

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’ — $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).

~ 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 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 @
  • ‘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: 2h or -2h (two hours ago), 3d, 1w; +2h is an error. The words now, today (midnight) and yesterday
  • ‘future’ — a time from now: 2h or +2h; -2h is an error. The words now, today and tomorrow
  • ‘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

Schema

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, and relative: 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: true instead 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 $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()
  • ‘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. Unlike from: [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.