Reference

CLI reference

descant is one binary generated from the same contract as the API: every command word, flag, help sentence and exit code below is read from it, so the binary cannot drift from the API it drives. Install it with the one-line installer.

Setup

descant login is the documented way to hold a key. It reads the key at a prompt that does not echo and puts it in the machine's own credential store: the macOS keychain, the system secret service on Linux, and otherwise a plain file. The command names the store it used, and says so plainly when this machine has no keychain tool and it used the file instead.

It does not downgrade in the other direction. When a store is present but refuses the write — a locked keychain, a secret service that will not unlock — descant login fails and says why. It does not quietly put the key in a file you did not choose, so a machine that looks like it has a keychain never turns out to be keeping your key on disk instead.

Where that file lands depends on the platform, so check the path the command printed rather than assuming one — this is the path to audit or delete by hand. The platform is decided first, and only then the variable:

  • On Windows, %APPDATA%\descant\credential — and ~\AppData\Roaming\descant\credential when APPDATA is unset, as it can be for a service account or a stripped image. XDG_CONFIG_HOME is not read on Windows, set or not.
  • Everywhere else, $XDG_CONFIG_HOME/descant/credential when that variable is set, and ~/.config/descant/credential when it is not. There the file is written 0600, owner-only.

On Windows that file is not permission-protected, and it is the ordinary path rather than a fallback. No Windows store can be read back once written, so descant login uses the file there by default. The binary asks for mode 0600 when it writes it, but on NTFS that sets the read-only attribute rather than an owner-only ACL — so the mode is not the protection it is on the other two platforms, and any account that can read your profile directory can read the key.

It is still the better of the two options there. Reaching for DESCANT_API_KEY instead would be a step down, not up: an exported variable is inherited by every process the shell starts, lands in shell history, and is visible in the process table — none of which is true of the file. So keep descant login on a Windows desktop, treat the profile directory as the thing actually protecting the key, and run descant logout when the machine no longer needs it. The variable's place is the next section: a CI runner or a container, which is ephemeral and holds it as a job secret.

descant logout removes it from whichever store holds it and names any it could not clear — including one it could not read, which it will not quietly treat as empty. A store it could not read but could still clear is reported as removed, because nothing is left in it. A store it could neither read nor clear is named, and logout tells you that you are not signed out rather than reporting success over a credential that would go live again the moment that store opens.

descant login
export DESCANT_API_BASE_URL=https://app.descant.run
descant run list

The key is never echoed, never written to shell history and never placed in a command argument — the store is fed it on standard input. descant whoami reports which credential is in use and which store it came from, identified by a short fingerprint rather than by any part of its value.

DESCANT_API_BASE_URL is the origin every request is sent to — set it to https://app.descant.run. It is not a secret, so it stays an ordinary environment variable.

--base-url overrides the variable for one invocation: The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Keys in CI and containers

A CI runner or a container has no keychain, so DESCANT_API_KEY keeps working and is the path to use there. It carries a tenant API key (dsc_…), created in the dashboard and shown once; the tenant is taken from the key, never from the request. Hand it to the job as a secret — not as a line in a script you commit.

export DESCANT_API_KEY=dsc_…
export DESCANT_API_BASE_URL=https://app.descant.run
descant run list

A stored credential wins. When both exist, the one descant login stored is used and the variable is ignored. Nothing switches silently: descant whoami names the one in use and says the variable is not being used, and --dry-run prints where the bearer came from on any of the generated commands below. descant login, descant logout and descant whoami take no flags at all — they refuse anything but --help and change nothing — so ask them the question by running descant whoami itself.

A store that exists but cannot be read — a locked keychain, a credential file whose permissions were changed — refuses the command with exit 3 rather than quietly using something else. That holds both ways round, which is the part worth knowing on a machine that has had more than one store written to it: it will not fall back to the variable, and it will not fall back to a key sitting in a weaker store underneath the one that faulted. Either would present a different key, possibly another tenant's, and a mutation is the wrong place to discover it. The refusal names the store that actually failed, so you fix the right one.

Commands

What descant --help prints for the commands a tenant key can run:

descant — the Descant CLI. Every command is generated from the API contract.

Usage:
  descant <command> [arguments] [flags]
  Command words and positional arguments come BEFORE flags.

Commands:
  descant run list                  List runs for the authenticated tenant.
  descant run show                  Read one run's timeline and findings, as its page shows them.
  descant run cancel                Stop a run that should not be running.
  descant repo list                 List the authenticated tenant's repositories.
  descant repo update               Pause a repository, or change what it picks and in what order.
  descant repo settings-audit       Show who paused or resumed a repository, and when.
  descant repo poller-status        Show why a repository is or is not being polled.
  descant repo config               Show what a repository is configured to pick, and in what order.
  descant repo issues               List a repository's open issues, as the enqueue picker sees them.
  descant repo labels               List a repository's label names for the eligibility-label setting.
  descant repo diagnostics          Explain why a repository's issues would or would not be picked.
  descant audit list                List the authenticated tenant's API calls, newest first.
  descant key list                  List the authenticated tenant's API keys.
  descant key create                Mint a key holding a subset of this key's scopes.
  descant key revoke                Revoke one of the authenticated tenant's API keys.
  descant key rotate                Replace a key with a successor; the old one lives on for a grace window.
  descant invite list               List this account's invites, with the budget left.
  descant invite create             Mint one invite and reveal its code once.
  descant invite revoke             Revoke one invite and reclaim its budget slot.
  descant repo grooming candidates  Read the relationships Descant would propose for a repository's backlog.
  descant repo grooming document    Read the relationship document a repository's issues encode.
  descant repo grooming dispatch    Land one relationship edit in a repository's issue bodies.
  descant repo order-preview        Preview the whole ordering walk under an unsaved draft.
  descant repo ordering-preview     Preview what one ordering query matches.

Credentials:
  descant login                     Store an API key so it is not in your shell.
  descant logout                    Remove the stored API key.
  descant whoami                    Show which credential is in use, and from where.
  descant version                   Show the contract version this binary was generated from.

Agents:
  descant mcp                       Serve this account's API to an agent as MCP tools, over stdio.
  descant plugin install            Install the Descant plugin — Skills plus an MCP server entry — into an agent client.

This binary:
  descant update                    Replace this binary with the newest published release, checksum-verified. `upgrade` is the same command.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

  descant <command> --help  shows that command's arguments, response fields and errors.
  descant version --spec    lists every command this binary reaches.

descant run list

List runs for the authenticated tenant. Drives GET /api/v1/runs.

