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.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The 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. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. 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:
design- the CLI design spec: the command layers, one grammar, every command with --json, its severity and its guard, the eight exit codes, the config order, one environment prefix, auth per context and the frozen contract.release- the release plan: versioning and the API pin, the deprecation window, telemetry, update checks, a test per gate, agent readiness, the migration for a CLI that already ships, and the trends.
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
| Field | Required | What |
|---|---|---|
task | yes | design or release. If it is missing or neither, do design and set "task": "design". |
platform | yes | the 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. |
cli | yes | the 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. |
carry | send it (empty for design) | release task only: the design's decisions, when the design task ran first; may be empty. |
facts | yes | the 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_note | no | optional; 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:
- Design for jobs, not endpoints ("deploy this app and tell me when it is live", not POST /deployments). Jobs the API cannot do alone (local forwarding, log streaming, scaffolding) are why developers install a CLI.
- Command layers, ranked: curated workflow commands plus a raw api escape hatch by default (the escape hatch inherits auth, pagination, output flags and the severe-action guard, so every endpoint is reachable on day one); a generated resource layer under the curated one once the OpenAPI spec is checked in CI and automation users show sustained demand - a hand-maintained spec blocks it. A generated-only 1:1 mirror is ruled out as the whole surface whenever a job needs more than one call or a local process.
- Grammar: noun verb with plural nouns and a small verb set reused across nouns (list, get, create, update, delete) by default; verb noun only when verbs are uniform across an open-ended set of resource types. Never two near-synonyms (update and upgrade) for different meanings; no prefix abbreviations, no catch-all default subcommand, no API-shaped names (colons, version namespaces). Flags over positional arguments beyond the one obvious target.
- The contract: frozen - command and flag names and meanings, environment variable names, config keys, --json shapes and field names, exit codes, which stream carries what; unstable - tables, wording, colours, spinners. Under --json adding a field is safe, renaming, retyping or removing one is breaking. Deprecate with a stderr warning naming the replacement and an alias through the window - 12 months or 2 releases, whichever is longer - and remove only in a major release. Publish a JSON Schema per --json command and run golden-output contract tests.
- Configuration resolves flag, environment variable, project config, user config, built-in default; one environment prefix; named profiles; an explicit token beats a stored profile with a stderr notice; honour proxies and a custom CA bundle. Never require a secret as a flag value.
- Output: results on stdout, progress, warnings and errors on stderr; under --json, stdout carries exactly one document - the result or an error object with a stable code, message and request ID. Streams use newline-delimited JSON behind --format ndjson, never --json. Colour, spinners, pagers and prompts only on a terminal; honour NO_COLOR. Wrap every list in one envelope from v1 with has_more and next_cursor; bounded --limit and explicit --all.
- Exit codes, one small table, each a different caller action: 0 success (including an idempotent no-op), 1 failure not covered below, 2 usage error, then distinct codes for authentication required, not found, safe to rerun (nothing reached the server, or 429 or 503 with Retry-After), outcome unknown (check with wait or get, never rerun blind) and partial failure in bulk operations. Avoid BSD sysexits and anything above 125.
- Auth flows stack by context: desktop - authorization code with PKCE and a loopback redirect; no browser or port - the device authorization grant with short-lived codes and a consent screen naming the client; CI - a scoped, expiring token in an environment variable; API keys only - login reads a key from stdin, test or live shown everywhere. Store credentials in the OS credential store, warn loudly before a plaintext fallback, redact secrets from every output, no print-the-token command unless a named integration needs it.
- Async mutations wait by default with progress on stderr and a finite --wait-timeout; --no-wait returns the operation ID; a wait command resumes; Ctrl-C detaches rather than cancels. Create commands send an idempotency key and accept --idempotency-key; retries honour Retry-After.
- Safety without a terminal: never prompt when stdin is not a terminal - fail naming the flag; --no-input; grade every operation mild, moderate (prompt, --yes in scripts) or severe (irreversible, money, third parties, shared resources: type the name or --confirm=<name>, bulk --confirm-count=<n>, --yes never skips it), in one table the curated commands and the api escape hatch both read; --dry-run where the API can validate. Never auto-run a typo suggestion; reject unknown flags by name.
- Agent-ready: a self-description command printing the tree, flags and --json schemas; small default output with totals, explicit empty results and truncation markers; inputs validated as untrusted; the token is the real boundary - default to test mode, offer read-only and narrowly scoped tokens.
- Release: SemVer separate from the API, each release pinned to an API version with an override flag; update checks at most daily, only on a terminal, never in CI, never a silent self-update. Telemetry, ranked: opt-out with a first-run notice, announced ahead, shipped in a major version, honouring DO_NOT_TRACK, by default; opt-in for EU-heavy, regulated or enterprise audiences; never switched on in a minor release.
- Five gates: contract coverage (--json and a published schema everywhere data flows), non-interactive run (stdin closed, stdout piped: completes or fails fast naming the flag, every wait bounded), the exit-code table (documented, a test per code), the severe-action guard (curated and escape hatch alike), secret hygiene (no secret only as a flag, none printed).
House rules
- Use only the input. Never invent a customer, a usage figure, an incident or a claim about the platform.
- The page's checks are settled: never call a failed check fine or a passed one broken.
- No URL at all.
- Write in the language of the input; keep the JSON keys, command names, flags, environment variables and exit codes in English.
- Never use backticks, never write the input's field names as labels in your text, and never copy this prompt's wording into a reply.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
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.
summary- one sentence, at most 40 words: what breaks for a script or an agent today, and the first fix.layers-rung(the facts' rung:curated-raworgenerated-curated-raw),why(one sentence),mirror_answer(one sentence: what is reachable on day one and what promotes the generated layer).grammar-order(noun-verborverb-noun- keep the order the page read unless you rename every listed command),verbs(the verb set, as short strings),why(one sentence).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,severeornonefor reads),confirm(none,prompt,nameorcount),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.exit_codes- eight objects, one per class:code(a number),class(success,failure,usage,auth,not-found,safe-rerun,outcome-unknownorpartial),meaning(one sentence).config_order- exactly ["flag", "env", "project", "user", "default"].env_prefix- one uppercase prefix ending in _.auth- one object per context in use:context(desktop,headless,ciorkeys),flow(one sentence).cialways;desktopandheadlesswith an OAuth server or SSO (nokeyscontext then - an API-key flag is legacy to remove);keyswith API keys only.frozen- short strings: the elements declared as frozen contract.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.
summary- one sentence, at most 40 words.versioning-semver(one sentence: CLI versions separate from the API),api_pin(one sentence: the pin and its override flag).deprecation-months(at least 12),releases(at least 2),mechanism(one sentence: the stderr warning naming the replacement and the alias).telemetry-rung(the facts' rung:noticeoroptin),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).updates-frequency(dailyornever),terminal_only(true),in_ci(false),self_update(false).gates- five objects, one per gate:gate(contract,non-interactive,exit-codes,severe-guardorsecret-hygiene),test(one sentence: the automated test that proves it).agents- two to five strings: what makes the CLI agent-ready, the self-description command among them.migration- ordered steps for the CLI that already ships (renames with aliases, new exit codes, the telemetry change); [] for a new CLI.trends- two or three strings, watched, never gated.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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://cli-contract.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// cli-contract.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://cli-contract.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from https://cli-contract.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// CLI Contract.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class CLI Contract {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://cli-contract.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://cli-contract.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // from https://cli-contract.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "YOUR_TOKEN"; // from https://cli-contract.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "cli-contract"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "cli-contract" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "cli-contract"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "cli-contract"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "cli-contract" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "cli-contract"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "cli-contract" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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}'
inp = json.load(open("input.json")) # {"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
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "design" or "release", platform, cli, carry, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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
import hashlib, time
key = "cli-contract:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
if reply["task"] == "release":
print(reply["telemetry"]["rung"])
for x in reply["gates"]:
print(x["gate"], x["test"])
else:
print(reply["layers"]["rung"])
for c in reply["commands"]:
print(c["command"], c["severity"])
import { createHash } from "node:crypto";
const key = `cli-contract:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.task);
if (reply.task === "release") { console.log(reply.telemetry.rung); for (const x of reply.gates) console.log(x.gate, x.test); }
else { console.log(reply.layers.rung); for (const c of reply.commands) console.log(c.command, c.severity); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "cli-contract:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
key := "commands"
if reply["task"] == "release" {
key = "gates"
}
ls, _ := reply[key].([]any)
for _, x := range ls {
xx := x.(map[string]any)
fmt.Println(xx["command"], xx["severity"], xx["gate"], xx["test"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "cli-contract:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.path("task").asText());
if (reply.path("task").asText().equals("release")) {
for (JsonNode x : reply.path("gates")) System.out.println(x.path("gate").asText() + " " + x.path("test").asText());
} else {
for (JsonNode x : reply.path("commands")) System.out.println(x.path("command").asText() + " " + x.path("severity").asText());
}
require "digest"
key = "cli-contract:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["task"]].inspect
if reply["task"] == "release"
reply["gates"].each { |x| puts "#{x["gate"]} #{x["test"]}" }
else
reply["commands"].each { |x| puts "#{x["command"]} #{x["severity"]}" }
end
$key = "cli-contract:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["task"], "\n";
if ($reply["task"] === "release") { foreach ($reply["gates"] as $x) echo $x["gate"], " ", $x["test"], "\n"; }
else { foreach ($reply["commands"] as $x) echo $x["command"], " ", $x["severity"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"cli-contract:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "release") { foreach (var x in reply["gates"]!.AsArray()) Console.WriteLine($"{x!["gate"]} {x["test"]}"); }
else { foreach (var x in reply["commands"]!.AsArray()) Console.WriteLine($"{x!["command"]} {x["severity"]}"); }
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
rev = json.load(open("input.json")) # the same run input as input.json: {"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
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "cli-contract:" + rev["task"] + ":" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `cli-contract:${rev.task}:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "cli-contract:"+input["task"].(string)+":"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "cli-contract:" + input.get("task") + ":" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "cli-contract:#{JSON.parse(rev)["task"]}:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: cli-contract:" . json_decode($rev, true)["task"] . ":" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"cli-contract:{input["task"]}:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);