# checklists-cli

Approve [checklists.io](https://checklists.io) human gates from the terminal.

When an agent runs a checklist and hits a step marked `gate: human`, the run stops and waits for
you. This is how you decide without leaving the terminal the agent is running in.

```bash
npm install -g checklists-cli
checklists auth login          # opens your browser, same sign-in as the website
checklists pending             # what is waiting on you
checklists show <run-id>       # the agent's case for the gate
checklists approve <run-id> --step cut-release
```

It also authors and runs them, so you never have to leave the terminal:

```bash
checklists runbooks pull <id> -o release.yaml   # the definition, as YAML
$EDITOR release.yaml
checklists runbooks push <id> -f release.yaml   # shows a diff, asks, applies
checklists runs start <id>
```

Ids can be shortened anywhere a full uuid is accepted — the first eight characters are enough, and
an ambiguous prefix is refused with the candidates listed rather than guessed at.

## Why a separate command instead of just telling the agent "yes"

If you tell the agent yes and the agent records the approval, the approval is the *agent's* API
call. Nothing downstream can distinguish that from an agent that decided to approve itself — which
is the exact thing a gate exists to prevent. So the agent has no approve tool: `request_approval`
and `wait_for_approval` are all it gets, and the decision is made with your credentials, by you.

There is a softer reason too, and in practice it matters more. A prompt that appears mid-conversation
with a **Yes** button gets clicked reflexively. Typing a command names the step you are approving and
puts the agent's reasoning on screen first. That friction is the feature.

## Commands

**Deciding**

| | |
| :--- | :--- |
| `checklists pending` | Every step awaiting your approval, across all runs. |
| `checklists show <run-id>` | The run, its steps, and the full context for anything pending. |
| `checklists approve <run-id> --step <name>` | Approve. `--yes` skips the confirmation prompt. |
| `checklists reject <run-id> --step <name> --reason <why>` | Reject — **fails the whole run**. |

**Runbooks**

| | |
| :--- | :--- |
| `checklists runbooks` | List them. |
| `checklists runbooks show <id>` | Steps, gates, dependencies, run count. |
| `checklists runbooks pull <id> [-o file]` | Write the definition as YAML. Stdout by default, so `pull > file.yaml` works. |
| `checklists runbooks push <id> -f <file>` | Diff the file against the live runbook, ask, apply, bump the version. |
| `checklists runbooks create -f <file>` | Create from a definition. |
| `checklists runbooks rm <id>` | Move to the recycle bin. |
| `checklists runbooks restore <id>` | Bring it back. |

**Runs**

| | |
| :--- | :--- |
| `checklists runs` | In progress. `--all` or `--status <s>` for the rest. |
| `checklists runs start <runbook-id>` | Start a run. |
| `checklists runs complete <run-id> --step <name>` | Complete a step yourself. `--evidence <json>` when the step requires proof. |
| `checklists runs abort <run-id>` | Stop a run. Not a failure, and not resumable. |

**Agent tokens**

| | |
| :--- | :--- |
| `checklists tokens` | List active tokens. `--all` includes revoked. |
| `checklists tokens mint <name>` | Mint a `ck_` token. Printed once. |
| `checklists tokens revoke <id>` | Revoke immediately. |

**Account**

| | |
| :--- | :--- |
| `checklists auth login` | Browser sign-in (Google). `--no-browser` for an emailed code, `--provider <name>` for another provider. |
| `checklists auth logout` | Remove stored credentials. |
| `checklists auth whoami` | Print the signed-in email. |

## Editing a runbook

`pull` writes the same YAML that `create` reads, so editing is a round trip through your editor and
your version control:

```yaml
name: Release
steps:
  - id: tests-green
    check: CI passes on main
    evidence: required
  - id: cut-release
    check: Tag and publish
    gate: human
    gate_surfaces: ["web"]
    depends_on: [tests-green]
```

`push` compares parsed values rather than text, so reformatting the file is never mistaken for a
change. It prints what it will do and waits:

```
  ~ tests-green  check  "CI passes on main" -> "CI passes on main AND the release branch"
  + security-scan  (new step)
  - staging-smoke  (removed)
  Apply? [y/N]
```

**Editing never disturbs a run already under way.** A run snapshots its steps when it starts, and
every rule the server enforces — gates, required evidence, dependencies, approval surfaces — is read
from that snapshot. Change a runbook mid-run and the run keeps executing the version it began with;
the change applies to the next one.

`--step` is required and can be the step's title (or a unique fragment of it) or its node id.
Approving whatever happens to be pending is how you approve something you did not read.

`checklist` is installed as an alias for `checklists`, because the singular is easy to type by
reflex. They are the same command.

## What is guaranteed, and what is not

**Guaranteed:** approval authority is your signed-in identity. The database derives the approver
from the authenticated session, not from what the caller claims, so an agent token cannot approve
anything — it is not a user credential and cannot reach the approval RPC at all.

**Not guaranteed:** that the approval came from a human sitting at the keyboard. An agent with shell
access to your machine can run this command, just as it could drive a browser you are already
signed into. The surface is not a security boundary and never was.

So the CLI records the surface instead of pretending: every approval writes `approved_via = "cli"`
and this machine's hostname to the step, and the run page shows it. "Approved via cli · my-laptop"
is weaker evidence than an approval from a device the agent cannot reach, and it should look weaker.

For gates where that matters, restrict the step:

```json
{ "gate": "human", "gate_surfaces": ["web"], "check": "production migration applied" }
```

Absent or empty means any surface. The check runs in the database, not the client.

## Sign-in

`checklists auth login` opens your browser, you authenticate where your session and password
manager already are, and the credential returns over `http://localhost:8976/callback`. PKCE, so the
returned code is useless to anyone who did not start the flow. No password is typed into or stored
by this CLI.

The session lives in `~/.config/checklists/config.json`, mode `0600`, and refreshes itself.

On a machine with no browser (SSH, a container), `checklists auth login --no-browser` sends a
one-time code by email instead.

### Self-hosting checklists.io

Point at your own project:

```bash
export CHECKLISTS_SUPABASE_URL=https://<ref>.supabase.co
export CHECKLISTS_SUPABASE_KEY=sb_publishable_...
```

Your project needs two things configured:

1. **`http://localhost:8976/callback` on the redirect allow-list** (Authentication → URL
   Configuration). Without it Supabase silently redirects to your site URL instead of the CLI, so
   sign-in appears to work in the browser while the terminal waits forever.
2. **`{{ .Token }}` in the Magic Link email template**, if you want `--no-browser` to work. The
   default template sends only a link, which cannot be pasted into a terminal.

## Requirements

Node 20+.
