← CLI Contract
API
Tokens

Run CLI Contract from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page reads the command table's grammar, checks --json coverage, severe-action guards, retry, wait and dry-run flags, secrets accepted as flags, the environment prefix and the exit codes against the eight caller actions, scores the five gates and ranks the command layer and telemetry posture, then hands it all to the run as facts. The run writes the design spec or the release plan; the page then holds it to its own facts (ccreply.js).

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"cli-contract"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, platform, cli, facts) is missing. The page always sends all five of task, platform, cli, carry, facts; carry is an empty string for design.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes, chosen by task, in the order the work runs - the surface, then shipping it:

A missing or unknown task is answered as design. Both lanes send the same five fields. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the design's decisions into carry for the release lane.

The input

FieldRequiredWhat
taskyesdesign or release. If it is missing or neither, do design and set "task": "design".
platformyesthe CLI's name, the platform and the interview: who runs it first, the endpoints, whether the spec is checked in CI, multi-call or local jobs, auth, test mode, whether a CLI already ships, the audience's region, the timing, the team and the automation demand; "not given" means not given.
cliyesthe command table as the page read it (each command's flags, kinds, severity and findings), the grammar as read, the exit codes with the class the page read for each, the environment variables, and the behaviour answers.
carrysend it (empty for design)release task only: the design's decisions, when the design task ran first; may be empty.
factsyesthe page's checks, one line each as area | id | status | evidence, the gates and the rungs. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The rules the model applies

From the source skill, @samber/cli-interface-design - the only standard the run applies:

House rules

The replies

design

Write the CLI design spec - the redesign, not a transcript of today. Renaming a command, naming a flag, choosing exit codes and grading severities are design decisions; say what changed.

  1. summary - one sentence, at most 40 words: what breaks for a script or an agent today, and the first fix.
  2. layers - rung (the facts' rung: curated-raw or generated-curated-raw), why (one sentence), mirror_answer (one sentence: what is reachable on day one and what promotes the generated layer).
  3. grammar - order (noun-verb or verb-noun - keep the order the page read unless you rename every listed command), verbs (the verb set, as short strings), why (one sentence).
  4. commands - one object per command: command (the path without the CLI's name), replaces (the listed path it renames, exactly as the page wrote it; "" when the name is unchanged or the command is new), json (true or false), severity (mild, moderate, severe or none for reads), confirm (none, prompt, name or count), add (the flags to add, as short strings; [] when none), remove (the flags to drop, such as a --force that skips a guard or a secret taken as a flag; [] when none). Every listed command appears once, by its path or in a replaces.
  5. exit_codes - eight objects, one per class: code (a number), class (success, failure, usage, auth, not-found, safe-rerun, outcome-unknown or partial), meaning (one sentence).
  6. config_order - exactly ["flag", "env", "project", "user", "default"].
  7. env_prefix - one uppercase prefix ending in _.
  8. auth - one object per context in use: context (desktop, headless, ci or keys), flow (one sentence). ci always; desktop and headless with an OAuth server or SSO (no keys context then - an API-key flag is legacy to remove); keys with API keys only.
  9. frozen - short strings: the elements declared as frozen contract.
  10. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "design",
  "summary": "one sentence",
  "layers": { "rung": "curated-raw", "why": "one sentence", "mirror_answer": "one sentence" },
  "grammar": { "order": "noun-verb", "verbs": [ "list" ], "why": "one sentence" },
  "commands": [ { "command": "apps delete", "replaces": "delete app", "json": true, "severity": "severe", "confirm": "name", "add": [ "--confirm" ], "remove": [ "--force" ] } ],
  "exit_codes": [ { "code": 0, "class": "success", "meaning": "one sentence" } ],
  "config_order": [ "flag", "env", "project", "user", "default" ],
  "env_prefix": "ACME_",
  "auth": [ { "context": "ci", "flow": "one sentence" } ],
  "frozen": [ "command and flag names" ],
  "caveats": [ ]
}

The page holds it to its own facts (check() in ccreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; no percentage; a summary of at most 40 words; the ranked layer and an answer to the mirror request; one grammar and no near-synonym verbs; every listed command present by path or in a replaces, no API-shaped names, every command in the chosen order; json true where data flows, never on a stream; severe stays severe and confirms by name, bulk by count; moderate prompts; creates add --idempotency-key, async commands a bounded --wait-timeout; no secret added as a flag; the eight exit classes once each, whole unique codes, 0 success, 1 failure, 2 usage, nothing above 125 or in the sysexits range; the config order; one uppercase prefix; a CI auth path, desktop and headless with OAuth or SSO, keys with API keys; a frozen list naming --json shapes and exit codes.

release

Write the release plan - prescriptive, fixing what the page flags.

  1. summary - one sentence, at most 40 words.
  2. versioning - semver (one sentence: CLI versions separate from the API), api_pin (one sentence: the pin and its override flag).
  3. deprecation - months (at least 12), releases (at least 2), mechanism (one sentence: the stderr warning naming the replacement and the alias).
  4. telemetry - rung (the facts' rung: notice or optin), plan (one sentence), do_not_track (true, at either rung), off_switch (the CLI's own environment variable that controls telemetry - under opt-in it stays off unless set).
  5. updates - frequency (daily or never), terminal_only (true), in_ci (false), self_update (false).
  6. gates - five objects, one per gate: gate (contract, non-interactive, exit-codes, severe-guard or secret-hygiene), test (one sentence: the automated test that proves it).
  7. agents - two to five strings: what makes the CLI agent-ready, the self-description command among them.
  8. migration - ordered steps for the CLI that already ships (renames with aliases, new exit codes, the telemetry change); [] for a new CLI.
  9. trends - two or three strings, watched, never gated.
  10. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "release",
  "summary": "one sentence",
  "versioning": { "semver": "one sentence", "api_pin": "one sentence" },
  "deprecation": { "months": 12, "releases": 2, "mechanism": "one sentence" },
  "telemetry": { "rung": "notice", "plan": "one sentence", "do_not_track": true, "off_switch": "ACME_TELEMETRY" },
  "updates": { "frequency": "daily", "terminal_only": true, "in_ci": false, "self_update": false },
  "gates": [ { "gate": "contract", "test": "one sentence" } ],
  "agents": [ "a self-description command" ],
  "migration": [ "a step" ],
  "trends": [ "escape-hatch usage per endpoint" ],
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; the same text rules; the ranked telemetry rung, DO_NOT_TRACK, the CLI's own off switch and a plan; at least 12 months and 2 releases of deprecation with a stderr warning and an alias; update checks daily or never, terminal only, never in CI, never a silent self-update; SemVer and the API pin; the five gates once each with a test; two to five agent-readiness strings including the self-description command; a migration when a CLI already ships; two or three trends.

Worked examples

Three runs from the page's free samples (invented CLIs): the design for a deploy CLI that already ships, the release plan for the same CLI carrying that design, and the design for a payments CLI run mostly by coding agents. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

design - A deploy CLI that already ships

The page read it as: scripts and agents will break (11 fail, 11 warn, 3 pass), 8 commands, 0 of 5 gates; layer by rank: curated-raw; telemetry by rank: notice; failing checks: order, synonyms, apishaped, json, unknownflags, exits, noninteractive, severe, secret, telemetry, updates.

Input (cli is 2181 characters; the first 1390 are shown; facts is 3648 characters; the first 1394 are shown):

{
 "task": "design",
 "platform": "CLI: dplox\nWhat the platform does: Deploys web apps and workers; developers deploy from laptops, CI pipelines deploy on merge, and support scripts roll back.\nWho runs it first: CI pipelines; endpoints: 140; OpenAPI spec checked in CI: no; jobs needing more than one call or a local process: yes\nAuth: An OAuth server; separate test mode: no; a CLI already ships: yes; EU-heavy, regulated or enterprise users: no\nTiming: A product surface run for years; thin team: no; sustained automation demand for per-endpoint commands: no",
 "cli": "Commands (command | flags | kinds | severity | the page's findings):\n- apps list | --json --limit | list | severity not given | no bounded --limit with an explicit --all\n- apps create | --name --region --json | create | mild | no --idempotency-key - a rerun after a timeout duplicates it; no --dry-run\n- deploy app | --wait --token | mutate, async | moderate | no --json; no --no-wait; no bounded --wait-timeout; no --dry-run; moderate, but no --yes for a script; a secret accepted as a flag (--token) - it leaks into ps and shell history\n- apps update | --name --json | mutate | mild | no --dry-run\n- apps upgrade | --runtime | mutate | moderate | no --json; no --dry-run; moderate, but no --yes for a script\n- delete app | --force | mutate | severe | no --json; no --dry-run; severe, but no --confirm=<name> - --yes or --force skips it\n- logs tail | --follow | stream | severity not given | a stream with no --format ndjson\n- domains:add | --hostname | create | mild | no --json; no --idempotency-key - a rerun after a timeout duplicates it; no --dry-run\nGrammar as read: noun-verb, against the grain: deploy app, delete app; verbs: list, create, update, upgrade, tail; near-synonyms: update/upgrade\nExit codes:\n- 0 | success | read as success\n- 1 | general failure | read as failure\n- 2 | usage error | read as usage\n- 3 | authentication required | read as auth\n- 4 | not found | read as\n\n[... 791 more characters cut from this display ...]",
 "carry": "",
 "facts": "The page's checks (11 fail, 11 warn, 3 pass, 0 n/a); gates passed 0 of 5:\n- Command layers and grammar | order | fail | against the grain: deploy app, delete app\n- Command layers and grammar | synonyms | fail | update / upgrade\n- Command layers and grammar | apishaped | fail | dplox domains:add - path and version namespaces leaking into the CLI\n- Command layers and grammar | escape | warn | no escape hatch - the curated layer has to chase full coverage\n- The contract surface | json | fail | deploy app (no --json), apps upgrade (no --json), delete app (no --json), logs tail (a stream with no --format ndjson), domains:add (no --json)\n- The contract surface | envelope | warn | bare arrays - moving to an envelope later is breaking\n- The contract surface | limit | warn | apps list\n- The contract surface | envprefix | warn | prefix DPLOX_; off: DEPLOY_REGION\n- The contract surface | configorder | warn | not documented\n- The contract surface | unknownflags | fail | silently ignored - plausible, wrongly scoped results\n- Output, streams and exit codes | jsonerrors | warn | errors not in the --json shape\n- Output, streams and exit codes | tty | pass | detected\n- Output, streams and exit codes | exits | fail | missing: safe to rerun, partial failure in a bulk operation; 255 is in the shell-reserved range above 125\n- Output, streams and exit codes | wait | warn | deploy app; Ctrl-C\n\n[... 2254 more characters cut from this display ...]"
}

Reply:

{
 "task": "design",
 "summary": "In CI today dplox can prompt, silently ignores unknown flags, takes the token as a flag and exits 255 on a deploy timeout; first fix: --no-input, --json on every command, unknown flags rejected and a bounded --wait-timeout.",
 "layers": {
  "rung": "curated-raw",
  "why": "The spec is hand-maintained and unchecked in CI, so a generated layer would freeze its drift into the contract, while deploys and log tails need curated commands.",
  "mirror_answer": "A new api command reaches all 140 endpoints on day one with auth, pagination, output flags and the severity table; a generated layer waits until the OpenAPI spec is checked in CI and per-endpoint automation demand appears."
 },
 "grammar": {
  "order": "noun-verb",
  "verbs": [
   "list",
   "create",
   "update",
   "delete",
   "deploy",
   "wait",
   "tail",
   "login"
  ],
  "why": "Most commands are already noun-verb, so deploy app, delete app and domains:add move under their nouns and upgrade gives way to update on a runtimes noun."
 },
 "commands": [
  {
   "command": "apps list",
   "replaces": "",
   "json": true,
   "severity": "none",
   "confirm": "none",
   "add": [
    "--all"
   ]
  },
  {
   "command": "apps create",
   "replaces": "",
   "json": true,
   "severity": "mild",
   "confirm": "none",
   "add": [
    "--idempotency-key",
    "--dry-run"
   ]
  },
  {
   "command": "apps deploy",
   "replaces": "deploy app",
   "json": true,
   "severity": "moderate",
   "confirm": "prompt",
   "add": [
    "--json",
    "--no-wait",
    "--wait-timeout",
    "--dry-run",
    "--yes"
   ]
  },
  {
   "command": "apps update",
   "replaces": "",
   "json": true,
   "severity": "mild",
   "confirm": "none",
   "add": [
    "--dry-run"
   ]
  },
  {
   "command": "runtimes update",
   "replaces": "apps upgrade",
   "json": true,
   "severity": "moderate",
   "confirm": "prompt",
   "add": [
    "--app",
    "--json",
    "--dry-run",
    "--yes"
   ]
  },
  {
   "command": "apps delete",
   "replaces": "delete app",
   "json": true,
   "severity": "severe",
   "confirm": "name",
   "add": [
    "--json",
    "--confirm",
    "--dry-run"
   ]
  },
  {
   "command": "logs tail",
   "replaces": "",
   "json": false,
   "severity": "none",
   "confirm": "none",
   "add": [
    "--format ndjson"
   ]
  },
  {
   "command": "domains create",
   "replaces": "domains:add",
   "json": true,
   "severity": "mild",
   "confirm": "none",
   "add": [
    "--json",
    "--idempotency-key",
    "--dry-run"
   ]
  },
  {
   "command": "operations wait",
   "replaces": "",
   "json": true,
   "severity": "none",
   "confirm": "none",
   "add": [
    "--json",
    "--wait-timeout"
   ]
  },
  {
   "command": "commands list",
   "replaces": "",
   "json": true,
   "severity": "none",
   "confirm": "none",
   "add": [
    "--json"
   ]
  },
  {
   "command": "auth login",
   "replaces": "",
   "json": true,
   "severity": "mild",
   "confirm": "none",
   "add": [
    "--json",
    "--device"
   ]
  },
  {
   "command": "api",
   "replaces": "",
   "json": true,
   "severity": "moderate",
   "confirm": "prompt",
   "add": [
    "--method",
    "--path",
    "--json",
    "--yes",
    "--confirm",
    "--confirm-count"
   ]
  }
 ],
 "exit_codes": [
  {
   "code": 0,
   "class": "success",
   "meaning": "The command succeeded, including an idempotent no-op."
  },
  {
   "code": 1,
   "class": "failure",
   "meaning": "A failure not covered by another code; read the error object."
  },
  {
   "code": 2,
   "class": "usage",
   "meaning": "Usage error, including an unknown flag or a missing flag off a terminal."
  },
  {
   "code": 3,
   "class": "auth",
   "meaning": "Authentication required: log in or set DPLOX_TOKEN."
  },
  {
   "code": 4,
   "class": "not-found",
   "meaning": "The app, deployment or domain was not found."
  },
  {
   "code": 5,
   "class": "safe-rerun",
   "meaning": "Nothing reached the server, or a 429 or 503 came with Retry-After; rerun after waiting."
  },
  {
   "code": 6,
   "class": "outcome-unknown",
   "meaning": "The outcome is unknown, such as a deploy wait that timed out; check with dplox operations wait, never rerun blind."
  },
  {
   "code": 7,
   "class": "partial",
   "meaning": "A bulk operation partly failed; read the per-item results."
  }
 ],
 "config_order": [
  "flag",
  "env",
  "project",
  "user",
  "default"
 ],
 "env_prefix": "DPLOX_",
 "auth": [
  {
   "context": "desktop",
   "flow": "dplox auth login runs the authorization code flow with PKCE and a loopback redirect, storing the token in the OS credential store."
  },
  {
   "context": "headless",
   "flow": "With no browser or free port, dplox auth login --device runs the device authorization grant with a short-lived code and a consent screen naming dplox."
  },
  {
   "context": "ci",
   "flow": "CI sets a scoped, expiring token in DPLOX_TOKEN; an explicit token beats a stored profile with a stderr notice."
  }
 ],
 "frozen": [
  "command and flag names and meanings",
  "environment variable names",
  "config keys",
  "--json shapes and field names",
  "exit codes",
  "which stream carries what"
 ],
 "caveats": [
  "apps deploy drops --token and apps delete drops --force; the token comes from DPLOX_TOKEN or the credential store, and --yes never skips the name check.",
  "Exit 255 moves to 6 and DEPLOY_REGION becomes DPLOX_REGION; both renames need aliases in the release migration.",
  "The print-the-token command goes, and a plaintext token file is written only after a loud warning.",
  "Lists move from bare arrays to an envelope with has_more and next_cursor, a breaking change for current scripts."
 ]
}

What step 5 prints for it: curated-raw | noun-verb | apps list | apps create | apps deploy (was deploy app) | apps update | runtimes update (was apps upgrade) | apps delete (was delete app) | logs tail | domains create (was domains:add) | operations wait | commands list | auth login | api

The page's check: kept - apps delete keeps --force - say it no longer skips the severe confirmation, or remove it.

release - A deploy CLI that already ships

The input carries the saved design's decisions (R.laneForm fills carry from the design reply), as the page's handoff does.

Input (cli is 2181 characters; the first 1390 are shown; facts is 3413 characters; the first 1394 are shown):

{
 "task": "release",
 "platform": "CLI: dplox\nWhat the platform does: Deploys web apps and workers; developers deploy from laptops, CI pipelines deploy on merge, and support scripts roll back.\nWho runs it first: CI pipelines; endpoints: 140; OpenAPI spec checked in CI: no; jobs needing more than one call or a local process: yes\nAuth: An OAuth server; separate test mode: no; a CLI already ships: yes; EU-heavy, regulated or enterprise users: no\nTiming: A product surface run for years; thin team: no; sustained automation demand for per-endpoint commands: no",
 "cli": "Commands (command | flags | kinds | severity | the page's findings):\n- apps list | --json --limit | list | severity not given | no bounded --limit with an explicit --all\n- apps create | --name --region --json | create | mild | no --idempotency-key - a rerun after a timeout duplicates it; no --dry-run\n- deploy app | --wait --token | mutate, async | moderate | no --json; no --no-wait; no bounded --wait-timeout; no --dry-run; moderate, but no --yes for a script; a secret accepted as a flag (--token) - it leaks into ps and shell history\n- apps update | --name --json | mutate | mild | no --dry-run\n- apps upgrade | --runtime | mutate | moderate | no --json; no --dry-run; moderate, but no --yes for a script\n- delete app | --force | mutate | severe | no --json; no --dry-run; severe, but no --confirm=<name> - --yes or --force skips it\n- logs tail | --follow | stream | severity not given | a stream with no --format ndjson\n- domains:add | --hostname | create | mild | no --json; no --idempotency-key - a rerun after a timeout duplicates it; no --dry-run\nGrammar as read: noun-verb, against the grain: deploy app, delete app; verbs: list, create, update, upgrade, tail; near-synonyms: update/upgrade\nExit codes:\n- 0 | success | read as success\n- 1 | general failure | read as failure\n- 2 | usage error | read as usage\n- 3 | authentication required | read as auth\n- 4 | not found | read as\n\n[... 791 more characters cut from this display ...]",
 "carry": "Layers: curated-raw; grammar: noun-verb (list, create, update, delete, deploy, wait, tail, login)\nRenames: deploy app -> apps deploy; apps upgrade -> runtimes update; delete app -> apps delete; domains:add -> domains create\nCommands (severity, confirm, flags added): apps list none/none +--all; apps create mild/none +--idempotency-key +--dry-run; apps deploy moderate/prompt +--json +--no-wait +--wait-timeout +--dry-run +--yes; apps update mild/none +--dry-run; runtimes update moderate/prompt +--app +--json +--dry-run +--yes; apps delete severe/name +--json +--confirm +--dry-run; logs tail none/none +--format ndjson; domains create mild/none +--json +--idempotency-key +--dry-run; operations wait none/none +--json +--wait-timeout; commands list none/none +--json; auth login mild/none +--json +--device; api moderate/prompt +--method +--path +--json +--yes +--confirm +--confirm-count\nExit codes: 0 success, 1 failure, 2 usage, 3 auth, 4 not-found, 5 safe-rerun, 6 outcome-unknown, 7 partial\nEnvironment prefix: DPLOX_; auth: desktop, headless, ci\nFrozen: command and flag names and meanings, environment variable names, config keys, --json shapes and field names, exit codes, which stream carries what",
 "facts": "The page's checks (11 fail, 11 warn, 3 pass, 0 n/a); gates passed 0 of 5:\n- Command layers and grammar | order | fail | against the grain: deploy app, delete app\n- Command layers and grammar | synonyms | fail | update / upgrade\n- Command layers and grammar | apishaped | fail | dplox domains:add - path and version namespaces leaking into the CLI\n- Command layers and grammar | escape | warn | no escape hatch - the curated layer has to chase full coverage\n- The contract surface | json | fail | deploy app (no --json), apps upgrade (no --json), delete app (no --json), logs tail (a stream with no --format ndjson), domains:add (no --json)\n- The contract surface | envelope | warn | bare arrays - moving to an envelope later is breaking\n- The contract surface | limit | warn | apps list\n- The contract surface | envprefix | warn | prefix DPLOX_; off: DEPLOY_REGION\n- The contract surface | configorder | warn | not documented\n- The contract surface | unknownflags | fail | silently ignored - plausible, wrongly scoped results\n- Output, streams and exit codes | jsonerrors | warn | errors not in the --json shape\n- Output, streams and exit codes | tty | pass | detected\n- Output, streams and exit codes | exits | fail | missing: safe to rerun, partial failure in a bulk operation; 255 is in the shell-reserved range above 125\n- Output, streams and exit codes | wait | warn | deploy app; Ctrl-C\n\n[... 2019 more characters cut from this display ...]"
}

Reply:

{
 "task": "release",
 "summary": "Fix CI safety at once, then freeze the redesigned contract in the next major: old names kept as warning aliases, exit 255 moved to 6, and telemetry turned back off until an announced opt-out with a first-run notice.",
 "versioning": {
  "semver": "dplox follows SemVer on its own line, separate from the platform API's versions, and changes frozen contract only in a major release.",
  "api_pin": "Each dplox release is pinned to one API version, overridable per call with --api-version or DPLOX_API_VERSION."
 },
 "deprecation": {
  "months": 12,
  "releases": 2,
  "mechanism": "Each old name stays as an alias that runs the new command and prints a stderr warning naming the replacement, for 12 months or 2 releases, whichever is longer, and goes only in a major release."
 },
 "telemetry": {
  "rung": "notice",
  "plan": "Turn off the telemetry a minor release switched on, announce the change ahead, then ship opt-out collection with a first-run notice in the next major.",
  "do_not_track": true,
  "off_switch": "DPLOX_TELEMETRY"
 },
 "updates": {
  "frequency": "daily",
  "terminal_only": true,
  "in_ci": false,
  "self_update": false
 },
 "gates": [
  {
   "gate": "contract",
   "test": "Golden-output contract tests validate every --json command, including the list envelope and the error object, against its published JSON Schema."
  },
  {
   "gate": "non-interactive",
   "test": "Every command runs with stdin closed and stdout piped under a timeout and must finish or exit 2 naming the missing flag, with no prompt, pager or update check."
  },
  {
   "gate": "exit-codes",
   "test": "One test per documented code forces its condition against a mock server, including a 503 with Retry-After for 5 and a timed-out apps deploy wait for 6."
  },
  {
   "gate": "severe-guard",
   "test": "apps delete and a severe endpoint through api exit 2 without --confirm=<name>, even when --yes or --force is passed."
  },
  {
   "gate": "secret-hygiene",
   "test": "No command accepts a token only as a flag, and a canary token never appears on stdout, stderr or in --json output."
  }
 ],
 "agents": [
  "dplox commands list prints the command tree, flags and --json schemas, so agents stop scraping help text.",
  "Every list returns one envelope with totals, has_more and next_cursor, and an explicit empty result.",
  "Under --json, errors are one document with a stable code, message and request ID.",
  "Agents get narrowly scoped, expiring tokens, since the token is the real boundary."
 ],
 "migration": [
  "In the next release, turn telemetry off, stop update checks in CI and off a terminal, drop the pager and prompts off a terminal, and add --no-input.",
  "Add apps deploy, runtimes update, apps delete and domains create, keeping deploy app, apps upgrade, delete app and domains:add as warning aliases through the window.",
  "Read DPLOX_REGION and keep DEPLOY_REGION as a warning alias; warn on every --token use and point to DPLOX_TOKEN or the credential store.",
  "Move tokens to the OS credential store with a loud warning before any plaintext fallback, and deprecate the print-the-token command.",
  "Announce the next major: exit 255 becomes 6 with new codes 5 and 7, lists move to the envelope, unknown flags are rejected and --force no longer skips the name check.",
  "Ship that major with opt-out telemetry, a first-run notice and DO_NOT_TRACK honoured, removing aliases only once their window has run."
 ],
 "trends": [
  "Escape-hatch usage per endpoint, to see which jobs deserve curated commands.",
  "Calls still using deprecated aliases, DEPLOY_REGION or --token.",
  "How often each exit code is returned, especially 6 after apps deploy."
 ],
 "caveats": [
  "The input names no API version, so the pinned version is the team's to choose.",
  "Until the major ships, --force still skips the delete guard, so the severe-action gate stays open."
 ]
}

What step 5 prints for it: notice | 12 months | contract | non-interactive | exit-codes | severe-guard | secret-hygiene

The page's check: kept - nothing to flag

design - A payments CLI run by coding agents

The page read it as: scripts and agents will break (4 fail, 0 warn, 19 pass), 8 commands, 2 of 5 gates; layer by rank: curated-raw; telemetry by rank: optin; failing checks: json, exits, severe, selfdescribe.

Input (cli is 1876 characters; the first 1389 are shown; facts is 2586 characters; the first 1397 are shown):

{
 "task": "design",
 "platform": "CLI: paxlo\nWhat the platform does: Payments: charges, refunds and payouts; most invocations now come from coding agents and CI jobs.\nWho runs it first: Coding agents; endpoints: 310; OpenAPI spec checked in CI: yes; jobs needing more than one call or a local process: yes\nAuth: API keys only; separate test mode: yes; a CLI already ships: yes; EU-heavy, regulated or enterprise users: yes\nTiming: A product surface run for years; thin team: no; sustained automation demand for per-endpoint commands: no",
 "cli": "Commands (command | flags | kinds | severity | the page's findings):\n- charges list | --json --limit --all | list | severity not given\n- charges get | --json | read | severity not given\n- charges create | --json --idempotency-key --dry-run | create | mild\n- refunds create | --json --idempotency-key --dry-run --confirm | create | severe\n- payouts send | --json --idempotency-key --dry-run --confirm-count | bulk, create | severe\n- logs tail | --json | stream | severity not given | a stream with no --format ndjson; --json on a stream - newline-delimited JSON is its own format\n- listen | --format --forward-to | stream | severity not given\n- api | --method --data --json --yes --dry-run | mutate | moderate\nGrammar as read: noun-verb; verbs: list, get, create, send, tail\nExit codes:\n- 0 | success | read as success\n- 1 | failure not covered below | read as failure\n- 2 | usage error | read as usage\n- 3 | authentication required | read as auth\n- 4 | not found | read as not-found\n- 5 | safe to rerun: 429 or 503 with Retry-After | read as safe-rerun\n- 70 | outcome unknown - check with paxlo wait | read as outcome-unknown\n- 6 | partial failure in a bulk payout | read as partial\nEnvironment variables: PAXLO_API_KEY, PAXLO_PROFILE, PAXLO_NO_INPUT, PAXLO_API_URL, DO_NOT_TRACK (prefix PAXLO_)\nBehaviour: api escape hatch yes (reads the severity table no); terminal detection and NO_COLOR yes;\n\n[... 487 more characters cut from this display ...]",
 "carry": "",
 "facts": "The page's checks (4 fail, 0 warn, 19 pass, 0 n/a); gates passed 2 of 5:\n- Command layers and grammar | order | pass | noun-verb, 8 commands\n- Command layers and grammar | synonyms | pass | list, get, create, send, tail\n- Command layers and grammar | escape | pass | every endpoint reachable on day one\n- The contract surface | json | fail | logs tail (a stream with no --format ndjson; --json on a stream - newline-delimited JSON is its own format)\n- The contract surface | envelope | pass | enveloped\n- The contract surface | limit | pass | bounded\n- The contract surface | envprefix | pass | PAXLO_ for all 4\n- The contract surface | configorder | pass | documented\n- The contract surface | unknownflags | pass | rejected by name\n- Output, streams and exit codes | jsonerrors | pass | yes\n- Output, streams and exit codes | tty | pass | detected\n- Output, streams and exit codes | exits | fail | all eight; 70 is a BSD sysexits code - deprecated, not a portability promise\n- Output, streams and exit codes | idem | pass | accepted\n- Safety without a terminal | noninteractive | pass | safe\n- Safety without a terminal | severe | fail | the api escape hatch skips the severity table\n- Safety without a terminal | dryrun | pass | offered\n- Safety without a terminal | typo | pass | suggested only\n- Auth and credentials | secret | pass | The OS credential store\n- Auth and credentials | testmode |\n\n[... 1189 more characters cut from this display ...]"
}

Reply:

{
 "task": "design",
 "summary": "Agents can send money through the api escape hatch without the severity table, must scrape help text, get logs tail behind --json and see sysexits code 70; first fix: api reads the shared severity table.",
 "layers": {
  "rung": "curated-raw",
  "why": "Charges, refunds, payouts and local event forwarding are jobs that earn curated commands, and no sustained demand for per-endpoint commands calls for a generated layer.",
  "mirror_answer": "The api escape hatch already reaches all 310 endpoints on day one; the spec is checked in CI, so sustained automation demand for per-endpoint commands is what would promote a generated layer."
 },
 "grammar": {
  "order": "noun-verb",
  "verbs": [
   "list",
   "get",
   "create",
   "send",
   "tail",
   "listen"
  ],
  "why": "The CLI is already noun-verb with no near-synonyms, so only the bare listen moves under an events noun."
 },
 "commands": [
  {
   "command": "charges list",
   "replaces": "",
   "json": true,
   "severity": "none",
   "confirm": "none",
   "add": []
  },
  {
   "command": "charges get",
   "replaces": "",
   "json": true,
   "severity": "none",
   "confirm": "none",
   "add": []
  },
  {
   "command": "charges create",
   "replaces": "",
   "json": true,
   "severity": "severe",
   "confirm": "name",
   "add": [
    "--confirm"
   ]
  },
  {
   "command": "refunds create",
   "replaces": "",
   "json": true,
   "severity": "severe",
   "confirm": "name",
   "add": []
  },
  {
   "command": "payouts send",
   "replaces": "",
   "json": true,
   "severity": "severe",
   "confirm": "count",
   "add": []
  },
  {
   "command": "logs tail",
   "replaces": "",
   "json": false,
   "severity": "none",
   "confirm": "none",
   "add": [
    "--format ndjson"
   ]
  },
  {
   "command": "events listen",
   "replaces": "listen",
   "json": false,
   "severity": "none",
   "confirm": "none",
   "add": []
  },
  {
   "command": "commands list",
   "replaces": "",
   "json": true,
   "severity": "none",
   "confirm": "none",
   "add": [
    "--json"
   ]
  },
  {
   "command": "api",
   "replaces": "",
   "json": true,
   "severity": "moderate",
   "confirm": "prompt",
   "add": [
    "--confirm",
    "--confirm-count"
   ]
  }
 ],
 "exit_codes": [
  {
   "code": 0,
   "class": "success",
   "meaning": "The command succeeded, including an idempotent no-op."
  },
  {
   "code": 1,
   "class": "failure",
   "meaning": "A failure not covered by another code; read the error object."
  },
  {
   "code": 2,
   "class": "usage",
   "meaning": "Usage error, including an unknown flag or a missing confirmation flag off a terminal."
  },
  {
   "code": 3,
   "class": "auth",
   "meaning": "Authentication required: log in or set PAXLO_API_KEY."
  },
  {
   "code": 4,
   "class": "not-found",
   "meaning": "The charge, refund or payout was not found."
  },
  {
   "code": 5,
   "class": "safe-rerun",
   "meaning": "Nothing reached the server, or a 429 or 503 came with Retry-After; rerun after waiting."
  },
  {
   "code": 6,
   "class": "partial",
   "meaning": "A bulk payout partly failed; read the per-item results."
  },
  {
   "code": 7,
   "class": "outcome-unknown",
   "meaning": "The outcome is unknown; check with paxlo wait or the matching get command, never rerun blind."
  }
 ],
 "config_order": [
  "flag",
  "env",
  "project",
  "user",
  "default"
 ],
 "env_prefix": "PAXLO_",
 "auth": [
  {
   "context": "keys",
   "flow": "Login reads the API key from stdin into the OS credential store, and test or live mode is shown on every command."
  },
  {
   "context": "ci",
   "flow": "CI and agents set a scoped, expiring test-mode key in PAXLO_API_KEY by default; an explicit key beats a stored profile with a stderr notice."
  }
 ],
 "frozen": [
  "command and flag names and meanings",
  "environment variable names",
  "config keys",
  "--json shapes and field names",
  "exit codes",
  "which stream carries what"
 ],
 "caveats": [
  "charges create moves from mild to severe because it moves money, so scripts must pass --confirm; the release migration covers it.",
  "api grades each call from the same severity table as the curated commands; moderate is its floor, and severe endpoints need --confirm or --confirm-count.",
  "logs tail drops --json for --format ndjson, and exit 70 moves to 7; both are breaking for current scripts.",
  "commands list prints the tree, flags and --json schemas so agents stop scraping help text."
 ]
}

What step 5 prints for it: curated-raw | noun-verb | charges list | charges get | charges create | refunds create | payouts send | logs tail | events listen (was listen) | commands list | api

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./cckit.js"); require("./ccreply.js")), then:

const K = CliKit, R = CliReply;
const f = K.form(Object.assign(K.blank(), { name: "acme", commands: "acme apps list | --json --limit --all | list\nacme apps delete | --json --confirm --dry-run | mutate | severe",
  exits: "0 | success\n1 | failure\n2 | usage error\n3 | authentication required" }));
// f.cmds (each command with its issues), f.gram (grammar), f.exit (missing caller actions), f.gate (the five gates), f.layer, f.tel
// K.exitMd(f), K.severityMd(f), K.selfDescribe(f) - the tables and the self-description
const b = K.buildInput(f, "design");                         // or "release" (R.laneForm(form, "release", design) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "design");
// R.check(reply, b)                                         // {status: kept or flagged, notes}

Get a token for the steps below from /tokens.html.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://cli-contract.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"cli-contract"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "design", "platform": "CLI: dplox\nWhat the platform does: ...", "cli": "Commands (command | flags | kinds | severity | the page's findings):\n- apps list | --json --limit | list ...", "carry": "", "facts": "The page's checks (11 fail, ...); gates passed 0 of 5:\n- Command layers and grammar | order | fail | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses cli-contract:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A design reply carries summary, layers, grammar, commands, exit_codes, config_order, env_prefix, auth, frozen and caveats; a release reply carries summary, versioning, deprecation, telemetry, updates, gates, agents, migration, trends and caveats. The code below prints the gist of either: the layer and each command, or the telemetry rung and each gate.

KEY="cli-contract:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "release" then (.telemetry.rung, (.gates[] | "\(.gate): \(.test)")) 
       else (.layers.rung, (.commands[] | "\(.command) \(.severity)")) end' reply.json

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: cli-contract:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json