sling CLI
sling is StarSling's agent-first CLI for GitHub Actions. It answers
questions about live CI data — one run, one job, one attempt, or an
account's runner-minutes — from StarSling's control plane, with structured
JSON on stdout and a stable exit-code table. In this release it is
read-only: it reads and analyzes, and there is no sling command that
changes anything in GitHub.
This skill's job is picking the right tool per request. Live question
about a run, a job, or spend → sling. Anything that changes CI state →
gh. A repo-wide audit of workflow files → hand off to the audit skill
that owns it and stop. Do not invent a sling subcommand for something
sling does not do; the command
surface is the complete list.
Every fact in this skill's command reference was read off sling itself
(v0.1.8) rather than from documentation — see
references/command-reference.md for the
verified per-command JSON shapes.
Preflight: is sling installed and authenticated?
Do this once per session, before the first sling command.
Is it on PATH? command -v sling. If it is missing, sling is
installed by a one-line installer published on the installation page at
https://docs.starsling.dev/sling-cli/installation — point the user at
that page and let them run it, rather than pasting an installer command
from memory. It supports Apple Silicon macOS and x64 glibc Linux
only; there is no Windows build, so on Windows say so plainly and use
the gh-only path in step 6.
Is the environment healthy? sling doctor --agent. It emits
{"checks": [{"key", "ok", "detail", ...}]} on stdout and exits 10
when a real check fails (0 when healthy). The checks name the
problem. Seven of the eight are actionable — token, control_plane,
clock_skew, git_remote, patch_tooling, version, org — so act on
the one that is ok: false instead of guessing (the eighth, agent_skill,
is advisory only; see below) — and when SEVERAL are ok: false, act on
control_plane first: an unreachable control plane fails token and org
as dependents, and the token check still attaches fix_command: "sling login" — a wrong lead during an outage, since the device flow cannot reach
the control plane either. Network first, never login. "skipped": true can
also mean "not checked" for exactly this reason — read detail, not just
the flag. A version check with warn: true is an upgrade
notice, not a failure — any fix_command it carries is the installer
one-liner, a shell pipeline, so surface it to the user rather than running
it yourself. version reads ok: false, skipped: true when it could not
compare at all (installer URL unreachable, served version unreadable) —
that is "not checked", not "out of date".
The agent_skill row is likewise advisory and can never make doctor
unhealthy: a check counts as failing only when it is ok: false AND not
skipped, and this row's only ok: false shape is also skipped: true.
It reports whether this skill is installed for a coding agent on the
machine, and it can read "not installed" while you are reading the skill,
because it only looks in a fixed set of directories. So report the row with
its detail and never run its fix_command yourself — installing is the
user's call
(see references/command-reference.md).
Not authenticated (doctor's token check fails, or a command exits
4 whose stderr does not name an org): ask the user to run sling login in their own terminal, then wait for them to say it is done.
Do not run it yourself, and never pass --agent to it.
Read the stderr before acting on exit 4. You don't have access to org "<name>" is a missing app installation, not a stale credential, and
it belongs to step 5 rather than here. It ends Run \sling login``
anyway, because a generic decorator appends that to every auth-class
error — so the code and the advice both point the wrong way. Signing in
again succeeds and changes nothing, which reads to the user as a broken
login rather than as the missing installation it is.
Signing in needs a person at a browser: sling login prints a device
code, opens a GitHub approval page, and blocks until someone approves it.
Launched as a subprocess that is unworkable in both directions — the
output arrives only once the command has already finished or timed out,
so the code is never visible while it can still be used, and --agent
would refuse the prompt outright with exit 2. There is no automated
path here, and pretending otherwise just hangs the session.
The credential is saved per user at ~/.config/sling/credentials, so
once they have signed in, an agent running as them is authenticated with
no further setup. Re-run sling doctor --agent to confirm, then carry
on.
Wrong org — you belong to several and have not picked one. doctor's
org check naming multiple slugs, an exit 2 about org ambiguity, or an
id that resolves to nothing but plausibly exists in another of your orgs:
sling org switch <slug>, or pass --org <slug> on the single command.
--org and --repo <owner/name> are accepted by most subcommands but
not by sling logs, which parses strictly and rejects both with exit
2 (Unknown flag "--org"). That is the command this routing table
reaches for most, so fix a wrong org for a logs read by switching the
default rather than by adding a flag to the call. --repo otherwise
defaults to the git remote of the current directory, and takes
owner/name — a bare repo name is rejected with exit 2.
doctor's org check also fails when you belong to no orgs, which
looks similar and is not this. There is no slug to switch to — that is
step 5.
StarSling gate — the app is not installed anywhere you can see.
sling reports CI that the StarSling GitHub App collected, so a valid
login sees nothing until the app is installed on a GitHub organization.
Two shapes, one cause, one answer:
- An org exists and you cannot see it — exit
4, You don't have access to org "<name>". Rule out a typo first. A slug that could
never exist returns byte-identically, so this message alone does not
establish that an org is real: sling org switch <slug> answers with
Unknown org "<slug>" — your orgs: …, which both settles it and prints
the list. Telling someone to install a GitHub App on an organization
they mistyped is worse than saying nothing. The URL carve-out below
does NOT cover the flag path: a third party's org passed via
--repo/--org lands here at exit 4 (verified: --repo vercel/skills), same split — your own orgs are listed by org switch;
a third party's routes to the gh read fallback of step 6. The message ends Run \sling login``, which
cannot work: the credential was never the problem. That suffix is
appended to every auth-class error by a generic decorator, so read it as
boilerplate rather than as advice about this case.
- You have no orgs at all — exit
2, You don't belong to any orgs yet., and doctor's org check saying the same. This is the ordinary
state of someone who installed sling and signed in before ever
installing the app; signing in succeeds and gives no hint that a step is
missing. Do not read exit 2 here as a bad flag, and do not send them
to sling org switch — there is nothing to switch to.
A sandboxed agent shell can also fail an auth probe for reasons of its
own, so retry once with host access before trusting a single failure.
In either shape, STOP and tell the user plainly — every answer this
skill gives comes from StarSling's record of their runs, so until the app
is installed there is no run data to read and only the gh read path
remains. Give them the path: install the app at
https://github.com/apps/starslingdev on the organization whose CI they
are asking about, or ask an owner to add them. Continue on the gh-only path ONLY if they say so, and name
what is unavailable when you do.
A pasted URL from someone else's org never reaches this gate. The
resolver searches only orgs you belong to, so a foreign Actions URL fails
with exit 3 (not-found), not exit 4 — verified live on a mastra-ai
run URL. The app-install remedy applies only to orgs the user belongs to;
for a third party, the right move is the gh read fallback of step 6.
Organizations only. The app installs on a personal repository but
StarSling does not pick up its jobs, so there is never data to report for
one — say that rather than reporting an empty result.
If sling cannot be installed or authenticated at all, fall back to
the gh read path for whatever is achievable (gh run view, gh run view --log-failed) and tell the user plainly that the richer
diagnosis — sling why's classification, sling time's phase
breakdown — is unavailable without sling. Do not silently degrade.
Run the gh gate below before promising that path: with neither CLI
available there is nothing left to fall back to, and saying so up front
beats discovering it one failed command at a time.
Always pass --agent
--agent is the machine-mode flag: JSON on stdout, no prompts, no colour.
Pass it on every data command and parse stdout as JSON. Do not also pass
--json — help pages render the pair together (--json, --agent), with
per-command wording.
Do not describe it as an alias for a longer flag list. The published docs
call it "exactly equivalent to --json --compact --no-input --no-color --yes", and on v0.1.8 --compact is not a flag this binary has at all:
passing it changes nothing because unknown flags are ignored, not because it
does anything. Output is pretty-printed, indented JSON either way, so feed
stdout to a real JSON parser and never to a line-oriented one that assumes
one object per line.
Never pass --agent to sling login. Signing in requires a human to
open a browser and approve a device code, and --agent carries
--no-input, which refuses the prompt rather than showing it — turning the
one command that needs a person into exit 2. sling org switch has the
same shape: under --agent its picker is refused, so give it an explicit
slug (sling org switch acme --agent).
Branch on the exit code, never on whether stdout looks empty. Human
chrome (spinners, summary rows, prompts) goes to stderr, and only when
stderr is a TTY. 6 and 10 are outcomes rather than errors: they emit
the full JSON payload on stdout with stderr empty — an unhealthy
sling doctor --agent exits 10 and still returns every check, which is
exactly what the preflight above asks you to read.
A non-zero exit does not mean stdout is empty, and a well-formed JSON
body does not mean success. sling logs on a job that stores no logs
exits 3 and still prints {"lines": [], "has_more": false, …};
sling resolve on an ambiguous id exits 2 and still prints
{"candidates": […]}. So read stdout as JSON and read the exit code,
and when the two disagree the exit code decides. Reporting that empty
lines array as "nothing in the log" is exactly the silent false negative
this skill exists to prevent — the reason is on stderr, which stays worth
reading even when stdout parsed cleanly.
Under --agent, every data command returns JSON, sling logs
included (sling exit-codes is help text: exit 0, empty stdout, prose on
stderr). A log-less job's payload carries its own machine discriminator —
"local": {"empty": {"kind": "absent"}} — alongside "lines": [].
JSON keys are snake_case on every command except whoami, which
returns camelCase (userId, githubLogin, expiresAt). Key a parser per
command, not on a local-vs-remote rule.
The traps in this contract are collected under Gotchas — worth
reading before the first parse.
The routing table
Decide with two questions, in order.
1. Does the request change anything, or only read? If the user wants to
change CI state — re-run a job, cancel a run, trigger a workflow, flip a
workflow on or off, pull down an artifact — sling cannot do it in this
release. Use gh directly; do not attempt a sling subcommand for these
under any name.
2. If it only reads: is it about one run, one job, or aggregate
cost/usage — or about the repo's CI configuration as a whole? One run,
one job, or spend attribution → sling. The repo's workflow YAML, its
best-practice adherence, its security posture, or a multi-run optimization
sweep → hand off (see Handoff) instead of
reading YAML yourself.
| User is asking about... |
Route to |
Command |
| Why a specific job failed |
sling |
sling why <run|job|attempt id|URL> |
| Where a run's wall-clock went (queue wait vs. provision vs. steps vs. teardown) |
sling |
sling time <run|job|attempt id|URL> |
| The same, as percentiles across a repo |
sling |
sling time --repo <owner/name> --window <n>d |
| Recent runs and their status |
sling |
sling runs list [--branch --status --conclusion --trigger --workflow-path --label --window --limit] |
| Detail on one run (its jobs, attempts, runner) |
sling |
sling runs show <run id|URL> [--wait] [--fail-fast] |
| Which jobs in a run failed, or jobs across a repo |
sling |
sling jobs list --run <id> / --repo <owner/name> |
| Which step in a job failed, and each step's duration |
sling |
sling jobs show <job id|URL> |
| Only the log lines that matter for a failure |
sling |
sling logs <run|job|attempt id|URL> [--grep <re>] [--since <dur>] [--limit <n>] |
| Turning a pasted Actions URL into a run/job/attempt id |
sling |
sling resolve <id|URL> [--target run|job|attempt] — a run id covering several jobs is ambiguous: exit 2, {"candidates": […]} on stdout. Pass one of the returned candidate ids (att_<jobid>.<n>) or --target run; --target job/attempt re-state the kind and return the same ambiguity |
| The biggest time or cost hotspots |
sling |
sling top [--by workflow|job|label|repo|branch] [--metric ...] |
| Runner-minutes and cost attributed per repo/workflow/label/day |
sling |
sling usage [--group-by <axis>] [--window <n>d] |
| What is owed this period, or past invoices |
sling |
sling bill, sling bill history |
| Which runner labels/sizes exist and what they cost |
sling |
sling labels list |
| Identity, org, credential, environment health |
sling |
sling whoami, sling doctor, sling org switch — and sling login, which the user runs, not you |
| Re-run a run or its failed jobs |
gh |
gh run rerun <run-id> [--failed] |
| Cancel an in-progress run |
gh |
gh run cancel <run-id> |
Trigger a workflow (workflow_dispatch) |
gh |
gh workflow run <workflow> [-f key=value] |
| Enable or disable a workflow |
gh |
gh workflow enable|disable <workflow> |
| Download an artifact |
gh |
gh run download <run-id> |
| Delete a run |
gh |
gh run delete <run-id> |
| Approve or reject a pending deployment |
gh |
gh api — see references/gh-fallback.md |
| The checks on a specific PR |
gh |
gh pr checks <pr> — then take the failing run/job id back into sling why / sling time |
| Secrets or repo/environment variables |
gh |
gh secret, gh variable |
| The contents of a workflow file |
neither |
Read the .yml directly — this is a file question, not a CLI action |
| A repo-wide grade, speed sweep, or security scan |
neither |
Hand off — see Handoff |
Ambiguous asks. With no action verb (re-run / cancel / trigger / enable
/ disable / download) and a specific run, job, or spend in view, default to
sling: it is the read path and costs nothing to try. If the ask is
read-only but sling has no matching subcommand — "what does this
workflow's on: trigger include" is YAML content, not run data — read the
file rather than forcing it through either CLI.
Compound asks — "tell me why this run failed, then re-run it" — are two
steps, in order: do the sling half first (sling why), report it,
then do the gh half (gh run rerun) as an explicit, separately announced
action — and run the gh gate (below) before promising that second half: a
working sling says nothing about whether gh is signed in. Never chain
into a state change without telling the user what changed.
Command reference
Full per-command flags and the verified JSON shape of every response:
references/command-reference.md. The
machine-readable list of every command that exists, which is what keeps this
skill from inventing one:
references/command-surface.json.
The short version, grouped the way sling --help groups them:
- Auth & setup —
login, logout, whoami, doctor, org switch.
- Inspect CI —
runs list, runs show, jobs list, jobs show,
logs, resolve.
- Diagnose —
why (classification, evidence, suggested actions, and a
ready-to-use prompt field), time (wall-clock split into
queue_wait, provision, image_pull, cache_restore,
checkout+patch, steps, cache_save, teardown).
- Cost & capacity —
usage, top, bill, bill history,
labels list.
Two habits worth keeping:
sling why on a RUN (id or URL) can answer for ONE selected job at exit
0 — verified on a cancelled three-job run, where it diagnosed the single
failed job and never mentioned the other two or the cancellation. Before
reporting that diagnosis as the cause of a run, confirm the run's job set
(jobs list --run <id>) whenever the run has several jobs or did not
conclude cleanly.
- Start from
why for a failure, not from logs. why is classified
server-side with no LLM in the loop, and its suggested_actions[] carry
the exact follow-up command (usually a sling logs --grep) instead of
making you guess a pattern. Its prompt field is written for an agent to
act on directly.
sling logs is a filter, not a dump. Reach for --grep and
--limit before pulling a whole transcript; has_more plus --cursor
pages the rest.
Exit codes
Branch on $?, read immediately after the command. The inference fails in
both directions: a successful result can be empty, and a failed command
can still print a full JSON body — logs exits 3 with {"lines": []},
resolve exits 2 with {"candidates": […]} — and so do logs, why
and time on a bare multi-job run id, with why/time putting nothing on
stderr at all. The invocation was valid: read the candidates off stdout and
pass one id (or --target run) rather than re-reading --help for a usage
error that is not there. Neither an empty stdout nor
a parseable one tells you what happened; only $? does.
| Code |
Meaning |
What to do |
0 |
Success |
— |
1 |
Unexpected internal error (a crash) — or a subcommand that does not exist |
Read stderr first. unknown command "<name>" for "sling" is a routing mistake, not a crash: the binary lists its real commands, so correct the name against that list rather than filing a bug — this is where the doctor fix_command gotcha below lands. stderr naming an environment variable (SLING_HOST must be a valid https URL …) is an environment fix, not a bug. Anything else: do not retry blindly, and surface the stderr text as a bug report |
2 |
Usage — bad flags, a prompt refused under --agent, org ambiguity, or no orgs at all |
Read the stderr first. You don't belong to any orgs yet. → step 5, the app is not installed; never a flag problem. Org-ambiguous → pass --org or sling org switch. Otherwise fix the invocation against sling <cmd> --help |
3 |
Not found — no such run/job/attempt in any org you can access, or a real job that stores no logs |
A pasted URL from a third-party org lands here, not on exit 4 — the resolver searches your orgs and reports not-found (No run/job/attempt matches that id in an org you can access). For an org the user does not belong to, do not suggest installing the app on it: fall back to the gh read path (public repos answer) and say the richer sling diagnosis is unavailable. Otherwise try sling resolve, confirm the org, then ask the user to confirm the id |
4 |
Auth — or no StarSling installation on that org, or a third party's org named via a flag. Read the stderr text before acting |
You don't have access to org "<name>" → the app is not installed there (or the user is not a member); point them at https://github.com/apps/starslingdev, do NOT retry login. Anything else → ask the user to run sling login themselves (a browser approval, never --agent), then retry once. If the retry exits 4 again, STOP and report what was verified — never ask for a second login |
5 |
Control-plane or API error (5xx or transport) |
Retry once after a short backoff (~2s); on a second failure fall back to the gh read equivalent and say sling was unreachable |
6 |
Partial — telemetry incomplete, result still emitted (time, why) |
Not an error. Use the result, and tell the user it is partial |
7 |
Rate limited (HTTP 429) |
Back off and retry once. On a second 429, stop: fall back to the gh read equivalent and say sling was rate limited |
10 |
Remote outcome failed — doctor unhealthy, or runs show --wait on a run that did not succeed |
Not a CLI error. This is the answer: report the unhealthy check, or the run's failure |
sling exit-codes on v0.1.8 prints only 0–5; codes 6, 7, and 10
are real and documented (and doctor --help names 10 itself), so treat
that help text as abridged. Any non-zero code not in this table: surface
stderr to the user rather than guessing a recovery.
Gotchas
These are the places where sling behaves differently from what its own
output, its --help, or its documentation implies. Each was found by
running the binary; none of them announce themselves at runtime. Everything
version-pinned here was verified against sling v0.1.8 and gh 2.93.0 —
re-verify on upgrade.
doctor's fix_command is advice, not a verified command. On 0.1.2
its version check recommended a command that did not exist — it emitted
"fix_command": "sling update", and sling update does not exist in any
release (0.1.8 still rejects it as unknown; the check now emits the
installer one-liner instead). Other checks emit templates with unfilled
placeholders: git_remote suggests git remote add origin <github-url>
verbatim. Never run a fix_command unchecked. The general lesson outlives
the original bug: a command name printed by a tool is not proof the tool
has it.
- Exit
4 does not always mean the credential is stale, and its message
misdirects when it does not. An org StarSling was never installed on
fails with You don't have access to org "<name>". Run \sling login`` —
the same shape as an expired session, ending in advice that cannot work.
Read the message before acting on the code: a named org means the app is
missing there, not that the login is.
- An empty listing is a coverage hole, never a finding.
sling runs list --repo <name> returns {"runs": []} and exit 0 both when the repo truly
had no runs in the window and when StarSling is not watching that repo at
all. The payload cannot tell them apart, so an empty result means this
check did NOT run — say so with the reason you cannot rule out, rather than
reporting "you have no CI runs" as a fact about their repo. A repo that does
not exist at all returns the same empty shape at exit 0 (verified live), so
when the answer matters, a cheap gh read — gh repo view, gh run list —
settles which of the three it is. The same three-way-ambiguous empty comes
back from jobs list --repo ({"jobs": []}), usage --repo
({"rows": []}) and time --repo ({"phases": []} — at exit 6, which
time --repo returns even on full data, so "partial" there carries no
signal; the empty phase list is the coverage hole).
- There is no client-side timeout. A black-holed control plane (packets
dropped, not refused) hangs
sling indefinitely — verified past 45s — with
no exit code and nothing on either stream. Wrap long calls in a timeout,
and when one fires report the timeout itself: a killed command produced no
result, and "no result" is never "no findings". A refused connection
returns exit 5 cleanly.
- Unknown flags are ignored, not rejected.
sling runs list --bogus
exits 0 and returns unfiltered rows. Exit 0 is therefore not evidence
that a filter applied — check the rows you got back before reporting a
filtered answer.
--help lists an abridged flag set. runs list --help omits
--trigger, --workflow-path, --label and the window flags, all of
which work. Absence from --help is not absence from the CLI; the fuller
list is in references/command-reference.md.
sling logs returns JSON under --agent — the same structured
envelope as every other data command. (An earlier revision claimed the
published docs disagreed; they do not — the docs document exactly this. The
real doc discrepancy is smaller: they call the envelope one-object-per-line,
and it is pretty-printed, indented JSON.)
- A zero phase in
sling time can mean "not measured". In v1,
image_pull and cold_start are bundled into provision and reported as
0 with a reason in meta.truncated. Read that array before telling a
user a phase took no time — a measurement gap reported as a finding is a
false finding.
- Exit
10 is an answer, not a failure. doctor unhealthy and runs show --wait on a failed run both exit 10. Report what it says. Exit 6
likewise carries a real result, flagged partial.
--agent silently includes --yes. Harmless while sling is
read-only, but do not treat a confirmation prompt as a safety net.
sling runs show gives no job_id — its jobs[] carry job_name
and attempts[] only. Use jobs list --run <id> or resolve when you
need job ids.
sling top can report a repo as (multiple) when one key spans
repos. Do not print that as a repo name without saying what it means.
sling bill has two totals. amount_due_usd is what is owed;
amount_usd is before credits. status: "open" means the period is still
accruing, so it is not a final number.
Handoff to the audit skills
sling reads live run data and has no ability to read workflow
configuration at all, so it structurally cannot audit, grade, or scan a
repo. When a conversation turns into "audit my whole CI setup", "grade my
CI", "why is CI slow generally", or "is this secure", stop and name the
right skill rather than looping sling over every run:
| The ask |
The skill |
| A configuration best-practices grade ("grade my CI", "CI score") |
ci-score |
| A measured speed / runner-minute audit across many runs ("why is CI slow") |
ci-speedup |
| A security scan of the workflow files ("is my CI secure") |
ci-secure |
Each reads a local checkout and runs its own catalog. Name the skill and let
the user run it; do not approximate one of these from run data.
Suggest, do not auto-chain. After sling usage or sling top shows a
clear cost outlier, or sling time shows one phase dominating repeatedly,
end the answer by suggesting ci-speedup for the across-many-runs root
cause and fix — sling shows that this run is slow or expensive;
ci-speedup shows why, with a fix. Never invoke another skill without the
user asking.
gh fallback
The mutating commands this skill routes to, with their real syntax:
references/gh-fallback.md.
gh gate, before the first gh call. If gh isn't installed or gh auth status fails, gh cannot do any of it — and gh authenticates separately
from sling, so a working sling says nothing about whether gh is signed
in. Sandboxed agent shells (Codex) can't reach keyring credentials: retry
with host access before trusting a failure, and never report auth "expired"
off a sandboxed probe. When host access is not
available to retry with, report gh's availability as UNVERIFIED rather
than expired or absent. Then STOP and tell the user plainly which half of
their request is unavailable — the reads still work through sling, the
state change does not. Give the path (https://cli.github.com; then gh auth login), and never report an action as done that never ran.
1---2name: sling3description: Diagnoses GitHub Actions failures and attributes CI time and cost using StarSling's `sling` CLI. Invoke this BEFORE using `gh` or fetching any logs whenever the user asks why a job or run failed, what made a run slow, about runner minutes or CI cost, asks to re-run or cancel a run, or pastes a GitHub Actions URL — `sling` returns server-classified failure causes and cost attribution that log-grepping misses; state changes route to `gh`. Not for repo-wide audits of workflow files (ci-score, ci-speedup, ci-secure), writing workflow YAML, or non-GitHub-Actions CI.4license: MIT5---67# sling CLI89`sling` is StarSling's agent-first CLI for GitHub Actions. It answers10questions about **live CI data** — one run, one job, one attempt, or an11account's runner-minutes — from StarSling's control plane, with structured12JSON on stdout and a stable exit-code table. In this release it is13**read-only**: it reads and analyzes, and there is no `sling` command that14changes anything in GitHub.1516**This skill's job is picking the right tool per request.** Live question17about a run, a job, or spend → `sling`. Anything that *changes* CI state →18`gh`. A repo-wide audit of workflow *files* → hand off to the audit skill19that owns it and stop. Do not invent a `sling` subcommand for something20`sling` does not do; [the command21surface](references/command-surface.json) is the complete list.2223Every fact in this skill's command reference was read off `sling` itself24(v0.1.8) rather than from documentation — see25[references/command-reference.md](references/command-reference.md) for the26verified per-command JSON shapes.2728## Preflight: is `sling` installed and authenticated?2930Do this once per session, before the first `sling` command.31321. **Is it on PATH?** `command -v sling`. If it is missing, `sling` is33 installed by a one-line installer published on the installation page at34 `https://docs.starsling.dev/sling-cli/installation` — point the user at35 that page and let them run it, rather than pasting an installer command36 from memory. It supports **Apple Silicon macOS and x64 glibc Linux37 only**; there is no Windows build, so on Windows say so plainly and use38 the `gh`-only path in step 6.39402. **Is the environment healthy?** `sling doctor --agent`. It emits41 `{"checks": [{"key", "ok", "detail", ...}]}` on stdout and **exits `10`42 when a real check fails** (`0` when healthy). The checks name the43 problem. Seven of the eight are actionable — `token`, `control_plane`,44 `clock_skew`, `git_remote`, `patch_tooling`, `version`, `org` — so act on45 the one that is `ok: false` instead of guessing (the eighth, `agent_skill`,46 is advisory only; see below) — and when SEVERAL are `ok: false`, act on47 `control_plane` first: an unreachable control plane fails `token` and `org`48 as dependents, and the `token` check still attaches `fix_command: "sling49 login"` — a wrong lead during an outage, since the device flow cannot reach50 the control plane either. Network first, never login. `"skipped": true` can51 also mean "not checked" for exactly this reason — read `detail`, not just52 the flag. A `version` check with `warn: true` is an upgrade53 notice, not a failure — any `fix_command` it carries is the installer54 one-liner, a shell pipeline, so surface it to the user rather than running55 it yourself. `version` reads `ok: false, skipped: true` when it could not56 compare at all (installer URL unreachable, served version unreadable) —57 that is "not checked", not "out of date".58 The `agent_skill` row is likewise advisory and can never make `doctor`59 unhealthy: a check counts as failing only when it is `ok: false` AND not60 `skipped`, and this row's only `ok: false` shape is also `skipped: true`.61 It reports whether this skill is installed for a coding agent on the62 machine, and it can read "not installed" while you are reading the skill,63 because it only looks in a fixed set of directories. So report the row with64 its detail and never run its `fix_command` yourself — installing is the65 user's call66 (see [references/command-reference.md](references/command-reference.md)).67683. **Not authenticated** (`doctor`'s `token` check fails, or a command exits69 `4` **whose stderr does not name an org**): **ask the user to run `sling70 login` in their own terminal**, then wait for them to say it is done.71 Do not run it yourself, and never pass `--agent` to it.7273 **Read the stderr before acting on exit `4`.** `You don't have access to74 org "<name>"` is a missing app installation, not a stale credential, and75 it belongs to step 5 rather than here. It ends `Run \`sling login\``76 anyway, because a generic decorator appends that to every auth-class77 error — so the code and the advice both point the wrong way. Signing in78 again succeeds and changes nothing, which reads to the user as a broken79 login rather than as the missing installation it is.8081 Signing in needs a person at a browser: `sling login` prints a device82 code, opens a GitHub approval page, and blocks until someone approves it.83 Launched as a subprocess that is unworkable in both directions — the84 output arrives only once the command has already finished or timed out,85 so the code is never visible while it can still be used, and `--agent`86 would refuse the prompt outright with exit `2`. There is no automated87 path here, and pretending otherwise just hangs the session.8889 The credential is saved per user at `~/.config/sling/credentials`, so90 once they have signed in, an agent running as them is authenticated with91 no further setup. Re-run `sling doctor --agent` to confirm, then carry92 on.93944. **Wrong org — you belong to several and have not picked one.** `doctor`'s95 `org` check naming multiple slugs, an exit `2` about org ambiguity, or an96 id that resolves to nothing but plausibly exists in another of your orgs:97 `sling org switch <slug>`, or pass `--org <slug>` on the single command.9899 `--org` and `--repo <owner/name>` are accepted by most subcommands but100 **not by `sling logs`**, which parses strictly and rejects both with exit101 `2` (`Unknown flag "--org"`). That is the command this routing table102 reaches for most, so fix a wrong org for a logs read by switching the103 default rather than by adding a flag to the call. `--repo` otherwise104 defaults to the git remote of the current directory, and takes105 `owner/name` — a bare repo name is rejected with exit `2`.106107 `doctor`'s `org` check also fails when you belong to **no** orgs, which108 looks similar and is not this. There is no slug to switch to — that is109 step 5.1101115. **StarSling gate — the app is not installed anywhere you can see.**112 `sling` reports CI that the **StarSling GitHub App** collected, so a valid113 login sees nothing until the app is installed on a GitHub organization.114 Two shapes, one cause, one answer:115116 - **An org exists and you cannot see it** — exit `4`, `You don't have117 access to org "<name>"`. **Rule out a typo first.** A slug that could118 never exist returns byte-identically, so this message alone does not119 establish that an org is real: `sling org switch <slug>` answers with120 `Unknown org "<slug>" — your orgs: …`, which both settles it and prints121 the list. Telling someone to install a GitHub App on an organization122 they mistyped is worse than saying nothing. The URL carve-out below123 does NOT cover the flag path: a third party's org passed via124 `--repo`/`--org` lands here at exit `4` (verified: `--repo125 vercel/skills`), same split — your own orgs are listed by `org switch`;126 a third party's routes to the `gh` read fallback of step 6. The message ends `Run \`sling login\``, which127 cannot work: the credential was never the problem. That suffix is128 appended to every auth-class error by a generic decorator, so read it as129 boilerplate rather than as advice about this case.130 - **You have no orgs at all** — exit `2`, `You don't belong to any orgs131 yet.`, and `doctor`'s `org` check saying the same. This is the ordinary132 state of someone who installed `sling` and signed in before ever133 installing the app; signing in succeeds and gives no hint that a step is134 missing. Do not read exit `2` here as a bad flag, and do not send them135 to `sling org switch` — there is nothing to switch to.136137 A sandboxed agent shell can also fail an auth probe for reasons of its138 own, so retry once with host access before trusting a single failure.139140 In either shape, **STOP and tell the user plainly** — every answer this141 skill gives comes from StarSling's record of their runs, so until the app142 is installed there is no run data to read and only the `gh` read path143 remains. Give them the path: install the app at144 `https://github.com/apps/starslingdev` on the organization whose CI they145 are asking about, or ask an owner to add them. Continue on the `gh`-only path ONLY if they say so, and name146 what is unavailable when you do.147148 **A pasted URL from someone else's org never reaches this gate.** The149 resolver searches only orgs you belong to, so a foreign Actions URL fails150 with exit `3` (not-found), not exit `4` — verified live on a `mastra-ai`151 run URL. The app-install remedy applies only to orgs the user belongs to;152 for a third party, the right move is the `gh` read fallback of step 6.153154 **Organizations only.** The app installs on a personal repository but155 StarSling does not pick up its jobs, so there is never data to report for156 one — say that rather than reporting an empty result.1571586. **If `sling` cannot be installed or authenticated at all**, fall back to159 the `gh` read path for whatever is achievable (`gh run view`, `gh run160 view --log-failed`) and **tell the user plainly** that the richer161 diagnosis — `sling why`'s classification, `sling time`'s phase162 breakdown — is unavailable without `sling`. Do not silently degrade.163164 Run the gh gate below before promising that path: with neither CLI165 available there is nothing left to fall back to, and saying so up front166 beats discovering it one failed command at a time.167168## Always pass `--agent`169170`--agent` is the machine-mode flag: JSON on stdout, no prompts, no colour.171Pass it on every **data** command and parse stdout as JSON. Do not also pass172`--json` — help pages render the pair together (`--json, --agent`), with173per-command wording.174175**Do not describe it as an alias for a longer flag list.** The published docs176call it "exactly equivalent to `--json --compact --no-input --no-color177--yes`", and on v0.1.8 `--compact` is not a flag this binary has at all:178passing it changes nothing because unknown flags are ignored, not because it179does anything. Output is pretty-printed, indented JSON either way, so feed180stdout to a real JSON parser and never to a line-oriented one that assumes181one object per line.182183**Never pass `--agent` to `sling login`.** Signing in requires a human to184open a browser and approve a device code, and `--agent` carries185`--no-input`, which refuses the prompt rather than showing it — turning the186one command that needs a person into exit `2`. `sling org switch` has the187same shape: under `--agent` its picker is refused, so give it an explicit188slug (`sling org switch acme --agent`).189190- **Branch on the exit code, never on whether stdout looks empty.** Human191 chrome (spinners, summary rows, prompts) goes to stderr, and only when192 stderr is a TTY. `6` and `10` are outcomes rather than errors: they emit193 the **full JSON payload on stdout** with stderr empty — an unhealthy194 `sling doctor --agent` exits `10` and still returns every check, which is195 exactly what the preflight above asks you to read.196197 **A non-zero exit does not mean stdout is empty, and a well-formed JSON198 body does not mean success.** `sling logs` on a job that stores no logs199 exits `3` and still prints `{"lines": [], "has_more": false, …}`;200 `sling resolve` on an ambiguous id exits `2` and still prints201 `{"candidates": […]}`. So read stdout as JSON *and* read the exit code,202 and when the two disagree the exit code decides. Reporting that empty203 `lines` array as "nothing in the log" is exactly the silent false negative204 this skill exists to prevent — the reason is on stderr, which stays worth205 reading even when stdout parsed cleanly.206- Under `--agent`, every **data** command returns JSON, `sling logs`207 included (`sling exit-codes` is help text: exit `0`, empty stdout, prose on208 stderr). A log-less job's payload carries its own machine discriminator —209 `"local": {"empty": {"kind": "absent"}}` — alongside `"lines": []`.210- JSON keys are `snake_case` on every command **except `whoami`**, which211 returns camelCase (`userId`, `githubLogin`, `expiresAt`). Key a parser per212 command, not on a local-vs-remote rule.213214The traps in this contract are collected under [Gotchas](#gotchas) — worth215reading before the first parse.216217## The routing table218219**Decide with two questions, in order.**220221**1. Does the request change anything, or only read?** If the user wants to222*change* CI state — re-run a job, cancel a run, trigger a workflow, flip a223workflow on or off, pull down an artifact — `sling` cannot do it in this224release. Use `gh` directly; do not attempt a `sling` subcommand for these225under any name.226227**2. If it only reads: is it about one run, one job, or aggregate228cost/usage — or about the repo's CI *configuration* as a whole?** One run,229one job, or spend attribution → `sling`. The repo's workflow YAML, its230best-practice adherence, its security posture, or a multi-run optimization231sweep → hand off (see [Handoff](#handoff-to-the-audit-skills)) instead of232reading YAML yourself.233234| User is asking about... | Route to | Command |235|---|---|---|236| Why a specific job failed | `sling` | `sling why <run\|job\|attempt id\|URL>` |237| Where a run's wall-clock went (queue wait vs. provision vs. steps vs. teardown) | `sling` | `sling time <run\|job\|attempt id\|URL>` |238| The same, as percentiles across a repo | `sling` | `sling time --repo <owner/name> --window <n>d` |239| Recent runs and their status | `sling` | `sling runs list [--branch --status --conclusion --trigger --workflow-path --label --window --limit]` |240| Detail on one run (its jobs, attempts, runner) | `sling` | `sling runs show <run id\|URL> [--wait] [--fail-fast]` |241| Which jobs in a run failed, or jobs across a repo | `sling` | `sling jobs list --run <id>` / `--repo <owner/name>` |242| Which step in a job failed, and each step's duration | `sling` | `sling jobs show <job id\|URL>` |243| Only the log lines that matter for a failure | `sling` | `sling logs <run\|job\|attempt id\|URL> [--grep <re>] [--since <dur>] [--limit <n>]` |244| Turning a pasted Actions URL into a run/job/attempt id | `sling` | `sling resolve <id\|URL> [--target run\|job\|attempt]` — a run id covering several jobs is **ambiguous**: exit `2`, `{"candidates": […]}` on stdout. Pass one of the returned candidate ids (`att_<jobid>.<n>`) or `--target run`; `--target job`/`attempt` re-state the kind and return the same ambiguity |245| The biggest time or cost hotspots | `sling` | `sling top [--by workflow\|job\|label\|repo\|branch] [--metric ...]` |246| Runner-minutes and cost attributed per repo/workflow/label/day | `sling` | `sling usage [--group-by <axis>] [--window <n>d]` |247| What is owed this period, or past invoices | `sling` | `sling bill`, `sling bill history` |248| Which runner labels/sizes exist and what they cost | `sling` | `sling labels list` |249| Identity, org, credential, environment health | `sling` | `sling whoami`, `sling doctor`, `sling org switch` — and `sling login`, which the **user** runs, not you |250| **Re-run a run or its failed jobs** | `gh` | `gh run rerun <run-id> [--failed]` |251| **Cancel an in-progress run** | `gh` | `gh run cancel <run-id>` |252| **Trigger a workflow (`workflow_dispatch`)** | `gh` | `gh workflow run <workflow> [-f key=value]` |253| **Enable or disable a workflow** | `gh` | `gh workflow enable\|disable <workflow>` |254| **Download an artifact** | `gh` | `gh run download <run-id>` |255| **Delete a run** | `gh` | `gh run delete <run-id>` |256| Approve or reject a pending deployment | `gh` | `gh api` — see [references/gh-fallback.md](references/gh-fallback.md) |257| The checks on a specific PR | `gh` | `gh pr checks <pr>` — then take the failing run/job id back into `sling why` / `sling time` |258| Secrets or repo/environment variables | `gh` | `gh secret`, `gh variable` |259| The contents of a workflow file | neither | Read the `.yml` directly — this is a file question, not a CLI action |260| A repo-wide grade, speed sweep, or security scan | neither | Hand off — see [Handoff](#handoff-to-the-audit-skills) |261262**Ambiguous asks.** With no action verb (re-run / cancel / trigger / enable263/ disable / download) and a specific run, job, or spend in view, default to264`sling`: it is the read path and costs nothing to try. If the ask is265read-only but `sling` has no matching subcommand — "what does this266workflow's `on:` trigger include" is YAML content, not run data — read the267file rather than forcing it through either CLI.268269**Compound asks** — "tell me why this run failed, then re-run it" — are two270steps, in order: do the `sling` half first (`sling why`), **report it**,271then do the `gh` half (`gh run rerun`) as an explicit, separately announced272action — and run the gh gate (below) before promising that second half: a273working `sling` says nothing about whether `gh` is signed in. Never chain274into a state change without telling the user what changed.275276## Command reference277278Full per-command flags and the verified JSON shape of every response:279[references/command-reference.md](references/command-reference.md). The280machine-readable list of every command that exists, which is what keeps this281skill from inventing one:282[references/command-surface.json](references/command-surface.json).283284The short version, grouped the way `sling --help` groups them:285286- **Auth & setup** — `login`, `logout`, `whoami`, `doctor`, `org switch`.287- **Inspect CI** — `runs list`, `runs show`, `jobs list`, `jobs show`,288 `logs`, `resolve`.289- **Diagnose** — `why` (classification, evidence, suggested actions, and a290 ready-to-use `prompt` field), `time` (wall-clock split into291 `queue_wait`, `provision`, `image_pull`, `cache_restore`,292 `checkout+patch`, `steps`, `cache_save`, `teardown`).293- **Cost & capacity** — `usage`, `top`, `bill`, `bill history`,294 `labels list`.295296Two habits worth keeping:297298- **`sling why` on a RUN (id or URL) can answer for ONE selected job at exit299 `0`** — verified on a cancelled three-job run, where it diagnosed the single300 failed job and never mentioned the other two or the cancellation. Before301 reporting that diagnosis as *the* cause of a run, confirm the run's job set302 (`jobs list --run <id>`) whenever the run has several jobs or did not303 conclude cleanly.304- **Start from `why` for a failure**, not from `logs`. `why` is classified305 server-side with no LLM in the loop, and its `suggested_actions[]` carry306 the exact follow-up command (usually a `sling logs --grep`) instead of307 making you guess a pattern. Its `prompt` field is written for an agent to308 act on directly.309- **`sling logs` is a filter, not a dump.** Reach for `--grep` and310 `--limit` before pulling a whole transcript; `has_more` plus `--cursor`311 pages the rest.312313## Exit codes314315Branch on `$?`, read immediately after the command. The inference fails in316both directions: a successful result can be empty, and a **failed command317can still print a full JSON body** — `logs` exits `3` with `{"lines": []}`,318`resolve` exits `2` with `{"candidates": […]}` — and so do `logs`, `why`319and `time` on a bare multi-job run id, with `why`/`time` putting **nothing on320stderr** at all. The invocation was valid: read the candidates off stdout and321pass one id (or `--target run`) rather than re-reading `--help` for a usage322error that is not there. Neither an empty stdout nor323a parseable one tells you what happened; only `$?` does.324325| Code | Meaning | What to do |326|---|---|---|327| `0` | Success | — |328| `1` | Unexpected internal error (a crash) — **or a subcommand that does not exist** | Read stderr first. `unknown command "<name>" for "sling"` is a routing mistake, not a crash: the binary lists its real commands, so correct the name against that list rather than filing a bug — this is where the `doctor` `fix_command` gotcha below lands. stderr naming an environment variable (`SLING_HOST must be a valid https URL …`) is an environment fix, not a bug. Anything else: do not retry blindly, and surface the stderr text as a bug report |329| `2` | Usage — bad flags, a prompt refused under `--agent`, org ambiguity, **or no orgs at all** | Read the stderr first. `You don't belong to any orgs yet.` → step 5, the app is not installed; never a flag problem. Org-ambiguous → pass `--org` or `sling org switch`. Otherwise fix the invocation against `sling <cmd> --help` |330| `3` | Not found — no such run/job/attempt in any org you can access, or a real job that stores no logs | **A pasted URL from a third-party org lands here, not on exit `4`** — the resolver searches your orgs and reports not-found (`No run/job/attempt matches that id in an org you can access`). For an org the user does not belong to, do not suggest installing the app on it: fall back to the `gh` read path (public repos answer) and say the richer `sling` diagnosis is unavailable. Otherwise try `sling resolve`, confirm the org, then ask the user to confirm the id |331| `4` | Auth — **or no StarSling installation on that org, or a third party's org named via a flag.** Read the stderr text before acting | `You don't have access to org "<name>"` → the app is not installed there (or the user is not a member); point them at `https://github.com/apps/starslingdev`, do NOT retry login. Anything else → ask the user to run `sling login` themselves (a browser approval, never `--agent`), then retry once. If the retry exits `4` again, STOP and report what was verified — never ask for a second login |332| `5` | Control-plane or API error (5xx or transport) | Retry once after a short backoff (~2s); on a second failure fall back to the `gh` read equivalent and **say** `sling` was unreachable |333| `6` | Partial — telemetry incomplete, result still emitted (`time`, `why`) | Not an error. Use the result, and tell the user it is partial |334| `7` | Rate limited (HTTP 429) | Back off and retry once. On a second `429`, stop: fall back to the `gh` read equivalent and say `sling` was rate limited |335| `10` | Remote outcome failed — `doctor` unhealthy, or `runs show --wait` on a run that did not succeed | **Not a CLI error.** This is the answer: report the unhealthy check, or the run's failure |336337`sling exit-codes` on v0.1.8 prints only `0`–`5`; codes `6`, `7`, and `10`338are real and documented (and `doctor --help` names `10` itself), so treat339that help text as abridged. Any non-zero code not in this table: surface340stderr to the user rather than guessing a recovery.341342## Gotchas343344These are the places where `sling` behaves differently from what its own345output, its `--help`, or its documentation implies. Each was found by346running the binary; none of them announce themselves at runtime. Everything347version-pinned here was verified against `sling` v0.1.8 and `gh` 2.93.0 —348re-verify on upgrade.349350- **`doctor`'s `fix_command` is advice, not a verified command.** On 0.1.2351 its `version` check recommended a command that did not exist — it emitted352 `"fix_command": "sling update"`, and `sling update` does not exist in any353 release (0.1.8 still rejects it as unknown; the check now emits the354 installer one-liner instead). Other checks emit templates with unfilled355 placeholders: `git_remote` suggests `git remote add origin <github-url>`356 verbatim. Never run a `fix_command` unchecked. The general lesson outlives357 the original bug: a command name printed by a tool is not proof the tool358 has it.359- **Exit `4` does not always mean the credential is stale, and its message360 misdirects when it does not.** An org StarSling was never installed on361 fails with `You don't have access to org "<name>". Run \`sling login\`` —362 the same shape as an expired session, ending in advice that cannot work.363 Read the message before acting on the code: a named org means the app is364 missing there, not that the login is.365- **An empty listing is a coverage hole, never a finding.** `sling runs list366 --repo <name>` returns `{"runs": []}` and exit `0` both when the repo truly367 had no runs in the window and when StarSling is not watching that repo at368 all. The payload cannot tell them apart, so an empty result means this369 check did NOT run — say so with the reason you cannot rule out, rather than370 reporting "you have no CI runs" as a fact about their repo. A repo that does371 not exist at all returns the same empty shape at exit `0` (verified live), so372 when the answer matters, a cheap `gh` read — `gh repo view`, `gh run list` —373 settles which of the three it is. The same three-way-ambiguous empty comes374 back from `jobs list --repo` (`{"jobs": []}`), `usage --repo`375 (`{"rows": []}`) and `time --repo` (`{"phases": []}` — at exit `6`, which376 `time --repo` returns even on full data, so "partial" there carries no377 signal; the empty phase list is the coverage hole).378- **There is no client-side timeout.** A black-holed control plane (packets379 dropped, not refused) hangs `sling` indefinitely — verified past 45s — with380 no exit code and nothing on either stream. Wrap long calls in a timeout,381 and when one fires report the timeout itself: a killed command produced no382 result, and "no result" is never "no findings". A *refused* connection383 returns exit `5` cleanly.384- **Unknown flags are ignored, not rejected.** `sling runs list --bogus`385 exits `0` and returns unfiltered rows. Exit `0` is therefore not evidence386 that a filter applied — check the rows you got back before reporting a387 filtered answer.388- **`--help` lists an abridged flag set.** `runs list --help` omits389 `--trigger`, `--workflow-path`, `--label` and the window flags, all of390 which work. Absence from `--help` is not absence from the CLI; the fuller391 list is in [references/command-reference.md](references/command-reference.md).392- **`sling logs` returns JSON under `--agent`** — the same structured393 envelope as every other data command. (An earlier revision claimed the394 published docs disagreed; they do not — the docs document exactly this. The395 real doc discrepancy is smaller: they call the envelope one-object-per-line,396 and it is pretty-printed, indented JSON.)397- **A zero phase in `sling time` can mean "not measured".** In v1,398 `image_pull` and `cold_start` are bundled into `provision` and reported as399 `0` with a reason in `meta.truncated`. Read that array before telling a400 user a phase took no time — a measurement gap reported as a finding is a401 false finding.402- **Exit `10` is an answer, not a failure.** `doctor` unhealthy and `runs403 show --wait` on a failed run both exit `10`. Report what it says. Exit `6`404 likewise carries a real result, flagged partial.405- **`--agent` silently includes `--yes`.** Harmless while `sling` is406 read-only, but do not treat a confirmation prompt as a safety net.407- **`sling runs show` gives no `job_id`** — its `jobs[]` carry `job_name`408 and `attempts[]` only. Use `jobs list --run <id>` or `resolve` when you409 need job ids.410- **`sling top` can report a repo as `(multiple)`** when one key spans411 repos. Do not print that as a repo name without saying what it means.412- **`sling bill` has two totals.** `amount_due_usd` is what is owed;413 `amount_usd` is before credits. `status: "open"` means the period is still414 accruing, so it is not a final number.415416## Handoff to the audit skills417418`sling` reads live run data and has **no ability to read workflow419configuration at all**, so it structurally cannot audit, grade, or scan a420repo. When a conversation turns into "audit my whole CI setup", "grade my421CI", "why is CI slow generally", or "is this secure", **stop and name the422right skill** rather than looping `sling` over every run:423424| The ask | The skill |425|---|---|426| A configuration best-practices grade ("grade my CI", "CI score") | `ci-score` |427| A measured speed / runner-minute audit across many runs ("why is CI slow") | `ci-speedup` |428| A security scan of the workflow files ("is my CI secure") | `ci-secure` |429430Each reads a local checkout and runs its own catalog. Name the skill and let431the user run it; do not approximate one of these from run data.432433**Suggest, do not auto-chain.** After `sling usage` or `sling top` shows a434clear cost outlier, or `sling time` shows one phase dominating repeatedly,435end the answer by suggesting `ci-speedup` for the across-many-runs root436cause and fix — `sling` shows *that* this run is slow or expensive;437`ci-speedup` shows *why*, with a fix. Never invoke another skill without the438user asking.439440## `gh` fallback441442The mutating commands this skill routes to, with their real syntax:443[references/gh-fallback.md](references/gh-fallback.md).444445**gh gate, before the first `gh` call.** If `gh` isn't installed or `gh auth446status` fails, `gh` cannot do any of it — and `gh` authenticates separately447from `sling`, so a working `sling` says nothing about whether `gh` is signed448in. Sandboxed agent shells (Codex) can't reach keyring credentials: retry449with host access before trusting a failure, and never report auth "expired"450off a sandboxed probe. When host access is not451 available to retry with, report `gh`'s availability as UNVERIFIED rather452 than expired or absent. Then STOP and tell the user plainly which half of453their request is unavailable — the reads still work through `sling`, the454state change does not. Give the path (https://cli.github.com; then `gh auth455login`), and never report an action as done that never ran.