descant run list [flags]

  List runs for the authenticated tenant.

  List this key's tenant's runs, newest first.

  Read-only. The key's tenant is taken from the matched key row and never from the request, so a
  key cannot read another tenant's runs. Rate limited per source before authentication, so an
  unauthenticated caller cannot probe cheaply. Ownership-scoped (#3307): runs of a repository
  the tenant has since removed are still returned, because `customer.repos.list` still shows
  that repository and eliding its runs would lose audit history. Paged by an opaque page token —
  a run history is unbounded, and this list ALREADY stopped at fifty rows, silently, with no way
  to ask for the fifty-first; follow `nextPageToken` until it is `null`. The rows arrive as
  `items`, not `runs`: the array was never going to stay complete, and a renamed field is a
  break a caller can see rather than a truncation it cannot. `billing` is a tenant fact read
  once per request and rides every page unchanged. `repositoryId`, `state` and the
  `createdAfter`/`createdBefore` window each narrow the page and are each optional; absent means
  unfiltered. `invalid_query` covers a malformed page token — one this server did not mint, or
  one minted by another paged read — which is refused rather than read as `start over`.

Flags:
  --limit <number>
    Page size. Defaults to 50, clamped to 200; the applied value is echoed.
  --page-token <string>
    Opaque, server-minted. Pass the previous response's `nextPageToken` verbatim; never construct one.
  --repository-id <string>
    Only this repository's runs. The id `customer.repos.list` returns, which is the only programmatic source of one. A repository belonging to another tenant matches nothing rather than erroring — the ownership join answers before the filter does.
  --state <idle|picking|planning|implementing|opening_pr|under_review|resolving|merging|capturing_learnings|reviewing_learnings|resolving_learnings|merging_learnings|filing_residuals|grooming_issues|refreshing_wiki|verifying|positioning_pr|positioning_resolve|self_reviewing|fixing_ci|decomposing|done|failed|cancelled> (repeatable)
    Repeatable. The run's CURRENT state, not its outcome — `failed` and `cancelled` are terminal, `under_review` is not. Absent or empty means every state.
  --created-after <string>
    Runs created at or after this instant, INCLUSIVE. Filters the column the list orders by.
  --created-before <string>
    Runs created at or before this instant, INCLUSIVE. Filters the column the list orders by.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  items[].issue
    The issue's external identifier, as the provider names it.
  items[].issueTitle
    Snapshotted on the run (#3262). `null` on legacy runs and on seed paths that could not fetch it.
  items[].state
    The pipeline state. Open by design: states are added as the pipeline grows.
  items[].dispatchBlockedReason
    `slot`: the run's account is at its concurrency cap (#1995). `vendor_budget`: the fleet vendor budget, or this tenant's fair share of it, is exhausted (#7553), and only ever present when the operator has armed that budget. `null` on a run that is not parked.
  items[].dispatchWaiting
    The dashboard's own reading of why this run rests. `null` when it is not waiting on any of these.
  nextPageToken
    `null` when this is the last page.
  limit
    The page size actually applied, after clamping.
  billing.pauseReason
    The persisted pause reason, narrowed to the known set — an unrecognised future value reads as `null` rather than leaking.
  billing.outOfCredits
    Derived: the pause is a balance pause rather than an operator or cap pause. `false` with no billing signal.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_query (400) → exit 1
    A query parameter failed validation.
  rate_limited (429) → exit 4
    Too many requests for this credential or source.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  GET /api/v1/runs
  operation customer.runs.list

descant run show

Read one run's timeline and findings, as its page shows them. Drives GET /api/v1/runs/{runId}.

descant run show <runId>

  Read one run's timeline and findings, as its page shows them.

  Read one run's timeline and findings, exactly as its own page shows them.

  The run page's live snapshot under the key's own tenant: the five canonical phases as
  `deriveTimeline` derives them, their statuses and mapped copy, the server clocks a stall is
  measured against, the review findings and the follow-up counts. IT PUBLISHES WHAT THE PAGE
  RENDERS, through the same projection — so the two cannot tell a customer different stories
  about one run, and a test pins that they call the same function. The MAPPED failure copy
  crosses and the raw failure code does not; the derived findings view crosses and the raw
  review checkpoint does not. Measure every age against `serverNowMs` rather than the caller's
  clock. A missing run and another tenant's run are one indistinguishable `not_found`, so this
  is not an oracle for what somebody else owns.

Arguments:
  <runId>

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  state
    The pipeline state. Open by design, as the list read's is.
  steps
    The five canonical phases as `deriveTimeline` derives them — the SAME call the page makes.
  steps[].errorMessage
    The MAPPED customer-facing copy, never the raw failure code — the projection strips that before it reaches any surface.
  steps[].reviewIterations
    How many review passes actually RAN. Present on the review step whenever that count is at least one, so `1` is an ordinary reviewed run rather than an anomaly; absent when no pass ran, and absent on every other step. The count is not a raw tally of review states: the pre-review positioning step maps to the same phase and is not a pass, and a drift-reposition abort re-enters the review state without running the reviewer, so it is subtracted rather than counted.
  stepDetails
    Sub-steps keyed by step `key`, for the steps that have them.
  serverNowMs
    The server clock this response was built at. Anchor every age against THIS, not the caller's clock.
  stallAnchorMs
    When the current customer-VISIBLE phase was entered — what the stall clock measures from. A multi-state visible phase deliberately does not reset it.
  stateEnteredAtMs
    The INTERNAL state's entry, verbatim — not bumped on a resolve self-loop.
  currentStepNotStarted
    The run is at a phase that has produced no heartbeat, so it renders `stalled` rather than `current`. Derived ONCE server-side so every pane reads the same answer.
  awaitingManualMerge
    A done run whose deliberately-unmerged PR still awaits a human.
  skippedStepNote
    The note a `skipped` Merge step renders.
  mergeStepNote
    Non-null only where the merge took the host-backstop path because the Checks permission was never granted.
  heldStepNote
    Non-null only where the timeline actually carries a `held` Merge step.
  seededFromRunId
    The run this one was seeded from, where it was.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_path (400) → exit 1
    A path parameter failed validation.
  not_found (404) → exit 1
    No such resource for this caller.
  rate_limited (429) → exit 4
    Too many requests for this credential or source.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  GET /api/v1/runs/{runId}
  operation customer.runs.get

descant run cancel

Stop a run that should not be running. Drives POST /api/v1/runs/{runId}/cancel.

descant run cancel <runId>

  Stop a run that should not be running.

  Stop a run that should not be running.

  The write that stops spend. Answers WHICH ending it reached rather than a boolean:
  `cancel_requested` is recorded-but-not-yet-stopped and calls for a re-read, while
  `already_finished` is final and calling again will never change it. Refusals keep their own
  discriminant, and an `outcome_unknown` answer means the cancel MAY have landed: verify the run
  before calling again. A deployment that is not wired to the runner answers `internal_error`,
  which is an operator problem and not something a caller can fix by retrying.

Arguments:
  <runId>

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  outcome
    How the cancel ended. `cancelled` is stopped. `cancel_requested` is RECORDED BUT NOT YET STOPPED — re-read the run rather than calling again. `already_finished` means there was nothing to stop and never will be. `privately_dispatched` means the work left this estate and cannot be recalled from here.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  rate_limited (429) → exit 4
    Too many requests for this credential or source.
  not_found (404) → exit 1
    No such resource for this caller.
  outcome_unknown (504) → exit 6
    The action may have taken effect; verify before retrying.
  upstream_auth (502) → exit 7
    An upstream service rejected this deployment's credential.
  upstream_timeout (503) → exit 7
    An upstream service did not answer in time.
  upstream_unavailable (503) → exit 7
    A service this operation depends on did not answer.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  POST /api/v1/runs/{runId}/cancel
  operation customer.runs.cancel

descant repo list

List the authenticated tenant's repositories. Drives GET /api/v1/repos.

descant repo list

  List the authenticated tenant's repositories.

  List this key's tenant's repositories.

  Read-only, and the entry point for every repo-scoped operation: the `id` each of those takes
  is obtainable nowhere else on the programmatic surface. The key's tenant is taken from the
  matched key row and never from the request, so a key cannot list another tenant's
  repositories. Rate limited per source BEFORE authentication, so an unauthenticated caller
  cannot probe cheaply. Unpaginated — the whole list comes back.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  repos[].id
    The tenant-repo id. What every repo-scoped operation takes.
  repos[].owner
    The account or organisation that owns the repository on its provider.
  repos[].repo
    The repository name, without the owner.
  repos[].onboardedStatus
    Where this repository is in onboarding, as the column stores it. `pending` until both installation ids are populated and the validation smoke test succeeds, then `active`; `disconnected` is the terminal state after self-serve offboarding. NOT a closed set — the column carries no constraint, so treat an unrecognised value as unknown rather than as an error.
  repos[].paused
    Whether the customer has paused this repository. A paused repository is not dispatched.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  rate_limited (429) → exit 4
    Too many requests for this credential or source.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  GET /api/v1/repos
  operation customer.repos.list

descant repo update

Pause a repository, or change what it picks and in what order. Drives PATCH /api/v1/repos/{id}.

descant repo update <id> [flags]

  Pause a repository, or change what it picks and in what order.

  Change whether a repository is paused, and what it picks.

  A PARTIAL update: send only the fields you mean to change. An empty object is an accepted
  no-op answering `changed: false`, not a refusal — the published schema accepts it, so refusing
  it would make this contract disagree with the server about what is a valid body. An ABSENT
  field is left alone; a `null` `maxConcurrentRunsThisRepo` CLEARS the sub-cap, which is a real
  write rather than an absence. A sub-cap above the account cap is inert, not refused — the
  effective cap is the lower of the two. `changed: false` is a success: re-sending the current
  state is an accepted no-op. An eligibility label must already exist on the repository — this
  operation does not create it, and a missing label makes pickup find nothing without an error.
  A label spelled like a label-mapping value is refused. Read the stored pickup settings back
  with the repo config read; this response carries the repository's own fields only.

Arguments:
  <id>

Flags:
  --paused / --no-paused
    Whether this repository is paused. A paused repository is not dispatched.
  --order-strategy <oldest-first|newest-first>
    The direction the picker walks a tier. Built from the same `@descant/types` constant the runtime narrows against, so the contract and the picker cannot disagree about the vocabulary.
  --max-concurrent-runs-this-repo <number|null>
    This repository's concurrency sub-cap. `null` CLEARS it, which is a real write and not an absence — the account cap alone then governs. The effective cap is `min(account, repo)`, so a value ABOVE the account cap is INERT rather than rejected: it will not raise anything. The upper bound is the column's own storage range, not a product ceiling.
  --eligibility-label <string>
    The label an issue must carry to be eligible in `labeled` mode. Stored trimmed. THE LABEL MUST ALREADY EXIST ON THE REPOSITORY: this write does not create it, and a label the repository lacks makes pickup find nothing, with no error. Refused: empty, a `P<digits>` priority label, a comma, `blocked` or `waiting`, or a spelling a label-mapping value already uses.
  --eligibility-mode <labeled|all-issues>
    Which issues are candidates at all: `labeled` requires the eligibility label, `all-issues` considers every open issue. Switching to `all-issues` keeps the stored label, so switching back restores it.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  changed
    Whether this request actually moved anything. FALSE is a success, not a refusal: re-sending the state a repository is already in is an accepted no-op, and the audit trail records the intent either way.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_body (400) → exit 1
    The request body failed validation.
  not_found (404) → exit 1
    No such resource for this caller.
  rate_limited (429) → exit 4
    Too many requests for this credential or source.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  PATCH /api/v1/repos/{id}
  operation customer.repos.update

descant repo settings-audit

Show who paused or resumed a repository, and when. Drives GET /api/v1/repos/{id}/settings-audit.

descant repo settings-audit <id>

  Show who paused or resumed a repository, and when.

  Read one repository's recorded settings intents, newest first.

  Who paused or resumed this repository, when, and whether the flip changed anything — readable
  by a key-holding program without a browser session. System pauses appear alongside operator
  ones, told apart by `actor`. A FIXED WINDOW of the 50 newest rows with no paging, so an empty
  `entries` is NOT proof the repository was never paused: it means nothing was recorded within
  that window. This is not a `paused` state oracle; the current pause reason is on the
  poller-status read. A malformed `id` answers `not_found`, exactly as an unknown or unreadable
  one does — never a validation error, so a caller cannot use the status to learn which ids are
  well-formed.

Arguments:
  <id>

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  entries[].actor
    Who recorded the intent, as an OPAQUE string. The write side owns this vocabulary and a reader must not branch on its prefix shape. System pauses appear here alongside operator ones — the scheduler's evidence-driven auto-pause appends its own row actored `service:scheduler-autopause` — and `actor` is what tells them apart.
  entries[].action
    The intent recorded, as the write side names it.
  entries[].changed
    Whether this intent was a REAL change rather than a no-op re-assertion of the state already held.
  entries[].detail
    The row's detail, crossing the wire VERBATIM. ALWAYS PRESENT: a pause or resume carries its field transition; a `detach` records `null`, and that explicit null IS the persisted state — never an absence, never collapsed or dropped. The values are unbounded on purpose: the contract does not own this shape.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  not_found (404) → exit 1
    No such resource for this caller.
  rate_limited (429) → exit 4
    Too many requests for this credential or source.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  GET /api/v1/repos/{id}/settings-audit
  operation customer.repos.settings-audit

descant repo poller-status

Show why a repository is or is not being polled. Drives GET /api/v1/repos/{id}/poller-status.

descant repo poller-status <id>

  Show why a repository is or is not being polled.

  Read why this repository is or is not being polled.

  The read that answers 'why is nothing happening'. `status` is a discriminated union over
  SIXTEEN causes, because 'not polling' has four different remedies and a boolean loses the
  difference: a repository HELD on its candidates, a worker that never consulted the ticket
  source, a billing pause, and a page already filed by earlier runs all look the same through
  `paused: true`. Branch on `status.kind`. `repo.paused` and `billing.paused` are the raw flags
  beneath that reading, and `lastPoll` is the stored tick beneath both. A malformed `id` answers
  `not_found`, exactly as an unknown or unreadable one does — never a validation error, so a
  caller cannot use the status to learn which ids are well-formed.

Arguments:
  <id>

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  lastPoll
    The last tick's stored fields, beneath the classification. Use `status` unless you need what was persisted.
  lastPoll.outcome
    The RAW stored outcome, unclassified. `status` is the reading of it.
  lastPoll.errorClass
    The raw stored error class, unnarrowed — unlike `status.errorClass`.
  billing.reason
    Why billing paused this tenant, from the closed set the handler narrows to. `null` is 'paused, reason not established' — absent or unrecognised — never 'not paused'.
  billing.degraded
    Whether the billing read itself was degraded — so `paused` and `reason` may be stale rather than wrong.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  not_found (404) → exit 1
    No such resource for this caller.
  rate_limited (429) → exit 4
    Too many requests for this credential or source.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  GET /api/v1/repos/{id}/poller-status
  operation customer.repos.poller-status

descant repo config

Show what a repository is configured to pick, and in what order. Drives GET /api/v1/repos/{id}/config.

descant repo config <id>

  Show what a repository is configured to pick, and in what order.

  Read what this repository is configured to pick, and in what order.

  The rule behind the other two repo reads: which issues are candidates, which label makes one
  eligible, whether assigned issues are excluded, and the order the picker walks. Values are
  EFFECTIVE — the defaults are composed in when no row is stored, and no flag distinguishes the
  two, because they behave identically. `effectiveOrderQueries` is derived and must never be
  written back. A malformed `id` answers `not_found`, exactly as an unknown or unreadable one
  does.

Arguments:
  <id>

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  config.eligibilityLabel
    The label an issue must carry to be eligible in `labeled` mode. Not consulted in `all-issues` mode.
  config.eligibilityMode
    Which issues are candidates at all. `labeled` requires the eligibility label; `all-issues` considers every open issue, and is why the diagnostics read reports no label-bucketed counts in that mode.
  config.excludeAssigned
    Whether assigned issues are excluded. NOT durable operator policy: this is an AUTO-MANAGED recovery knob the reconciler can flip and restore without any operator write, so `false` must not be read as 'someone chose this'.
  config.orderStrategy
    The direction the picker walks a tier: oldest issue first, or newest first.
  config.orderQueries
    The persisted walk, verbatim, or `null` meaning the mode's default walk. `null` is the common case and is not an error.
  config.effectiveOrderQueries
    DERIVED, NOT STORED — what the picker actually walks: `orderQueries` when set, otherwise the mode-aware default (the P0-P3 tiers plus the unlabeled final tier). Never write this back as configuration; `orderQueries` is the field that is set.
  config.autoMergeEnabled
    Whether a run's pull request is merged automatically once it is green.
  config.commitPlanDocument
    Whether runs also commit the plan document into the repository.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  not_found (404) → exit 1
    No such resource for this caller.
  rate_limited (429) → exit 4
    Too many requests for this credential or source.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  GET /api/v1/repos/{id}/config
  operation customer.repos.config

descant repo issues

List a repository's open issues, as the enqueue picker sees them. Drives GET /api/v1/repos/{id}/issues.

descant repo issues <id>

  List a repository's open issues, as the enqueue picker sees them.

  List a repository's open issues, as the enqueue picker sees them.

  The issues a run can be started for, read through this deployment's author App on the
  repository the path names — the same listing the console's picker renders. `configured: false`
  is a SUCCESS and means this deployment cannot enumerate (no author-App credentials, or this
  repository's author installation never landed); enqueue still works with an issue number you
  supply, so branch on the field rather than on the status. A malformed id, an unknown id and
  another tenant's id are one indistinguishable `not_found`, answered with no upstream call.
  Upstream failures are classified rather than proxied: `upstream_rate_limited` carries its
  interval and is worth retrying, `upstream_auth` is this deployment's credential and not yours
  to fix.

Arguments:
  <id>
    The `tenant_repos` row, as `customer.repos.list` returns it.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_path (400) → exit 1
    A path parameter failed validation.
  not_found (404) → exit 1
    No such resource for this caller.
  upstream_rate_limited (503) → exit 4
    An upstream service is rate limiting us; retry after the stated interval.
  upstream_auth (502) → exit 7
    An upstream service rejected this deployment's credential.
  upstream_timeout (503) → exit 7
    An upstream service did not answer in time.
  upstream_unavailable (503) → exit 7
    A service this operation depends on did not answer.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  GET /api/v1/repos/{id}/issues
  operation customer.repos.issues.list

descant repo labels

List a repository's label names for the eligibility-label setting. Drives GET /api/v1/repos/{id}/labels.

descant repo labels <id>

  List a repository's label names for the eligibility-label setting.

  List a repository's label names, as the eligibility-label setting reads them.

  The label NAMES this repository carries — what `customer.repos.update` stores as the
  eligibility label, and what the poller matches an issue against, so this is the read that
  makes that setting writable without guessing. `configured: false` is a SUCCESS on the same
  terms as the issue picker: setting a label by name still works when enumeration is
  unavailable. The `not_found` and upstream-classification rules are that operation's; see it.

Arguments:
  <id>
    The `tenant_repos` row, as `customer.repos.list` returns it.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_path (400) → exit 1
    A path parameter failed validation.
  not_found (404) → exit 1
    No such resource for this caller.
  upstream_rate_limited (503) → exit 4
    An upstream service is rate limiting us; retry after the stated interval.
  upstream_auth (502) → exit 7
    An upstream service rejected this deployment's credential.
  upstream_timeout (503) → exit 7
    An upstream service did not answer in time.
  upstream_unavailable (503) → exit 7
    A service this operation depends on did not answer.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  GET /api/v1/repos/{id}/labels
  operation customer.repos.labels.list

descant repo diagnostics

Explain why a repository's issues would or would not be picked. Drives GET /api/v1/repos/{id}/poller-diagnostics.

descant repo diagnostics <id>

  Explain why a repository's issues would or would not be picked.

  Explain why this repository's issues would or would not be picked.

  A LIVE read: one provider call per request, never cached. Answers what the picker would do
  right now — how many issues carry the eligibility label, how many would be picked, and which
  buckets hold the rest. TWO of the three success shapes report NO counts (`configured: false`,
  and `mode: "all-issues"`), because neither can be measured in label buckets and a fabricated
  zero would read as "nothing is eligible" rather than "not measured". Branch on `configured`,
  then on `mode`. Counts are bounded by one page: check `truncated` before reading any of them
  as a repository total, and by the DEFAULT tier semantics: when `customOrdering` is true the
  picker walks the repository's own queries and the buckets describe the default walk rather
  than what it would pick. A malformed `id` answers `not_found`, exactly as an unknown or
  unreadable one does.

Arguments:
  <id>

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  rate_limited (429) → exit 4
    Too many requests for this credential or source.
  not_found (404) → exit 1
    No such resource for this caller.
  upstream_rate_limited (503) → exit 4
    An upstream service is rate limiting us; retry after the stated interval.
  upstream_auth (502) → exit 7
    An upstream service rejected this deployment's credential.
  upstream_timeout (503) → exit 7
    An upstream service did not answer in time.
  upstream_unavailable (503) → exit 7
    A service this operation depends on did not answer.

Wire:
  GET /api/v1/repos/{id}/poller-diagnostics
  operation customer.repos.poller-diagnostics

descant audit list

List the authenticated tenant's API calls, newest first. Drives GET /api/v1/audit.

descant audit list [flags]

  List the authenticated tenant's API calls, newest first.

  List this tenant's authenticated API calls, newest first.

  One row per authenticated call against this tenant, including the ones that did not succeed: a
  403 for a key outside its scope, and a 500 for a call that failed after it was admitted. A 429
  is never here — the only one this surface answers is the pre-authentication per-source bound,
  which refuses before a credential is read and so has no tenant to attribute a row to.
  `principal` says who: `api-key:<keyId>` when one of this tenant's credentials called,
  `user:<userId>` when a signed-in person exercised the same capability from the console — a key
  minted on the settings page is the same capability as a mint over this API, and both leave a
  row. A call with no tenant to attribute is not recorded, so an empty page does not mean
  nothing was tried. Paged by an opaque page token; `principal` narrows to one caller.
  `invalid_query` covers a malformed page token — one this server did not mint, or one minted by
  another paged read — which is refused rather than read as `start over`.

Flags:
  --limit <number>
    Page size. Defaults to 50, clamped to 200; the applied value is echoed.
  --page-token <string>
    Opaque, server-minted. Pass the previous response's `nextPageToken` verbatim; never construct one.
  --principal <string>
    Narrow the page to one credential's calls.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  items[].principal
    Who made the call, as an OPAQUE string the write side owns; do not branch on its prefix shape. A live row reads `api-key:<keyId>`, or `user:<userId>` for a signed-in person exercising the same capability from the console; a row whose subject has been erased reads a redaction marker instead.
  items[].operationId
    The operation the route serves, or `null` for a route the contract has not declared yet.
  items[].method
    The HTTP method, upper case.
  items[].path
    The request path without its query string. A segment may be a resource id, never prose.
  items[].scope
    The scope the operation required, `<family>:<read|write>`.
  items[].status
    The HTTP status answered.
  items[].outcome
    Derived from `status`: below 400 ok, 4xx refused, 5xx failed.
  items[].requestId
    The `X-Request-Id` the response carried.
  nextPageToken
    `null` when this is the last page.
  limit
    The page size actually applied, after clamping.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_query (400) → exit 1
    A query parameter failed validation.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  GET /api/v1/audit
  operation customer.audit.list

descant key list

List the authenticated tenant's API keys. Drives GET /api/v1/keys.

descant key list [flags]

  List the authenticated tenant's API keys.

  List this tenant's API keys, newest first.

  Every key the tenant has minted, active and revoked, newest first — a revoke is a soft one, so
  a revoked key stays in this list with `revokedAt` set. Never the secret: only the display
  prefix. Paged by an opaque page token, because a key holding `keys:write` mints without a
  person and nothing bounds the row count; follow `nextPageToken` until it is `null`.
  `invalid_query` covers a malformed page token — one this server did not mint, or one minted by
  another paged read — which is refused rather than read as `start over`.

Flags:
  --limit <number>
    Page size. Defaults to 50, clamped to 200; the applied value is echoed.
  --page-token <string>
    Opaque, server-minted. Pass the previous response's `nextPageToken` verbatim; never construct one.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  items[].keyPrefix
    `dsc_` plus the first eight characters of the secret: how a key is named after it is minted.
  items[].scopes[]
    `<family>:<read|write>`, from the closed set the contract publishes.
  items[].expiresAt
    `null` never expires.
  items[].revokedAt
    `null` while the key is active.
  nextPageToken
    `null` when this is the last page.
  limit
    The page size actually applied, after clamping.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_query (400) → exit 1
    A query parameter failed validation.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  GET /api/v1/keys
  operation customer.keys.list

descant key create

Mint a key holding a subset of this key's scopes. Drives POST /api/v1/keys.

descant key create [flags]

  Mint a key holding a subset of this key's scopes.

  Mint a key holding a subset of this key's scopes.

  Mints a key for the same tenant, attributed to the same creating account as the minting key.
  The requested scopes must be a subset of the minting key's (a key cannot widen itself);
  `conflict` names the scope that is not held. A minting key with no creating account on record
  (a legacy key whose creator was erased) is refused with `conflict` rather than minting a child
  nothing is attributed to. The secret appears in this response and nowhere else.

Flags:
  --label <string> (required)
  --scopes <string> (required, repeatable)
    The scopes the new key holds. Must be a subset of the minting key's own — a key cannot widen itself.
  --expires-at <string>
    When the key stops authenticating. Omit for a key that does not expire. Must be in the future.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  key.keyPrefix
    `dsc_` plus the first eight characters of the secret: how a key is named after it is minted.
  key.scopes[]
    `<family>:<read|write>`, from the closed set the contract publishes.
  key.expiresAt
    `null` never expires.
  key.revokedAt
    `null` while the key is active.
  key.rawKey
    The secret, shown exactly once. It is stored hashed and cannot be recovered; a caller that loses it mints a new key.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_body (400) → exit 1
    The request body failed validation.
  conflict (409) → exit 1
    The resource is not in a state that admits this.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  POST /api/v1/keys
  operation customer.keys.create

descant key revoke

Revoke one of the authenticated tenant's API keys. Drives DELETE /api/v1/keys/{keyId}.

descant key revoke <keyId>

  Revoke one of the authenticated tenant's API keys.

  Revoke one of this tenant's keys.

  Soft-revokes: the row stays so the list can show it, and the key stops authenticating at once.
  A repeat reports `already-revoked`. A key id that belongs to another tenant, or to nothing, is
  one indistinguishable `not_found`. A key may revoke ITSELF; the call that does so is its last.

Arguments:
  <keyId>

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  key.outcome
    `already-revoked` on a repeat: the key was not active, so nothing changed.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_path (400) → exit 1
    A path parameter failed validation.
  not_found (404) → exit 1
    No such resource for this caller.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  DELETE /api/v1/keys/{keyId}
  operation customer.keys.revoke

descant key rotate

Replace a key with a successor; the old one lives on for a grace window. Drives POST /api/v1/keys/{keyId}/rotate.

descant key rotate <keyId> [flags]

  Replace a key with a successor; the old one lives on for a grace window.

  Replace a key with a successor; the old one lives on for a grace window.

  Mints a successor holding the target's label and scopes, attributed to the minting key's
  creating account, and sets the target to stop authenticating at the end of the grace window
  (`graceSeconds`: default one hour, `0` at once) or at its own expiry, whichever is earlier —
  an expiry is never extended. A rotation is a mint, so the minting key must hold every scope
  the target does; `conflict` names the scope that is not held. A target already revoked is
  `conflict` too: it has nothing to rotate. So is a target holding a scope the server answering
  does not recognise — rotating it would drop that scope from the successor permanently, so it
  is refused rather than silently narrowed; this can happen briefly while a deployment is
  rolling, and a retry clears it. A key may rotate itself. The successor's secret appears in
  this response and nowhere else.

Arguments:
  <keyId>

Flags:
  --grace-seconds <number>
    How long the retired key keeps authenticating once the successor exists, in seconds. `0` retires it at once; the default is one hour; at most one day.
  --expires-at <string>
    When the SUCCESSOR stops authenticating. Omit for a successor that does not expire; the target's own expiry is not inherited. Must be in the future.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  key.keyPrefix
    `dsc_` plus the first eight characters of the secret: how a key is named after it is minted.
  key.scopes[]
    `<family>:<read|write>`, from the closed set the contract publishes.
  key.expiresAt
    `null` never expires.
  key.revokedAt
    `null` while the key is active.
  key.rawKey
    The secret, shown exactly once. It is stored hashed and cannot be recovered; a caller that loses it mints a new key.
  retired.expiresAt
    When the retired key stops authenticating: the earlier of its own expiry and the end of the grace window.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_path (400) → exit 1
    A path parameter failed validation.
  invalid_body (400) → exit 1
    The request body failed validation.
  not_found (404) → exit 1
    No such resource for this caller.
  conflict (409) → exit 1
    The resource is not in a state that admits this.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  POST /api/v1/keys/{keyId}/rotate
  operation customer.keys.rotate

descant invite list

List this account's invites, with the budget left. Drives GET /api/v1/invites.

descant invite list

  List this account's invites, with the budget left.

  List the key's own account's invites, with the budget and referral status.

  The invites belonging to the account that CREATED THIS KEY — not the tenant's current owner;
  see the family note, because which key you use decides whose budget you read. Metadata only:
  the invite code and its hash never appear here, only in the create response. A key with no
  creating account (minted before attribution, or one whose creator was erased) resolves to no
  account and answers `not_found`, the same answer as asking about somebody else's.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  invites
    METADATA ONLY — never the code or its hash. A list is re-readable, and a secret that can be re-read is not reveal-once.
  invites[].id
    The invite's id — what `customer.invites.revoke` takes.
  invites[].status
    Open by design, as the run state is: statuses are added as the invite lifecycle grows.
  invites[].redeemedAt
    `null` while the invite is unredeemed.
  referralStatus.creditsEarnedMicros
    Micros as a decimal STRING, not a number: the value is exact and JSON numbers are not, which is the convention this API already uses for exact ids.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  not_found (404) → exit 1
    No such resource for this caller.
  rate_limited (429) → exit 4
    Too many requests for this credential or source.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  GET /api/v1/invites
  operation customer.invites.list

descant invite create

Mint one invite and reveal its code once. Drives POST /api/v1/invites.

descant invite create

  Mint one invite and reveal its code once.

  Mint one invite, and reveal its code once.

  REVEAL-ONCE: the `code` and `path` exist in this response and nowhere else. Surface the link
  and do not persist the code — no read can return it, and a lost code means minting another.
  The body carries no fields, so an ABSENT or empty body is the natural call and is read as
  `{}`; a body with an unknown field is refused rather than ignored. Spending the account's last
  slot answers `invite_limit_reached` — a well-formed request that a rule refused, which is why
  it is not the malformed-body code. It spends the CREATING account's budget, which is the
  binding that keeps one co-owner's key from spending another's.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  invite.code
    THE CODE, and this is the ONLY response that carries it. Surface it, hand it over, and do not persist it — nothing can return it again.
  invite.path
    The redemption path the code belongs to.
  budget
    The budget AFTER this mint, so a caller need not re-list to know what is left.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_body (400) → exit 1
    The request body failed validation.
  invite_limit_reached (422) → exit 1
    The tenant's invite limit is reached.
  not_found (404) → exit 1
    No such resource for this caller.
  rate_limited (429) → exit 4
    Too many requests for this credential or source.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  POST /api/v1/invites
  operation customer.invites.create

descant invite revoke

Revoke one invite and reclaim its budget slot. Drives DELETE /api/v1/invites/{id}.

descant invite revoke <id>

  Revoke one invite and reclaim its budget slot.

  Revoke one invite, reclaiming its budget slot.

  Kills the link and returns the slot to the account's budget — the same primitive the
  dashboard's per-row Revoke uses, so the two surfaces cannot drift. A missing invite, an
  already-terminal one and ANOTHER ACCOUNT'S are one indistinguishable `not_found`: the
  underlying predicate is on the acting account, so nothing here discloses that an invite exists
  for somebody else. That also means a revoke is safe to repeat in the sense that matters — the
  second call answers `not_found` rather than doing something new — but it is not idempotent in
  the `Idempotency-Key` sense and does not claim to be.

Arguments:
  <id>
    The invite id, as the list read returns it.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  not_found (404) → exit 1
    No such resource for this caller.
  rate_limited (429) → exit 4
    Too many requests for this credential or source.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  DELETE /api/v1/invites/{id}
  operation customer.invites.revoke

descant repo grooming candidates

Read the relationships Descant would propose for a repository's backlog. Drives GET /api/v1/repos/{id}/grooming/candidates.

descant repo grooming candidates <id>

  Read the relationships Descant would propose for a repository's backlog.

  Read the relationships Descant would propose for this repository's backlog.

  PROPOSALS WITH THEIR EVIDENCE, not edges: nothing exists until
  `customer.repos.grooming.dispatch` lands it, and each candidate names the phrase in the
  tenant's own issue body that suggested it, because a relationship asserted with no reason
  cannot be reviewed. Served from the local copy of the backlog rather than a live walk — a
  proposal is a proposed WRITE against the copy the caller is looking at, so it must be derived
  from that same copy. `asOf`, `truncated` and `capped` say how current and how complete that
  copy was. WHEN THE COPY CANNOT SERVE, this answers `upstream_unavailable` and NEVER an empty
  list: an empty list is what a working detector returns over a backlog with no relationships,
  and the two must not be spelled the same.

Arguments:
  <id>
    The `tenant_repos` row, as `customer.repos.list` returns it.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  candidates
    PROPOSALS, not edges. Nothing exists until `dispatch` lands it.
  candidates[].id
    The candidate's own id, derived from its content — what a caller cites when acting on it.
  candidates[].from
    An issue, as the store names it — OPAQUE by design: the store compares references and never parses, reformats or resolves one, and this deployment spells a home-repository reference differently from a cross-repository one. So the only shape promised here is non-empty. A well-formed reference the document does not hold is not a malformed request: it answers `rejected` naming the reference, because only the document can know.
  candidates[].to
    An issue, as the store names it — OPAQUE by design: the store compares references and never parses, reformats or resolves one, and this deployment spells a home-repository reference differently from a cross-repository one. So the only shape promised here is non-empty. A well-formed reference the document does not hold is not a malformed request: it answers `rejected` naming the reference, because only the document can know.
  candidates[].evidence
    WHY the detector proposed this. `token` names the detector; `text` is the phrase from the tenant's own issue body that triggered it — free text, and the only free text this family publishes. It is the tenant's own backlog returned to that tenant, and without it a proposal is an assertion nobody can review.
  asOf
    When this copy was last known to have no unprocessed backlog. A candidate is only as current as the copy it was derived from.
  truncated
    The copy cut the backlog off, so the proposal set is over a partial view.
  capped
    The detector stopped at its own ceiling, so there may be proposals it never reached.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_path (400) → exit 1
    A path parameter failed validation.
  not_found (404) → exit 1
    No such resource for this caller.
  upstream_unavailable (503) → exit 7
    A service this operation depends on did not answer.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  GET /api/v1/repos/{id}/grooming/candidates
  operation customer.repos.grooming.candidates

descant repo grooming document

Read the relationship document a repository's issues encode. Drives GET /api/v1/repos/{id}/grooming/document.

descant repo grooming document <id>

  Read the relationship document a repository's issues encode.

  Read the relationship document this repository's issues currently encode.

  The issues and the edges together, as the workspace renders them — what IS, where the
  candidates read is what COULD be. `cycleGraphIncomplete` is non-null when the cycle check
  could not see the whole graph: do not read 'no cycles' from an incomplete one.
  `encodingRefused` names issues whose relationships could not be written back into their
  bodies, so their edges are readable here and NOT persisted upstream — a distinction that
  decides whether another reader will see them. Mirror-served and typed-refusal on the same
  terms as the candidates read; see it.

Arguments:
  <id>
    The `tenant_repos` row, as `customer.repos.list` returns it.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Response fields:
  document
    The whole document — issues and edges together. Never a patch or a slice; see the family header.
  document.issues[].ref
    An issue, as the store names it — OPAQUE by design: the store compares references and never parses, reformats or resolves one, and this deployment spells a home-repository reference differently from a cross-repository one. So the only shape promised here is non-empty. A well-formed reference the document does not hold is not a malformed request: it answers `rejected` naming the reference, because only the document can know.
  document.issues[].priority
    The RESOLVED tier. See `priorityDisagreement` when the issue's two carriers disagree.
  document.issues[].updatedAt
    THE CONFLICT TOKEN. Send it back in `dispatch`'s `expected` as `ref=updatedAt`; an issue that moved since you read it answers `conflict` rather than overwriting an edit you never saw.
  document.issues[].order
    `null` on a closed issue: it holds no position.
  document.issues[].order.rank
    `null` when the issue holds no rank.
  document.issues[].order.holds[].label
    The runner's one-word chip for a hold it imposes. `tracker` holds only, and not all of them — a cut-short readiness verdict and a unit over its member cap carry none.
  document.issues[].priorityDisagreement
    Non-null only when the issue's label and its frontmatter name different tiers. Recorded rather than derived — by the time `priority` exists the two have been collapsed into one number.
  document.issues[].priorityDisagreement.usedLabel
    The tier label AS WRITTEN ON THE ISSUE, never the configured spelling.
  document.issues[].priorityDisagreement.ignoredPriority
    The frontmatter tier that was set aside; the format resolves label-first.
  document.edges[].id
    Derived from the edge's content rather than assigned, so the same relationship has the same id wherever it is read.
  document.edges[].from
    An issue, as the store names it — OPAQUE by design: the store compares references and never parses, reformats or resolves one, and this deployment spells a home-repository reference differently from a cross-repository one. So the only shape promised here is non-empty. A well-formed reference the document does not hold is not a malformed request: it answers `rejected` naming the reference, because only the document can know.
  document.edges[].to
    An issue, as the store names it — OPAQUE by design: the store compares references and never parses, reformats or resolves one, and this deployment spells a home-repository reference differently from a cross-repository one. So the only shape promised here is non-empty. A well-formed reference the document does not hold is not a malformed request: it answers `rejected` naming the reference, because only the document can know.
  asOf
    When this copy was last known to have no unprocessed backlog.
  truncated
    The open enumeration was CUT at the row cap — surfaced rather than laundered into a shorter backlog.
  cycleGraphIncomplete
    Non-null when the cycle check could not see the whole graph, naming why — a caller must not read 'no cycles' from an incomplete one. A write that could close a cycle is refused while this is set.
  encodingRefused
    Issues whose relationships could not be encoded back into their bodies. Their edges are readable here and are NOT persisted upstream.
  host
    What the HOST is doing right now — the runs in flight and the cap over them. An object, not a name: an earlier cut declared this a string and the strict schema rejected every real answer.
  host.running[].key
    The document key of the issue being worked — `owner/repo#N`.
  host.running[].phase
    The runner's phase word, in the operator's vocabulary.
  host.truncated
    The open enumeration was cut, so the host's own counts are omitted rather than stated as totals.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_path (400) → exit 1
    A path parameter failed validation.
  not_found (404) → exit 1
    No such resource for this caller.
  upstream_unavailable (503) → exit 7
    A service this operation depends on did not answer.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  GET /api/v1/repos/{id}/grooming/document
  operation customer.repos.grooming.document

descant repo grooming dispatch

Land one relationship edit in a repository's issue bodies. Drives POST /api/v1/repos/{id}/grooming/dispatch.

descant repo grooming dispatch <id> [flags]

  Land one relationship edit in a repository's issue bodies.

  Land one relationship edit in the issue bodies, fenced on what the caller read.

  ONE mutation per call — create, delete, retype or flip — written into the GitHub ISSUE BODY
  through this repository's App installation. It does NOT seed a run and never reaches the
  runner, so no capacity answer applies. FENCED ON `expected`: issue ref to the `updatedAt` you
  hydrated with. An issue that moved since then answers `outcome: "conflict"` carrying the
  authoritative document, so a caller re-derives rather than overwriting an edit it never saw.
  WHEN AN ANSWER CARRIES A DOCUMENT IT IS THE WHOLE ONE, never a patch — `applied`, `unchanged`
  and `conflict` each carry it, and `unchanged` carries it precisely because 'nothing to do' is
  often said when your copy is stale and somebody else already created the edge. A `rejected`
  carries NO document: it carries a reason, and the `landed` flag if the write reached the issue
  before the failure. `rejected` means the write was refused upstream and nothing changed there
  — WITH ONE EXCEPTION that inverts it: a `rejected` carrying `landed: true` means the write DID
  land and only the local copy is behind, so drop the document and the `updatedAt` tokens you
  hold and re-read before your next edit. WHICH FIELDS GO WITH WHICH `op`, because the body is
  flat rather than a union: `create` takes `kind`, `from` and `to`; `delete` and `flip` take
  `edgeId`; `retype` takes `edgeId` and `nextKind`. `mutationId` is yours to mint and is
  required on every op. A field belonging to a different op is REFUSED rather than ignored — a
  body carrying both describes two edits and applying either is a guess — and a missing one
  answers `invalid_body` naming the field.

Arguments:
  <id>
    The `tenant_repos` row, as `customer.repos.list` returns it.

Flags:
  --op <create|delete|retype|flip> (required)
    WHICH edit. The fields that go with it are listed in this operation's description and checked by the route.
  --mutation-id <string> (required)
    YOURS TO MINT, required on every op, and echoed in this deployment's logs so one edit can be traced end to end. It is NOT replay protection: nothing here deduplicates on it, and resending the same id performs the edit again. `expected` is what makes a replay safe — a stale stamp answers `conflict` rather than writing twice.
  --kind <blocked-by|decomposed-from|duplicate-of|serialize-with>
    `create` only: the relationship to add. `together-with` is absent deliberately — the writer cannot splice it, so offering it would advertise a create that always fails.
  --from <string>
    `create` only.
  --to <string>
    `create` only.
  --edge-id <string>
    `delete`, `retype` and `flip`: the relationship being changed, as the document returns it.
  --next-kind <blocked-by|decomposed-from|duplicate-of|serialize-with>
    `retype` only: what the relationship becomes. Not `together-with`, for the reason `kind` gives — and a retype OUT of an existing `together-with` is refused by the writer too, which no enum here can say.
  --expected <string> (required, repeatable)
    THE CONFLICT FENCE, as `issueRef=updatedAt` entries — the stamps you hydrated with. An issue that moved upstream since then answers `conflict` with the authoritative document rather than overwriting an edit you never saw. Entries rather than a map because the flag generator has no spelling for a map; the route splits them on the first `=`.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_path (400) → exit 1
    A path parameter failed validation.
  invalid_body (400) → exit 1
    The request body failed validation.
  not_found (404) → exit 1
    No such resource for this caller.
  upstream_auth (502) → exit 7
    An upstream service rejected this deployment's credential.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  POST /api/v1/repos/{id}/grooming/dispatch
  operation customer.repos.grooming.dispatch

descant repo order-preview

Preview the whole ordering walk under an unsaved draft. Drives POST /api/v1/repos/{id}/order-preview.

descant repo order-preview <id> [flags]

  Preview the whole ordering walk under an unsaved draft.

  Preview the whole ordering walk under an unsaved draft.

  What this repository's backlog WOULD be worked in, under a draft you have not saved — the
  point being to see the draft's effect before committing to it. THE BODY IS A PARTIAL OVERLAY
  and omission is meaningful: leaving a field out keeps the SAVED value, and `orderQueries`
  additionally distinguishes `null` (clear them) from absent (keep them). Sending `null` when
  you meant 'no opinion' previews a backlog with no queries. `partial` non-null means the walk
  did not finish and `rows` is a prefix rather than the answer. `configured: false` is a success
  meaning this deployment cannot reach the provider — the settings stay writable. It WRITES
  NOTHING: a preview computes and discards.

Arguments:
  <id>
    The `tenant_repos` row, as `customer.repos.list` returns it.

Flags:
  --eligibility-mode <labeled|all-issues>
    OMIT to keep the saved mode. `labeled` requires the eligibility label; `all-issues` considers every open issue. The same closed set `customer.repos.config` takes.
  --eligibility-label <string>
    OMIT to keep the saved label. Validated against the stored label exactly as the save path validates it.
  --order-strategy <oldest-first|newest-first>
    OMIT to keep the saved strategy. The direction the picker walks a tier — the same closed set `customer.repos.config` takes.
  --exclude-assigned / --no-exclude-assigned
    OMIT to keep the saved value. `false` MEANS false — no presence marker is needed here, because unlike an HTML checkbox a JSON key that is present is present.
  --label-map <string|null> (repeatable)
    The label map as `KEY=label` entries, split on the FIRST `=` so a label containing one survives. Keys: `P0`, `P1`, `P2`, `P3`, `type:bug`, `type:feature`, `type:chore`. OMIT to keep the saved map. Send `null` — or an empty array — to preview with NO mapping at all: `null` is the spelling that survives the generated CLI, where a repeatable flag given zero times is indistinguishable from omitting the field and so cannot express an empty list. An entry with an empty value (`P1=`) leaves THAT key unmapped, as the settings form reads a blank control. An unknown or repeated key is `invalid_body` naming the entry.
  --order-queries <string|null> (repeatable)
    THREE STATES, and the one field where absent and `null` differ. OMIT to keep the saved queries; send `null` to CLEAR them and preview with none; send an array to replace them. A caller that sends `null` meaning "nothing to say about queries" will preview a backlog with no queries at all.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_path (400) → exit 1
    A path parameter failed validation.
  invalid_body (400) → exit 1
    The request body failed validation.
  not_found (404) → exit 1
    No such resource for this caller.
  upstream_auth (502) → exit 7
    An upstream service rejected this deployment's credential.
  upstream_timeout (503) → exit 7
    An upstream service did not answer in time.
  upstream_unavailable (503) → exit 7
    A service this operation depends on did not answer.
  upstream_rate_limited (503) → exit 4
    An upstream service is rate limiting us; retry after the stated interval.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.
  conflict (409) → exit 1
    The resource is not in a state that admits this.

Wire:
  POST /api/v1/repos/{id}/order-preview
  operation customer.repos.order-preview

descant repo ordering-preview

Preview what one ordering query matches. Drives POST /api/v1/repos/{id}/ordering-preview.

descant repo ordering-preview <id> [flags]

  Preview what one ordering query matches.

  Preview what one ordering query matches.

  The per-query half of the pair: what a single search actually matches, as a count plus a
  sample. An author checking a query they are writing wants this; an author checking the RESULT
  wants `customer.repos.order-preview`. `executedQuery` is the query as actually sent after the
  product's own scoping was folded in — read it when a result surprises you, because the
  difference is usually there. `items` is a sample and `totalCount` is the count; do not read
  the length of one as the other. It WRITES NOTHING.

Arguments:
  <id>
    The `tenant_repos` row, as `customer.repos.list` returns it.

Flags:
  --query <string> (required)
    The one search to preview, in the provider's own query syntax.

Global flags:
  --json / --no-json
    Print the API's response unmodified. The default when stdout is not a terminal.
  --help / --no-help
    Show this command's arguments, flags and response fields.
  --dry-run / --no-dry-run
    Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  --spec / --no-spec
    On `descant version`: report the contract version this binary was generated from, and every command it reaches.
  --base-url <string>
    The API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.

Credential:
  Read from the credential `descant login` stored, else $DESCANT_API_KEY.
  A tenant API key (`dsc_…`). The tenant is taken from the key, never from the request.
  `descant whoami` names which of the two is in use, and never prints it.

Errors:
  invalid_path (400) → exit 1
    A path parameter failed validation.
  invalid_body (400) → exit 1
    The request body failed validation.
  not_found (404) → exit 1
    No such resource for this caller.
  upstream_auth (502) → exit 7
    An upstream service rejected this deployment's credential.
  upstream_timeout (503) → exit 7
    An upstream service did not answer in time.
  upstream_unavailable (503) → exit 7
    A service this operation depends on did not answer.
  upstream_rate_limited (503) → exit 4
    An upstream service is rate limiting us; retry after the stated interval.
  internal_error (500) → exit 7
    The request failed for a reason the caller cannot fix.

Wire:
  POST /api/v1/repos/{id}/ordering-preview
  operation customer.repos.ordering-preview

Scripting and agents

The global flags, as the binary describes them. What a program should know beyond them — machine output, dry runs, what a retry costs, the exit codes and every refusal a command can meet — is on the page for agents and scripts.

  • --jsonPrint the API's response unmodified. The default when stdout is not a terminal.
  • --helpShow this command's arguments, flags and response fields.
  • --dry-runPrint the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` take no flags at all, and `version` sends nothing. A credential must still resolve, because the request is built in full.
  • --specOn `descant version`: report the contract version this binary was generated from, and every command it reaches.
  • --base-urlThe API origin to call. Defaults to $DESCANT_API_BASE_URL, then https://app.descant.run — the origin the deployment answers on, not the marketing site at the apex.