Imported from maelys-dev/maelys-egress (
AGENTS.md). Install upstream withnpx skills add maelys-dev/maelys-egress. Copyright stays with the author.
Agent instructions
The public CLI is an executable protocol shared by humans, scripts, SDKs and
LLMs. Before adding or changing a command, option, configuration key, exit
status or lifecycle event, read and follow
.claude/skills/egress-cli-contract/SKILL.md.
Do not hand-edit files under docs/generated/. Update the central catalog or
protocol schema, then regenerate: maelys-release adopt . --apply writes
docs/cli.md and docs/cli-contract.json, make config-reference the
configuration one. Commit the generated result.
No backward-compatibility alias should be introduced without an explicit product decision. The 0.x series prefers one clear contract over parallel old and new spellings.
Adopting a new maelys-release, maelys-cli or agent-cli-spec is not publishing,
and not every tag needs adopting. Run maelys-release adopt . without
--apply first: its impact line says whether any version since this
repository's pin asks it a gesture. When one does, adopt promptly and follow
the order it names. Otherwise group adoptions, about once a week. Merge an
adoption without a tag unless it changes what a user receives: a difference
under include/, protocol/, cli/, in docs/cli.md or
docs/cli-contract.json, or in an installed file. maelys-cli is linked
statically, so a new pin can change the shipped binary with none of those
moving; read its changelog for runtime changes before deciding.
Maelys CLI framework (maelys-cli 0.5.29, da983b2)
This project builds its command-line interface on libmaelys_cli. The
complete guide is in docs/maelys-cli-guide.md; this block is the summary
that must hold for every change.
Using a Maelys CLI from an agent
- Start with
PROGRAM describe --summary --format json --compact --non-interactive, thenPROGRAM describe COMMAND_ID --format jsonbefore invoking a command. TreatinputandoutputSchemaas one public contract; never build a call from the human help text. - Always pass
--format json --non-interactivein automation.--jsonis an exact alias of--format json;--compactkeeps one line. - Exit
0is success,1is an execution failure,2is a completed validation report that found violations. Success data is on stdout only; failures are a JSON envelope on stderr with a stableerror.codeand an actionableerror.hint. - Transactional commands plan by default and write only with
--apply. Review the plan, then repeat the same invocation with--apply.--dry-runand--planare rejected. - Never pass rendering flags to a command whose
outputModeisprotocol-stream; its stdout belongs to the declared protocol. SetMAELYS_CLI_FORMAT=jsonin the environment to receive its failure envelope as JSON on stderr. describe COMMAND_IDis minimal;describe --summarylists everything;describe --summary --prefix NAMESPACElists one namespace. A descriptor withavailable: falsenames a command this build cannot run (unavailableReason). Operands may carrytypeandchoiceslike options;input.constraintsstatesrequires,at-most-one,exactly-oneandall-or-nonerules;exactly-onehas no other site.PROGRAM completion bash|zsh|fishprints the shell completion generated from the catalog.- Unknown, duplicated or foreign options are refused. Fix the invocation instead of retrying it.
Adding or changing a CLI action
One command is one entry of the central catalog (maelys_cli_command_t),
one handler and one JSON Schema file. In the same change, update:
- the catalog entry, written with the declaration macros
(
MAELYS_CLI_READ,_RECORDS,_TRANSACTION,_EXECUTE,_STREAM,_PROTOCOL_STREAM,_EXTERNAL;MAELYS_CLI_OPERAND,_OPERAND_OPTIONAL,_OPERAND_REST,_OPERAND_CHOICE,_OPERAND_KIND, an operand typed like an argument with.hex_digits,.choices,.pattern;MAELYS_CLI_FLAG,_STRING,_PATH,_ABSOLUTE_PATH,_UNSIGNED,_INTEGER,_SIZE,_DURATION,_CHOICE,_HEX,_HEX_OR,_DIGEST) plus.required,.repeatable,.depends_on,.depends_on_all,.conflicts_with,.group(all-or-none), on the commandMAELYS_CLI_CONSTRAINTSofMAELYS_CLI_CONSTRAINT(kind, options)forexactly-one,at-most-oneandrequiresover several options (spec 2.5; never validated in the handler), and.default_text(validated at startup, returned by the typed accessors: never repeat a default in the handler;MAELYS_CLI_DEFAULT_OF(LIB_CONSTANT)when the library owns the value); a command this build cannot provide declares.unavailable = "reason"; a product with build variants composes its catalog at startup withmaelys_cli_catalog_concat()(a later part may only replace an.unavailabledeclaration of the same identifier; anything else isEEXIST); - the output schema: a JSON Schema file under the project's schema
directory, embedded by
maelys-cli-embedand referenced withMAELYS_CLI_SCHEMA(symbol); never a hand-escaped C string; - the handler, which reads only through
maelys_cli_operand*(),maelys_cli_option*()andmaelys_cli_flag()(typed kinds replace hand validation of paths, digests and choices), replies exactly once throughmaelys_cli_succeed*(),maelys_cli_emit_record*()+maelys_cli_finish_records()ormaelys_cli_fail*(), and testsmaelys_cli_replied()after a helper that may have replied; optional helper programs are found withmaelys_cli_resolve_helper(); - focused tests: accepted and refused inputs, plan without write,
--applywith write, error codes and exit codes,describe COMMAND_IDexposing the exact contract; - the generated CLI reference when the project keeps one.
A product written in Python builds on python/maelys_cli.py of the same
pinned maelys-cli (one file, standard library only): the same declarations
(cli.read, cli.records, cli.transaction, cli.stream, cli.external;
cli.operand, cli.option, cli.flag, cli.argument), the same built-ins,
envelopes and exit codes; docs/python.md is its guide and the conformance
kit of agent-cli-spec runs on the product's program in its CI.
Linking: a product CLI links libmaelys_cli.a only (no dependency). A
dispatcher that runs external commands from manifests adds
libmaelys_cli_extension.a and one libmaelys-json.a (pkg-config
maelys-cli-extension, CMake maelys::cli_extension); never embed a
dependency archive into your own .a. Reading untrusted JSON is
maelys-json's job, not the framework's; the framework only writes JSON
and refuses invalid UTF-8 in what it writes.
Rules that must not be broken: no second usage string outside the catalog; no
hand-written argv parsing in main(); no product type inside the shared
framework; validation errors in causal order (command, options, values,
dependencies, operands, files, syntax, schema, state); configuration,
manifests and secrets read with maelys_cli_read_trusted_file (trust judged
on the descriptor read, bounded by the bytes read) and failures reported
with maelys_cli_fail_file; explicit
MAELYS_CLI_WRITE_REPLACE / MAELYS_CLI_WRITE_NO_REPLACE on every file
write; external programs started with absolute paths and execve, never a
shell or PATH lookup, with a trusted immediate parent held open through exec;
delegate scripts use a direct absolute interpreter never named env; relative
and env shebangs are refused.
Maelys release socle (maelys-release)
This repository publishes through the shared maelys-release workflows. The
rules below hold for every release-related change; the complete conventions
are in docs/conventions.md of maelys-release. The first rules hold for
every repository on the socle; those after "If this repository" apply only
when it has what they name.
.github/workflows/release.ymland the twoscripts/checkout-dependenc*.share generated bybin/maelys-release adoptof maelys-release fromdependencies/*.pin,dependencies/packagesandpackaging/homebrew/*.rb.in. Never edit them by hand; change the declarations, then runmaelys-release adopt DIR --applyfrom a maelys-release checkout at the wanted tag.maelys-release check DIR(exit 2 on any violation) verifies;maelys-release preflight DIRchecks the tag preconditions before a release.check,preflightandrehearserun as the socle this repository pins;cut,adopt,protectanddependenciesrun as the checkout at hand, andcutsays which. The command follows agent-cli/v2:--format jsoneverywhere,describefor the catalog, and--field NAMEto read one member of the result without ajqexpression.- A release is a signed, annotated tag
vX.Y.Zonmainwhose commit carriesVERSION=X.Y.Zand a datedCHANGELOG.mdentry. Never push a tag beforemake checkpasses on that exact commit, never move or force a tag, never publish from a branch.maelys-release cut DIR X.Y.Z --applydoes exactly that in two stops: it writesVERSION, commits it signed onrelease/vX.Y.Z, opens the pull request and waits for its checks; after the merge,cut DIR X.Y.Z --tag --applysigns the tag on the merge commit those checks ran on. It never merges its own pull request. An adoption does not have to be published: it travels in the next release. - The checks
mainrequires are derived, never typed:maelys-release protect DIRcomputes them and--applywrites them, as a ruleset when a ruleset protects the branch. When this repository still requires the legs under their names before 0.54.0 (check (ubuntu-26.04),check (ubuntu-26.04-arm),check (macos-15)): adopt, merge, thenprotect DIR --apply. The socle keeps reporting those names as aliases, so the adoption loses nothing, andprotect --applyreplaces each alias by its leg in one write: the branch never requires less. It refuses before the adoption is merged. Never narrow a protection to get an adoption through;--without-legsexists for a rename that comes without aliases. adopt DIRwithout--applyprints the plan and, for every socle version between this repository's pin and the checkout, the line that says what that version asks of a product. Read those lines, not the changelog.- A branch is named after the change it carries, with the prefix that
change would take in a commit message (
fix/,docs/,release/…), never after the tool that created it: a name says what changes, not who typed. No list is closed; the commit prefixes this repository already uses are its vocabulary. - A tag whose release or formula failed for a reason outside the code — a
cancelled job, an expired approval, a tap push lost to a race — is replayed
with
gh workflow run release.yml --ref vX.Y.Z -f tag=vX.Y.Z.--refnames the tag: thereleaseenvironment only accepts tagsv*, so a run started from the default branch is refused atpublish. A replay runs the socle that tag pinned; when the socle is at fault, the remedy is a new patch release carrying the corrected pin. A tag is never moved or recreated. - Never commit a secret or a key.
docs/carries what a machine writes and what this repository engages publicly, and no other prose. Where the prose lives is not named from this repository's files, which a public reader may see: do not name a documentation repository here. A private repository that wants the name declares[docs] namedinmaelys-release.conf.- The workflow verifies the tag through the GitHub API, builds on Linux
x86_64, Linux arm64 and macOS arm64 with
scripts/package-release.sh TARGET— or on the targets[targets]names, packaging only on those[package]names when it names any — attests provenance, publishes the GitHub release, renders packaging/homebrew/maelys-egress.rb.in from the tag's own copy, builds bottles when configured and pushes the formula tomaelys-dev/homebrew-tap. - If this repository pins other Maelys repositories: a dependency is
dependencies/<name>.pin(tag on line 1, the commit that tag names on line 2). A repository that declares[dependencies] apartreads$MAELYS_DEPENDENCIES_DIRinstead of a sibling, and the socle it pins from$MAELYS_RELEASE_DIR; every job that builds runssh scripts/checkout-dependencies.sh "$RUNNER_TEMP/dependencies" >>"$GITHUB_ENV"to get both — a variable reaches no other job, each running on another machine. The packages the build needs on the runners are listed independencies/packagesunder[linux]and[macos]; no script installs them..github/workflows/ci.ymlcalls the socle'scheck-product.yml, which reads these declarations itself.maelys-release rehearse DIR TARGETreplays the Linux build job in Docker before a first tag. - If this repository publishes Homebrew formulas: a command is named after
its binary (
maelys-egress), a library after its archive with alibprefix (libmaelys-sys). Dependency pins in a formula are copied from the tag'sdependencies/files, never typed. Tap credentials are the repository secretsHOMEBREW_TAP_TOKENandHOMEBREW_TAP_SIGNING_KEY; without them the tap job renders, lints and reports instead of failing. - If this repository declares runners:
[runners]namesmacos,linux-x86_64andlinux-arm64. A declared runner is honoured wherever only a writer can start the workflow — the release, its channels, the tap — and on the pull-request checks of a private repository only, since on a public one anyone can open a pull request.linux-x86_64is also where the release's write token runs.