---
name: cli
description: >
  Drive the Hephaistos code factory from a terminal or a script with the
  `hepha` CLI: create a session from a prompt, follow its transcript, answer
  its question rounds, read the delivery, and escalate guardrail approvals to
  a human. Load when the task mentions hepha, Hephaistos, HEPHA_API_KEY,
  `hepha run`, exit code 3, waiting_human, awaiting_approval, question
  batches, or the `/api/v1` public API.
metadata:
  type: core
  library: "@impulselab/hepha"
  library_version: "0.2"
sources:
  - "impulse-studio/hephaistos:apps/cli/README.md"
  - "impulse-studio/hephaistos:apps/cli/src/commands/*.ts"
  - "impulse-studio/hephaistos:apps/cli/src/lib/follow.ts"
  - "impulse-studio/hephaistos:apps/cli/src/lib/session-outcome.ts"
  - "impulse-studio/hephaistos:packages/database/constants.ts"
  - "impulse-studio/hephaistos:packages/server/src/routers/v1/*/router.ts"
  - "impulse-studio/hephaistos:packages/server/src/routers/v1/schemas.ts"
---

# Hephaistos from the command line

## What the platform does

Hephaistos is a code factory: a demand goes in, a **session** carries it to a
delivered result. One session is one accountable run — it owns an isolated
cloud sandbox, a transcript, and a lifecycle you can drive from this CLI.

| Task type      | What the session produces                                   |
| -------------- | ----------------------------------------------------------- |
| `implement`    | A change on an existing repo, delivered as a pull request   |
| `new-project`  | A new repo scaffolded, provisioned, planned and implemented |
| `orchestrator` | A coordinated change split across delegated tasks           |
| `design`       | A published design bundle and a public preview link         |

Three platform behaviours decide how a caller must drive a session:

- **The question cycle is infinite.** An agent that is not confident asks
  instead of guessing. The session moves to `waiting_human` and stays there
  forever until a round is answered — it costs nothing and never times out.
  Answering is a normal scope, so a script or an agent can do it.
- **Four guardrails are human-only**: merge a pull request, touch production,
  reply to an external client, push to a main branch. The session moves to
  `awaiting_approval` and an API key can read the request but never decide it.
- **Delivery is proven, not claimed.** A finished code session carries a PR
  URL, a verification report (tests + recorded proof) and its CI status —
  read them to judge the result.

Sessions run in parallel on the same repo without conflict: isolation is by
branch, and collisions are resolved at PR time.

## Setup

```bash
npm install --global @impulselab/hepha

# Agents: a key minted by a human with `hepha keys create`.
export HEPHA_API_KEY=hepha_xxxxxxxxxxxxxxxxxxxxxxxx
hepha whoami --json
```

`whoami --json` reports the scopes actually in play — read it first and fail
early instead of halfway through a task.

| Variable                          | Effect                                                                                                    |
| --------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `HEPHA_API_KEY`                   | Act as an agent. **Takes precedence** over a stored `hepha login`.                                        |
| `HEPHA_TOKEN`                     | A device-flow token, instead of `~/.hepha/config.json`.                                                   |
| `HEPHA_URL`                       | Platform base URL (a dev box points it at `http://localhost:3000`).                                       |
| `HEPHA_TIMEOUT_MS`                | Per-request timeout, default `30000`.                                                                     |
| `HEPHA_FOLLOW_MAX_OUTAGE_SECONDS` | Make `--follow` give up this long after its first failure. Unset, it waits for the platform to come back. |

Humans sign in with `hepha login` (device authorization, approved in a
browser). It is the one interactive command and the one that does not honour
`--json`. An agent should never run it.

Scopes are `resource:action`: `sessions:read`, `sessions:write`,
`questions:read`, `questions:answer`, `approvals:read`, `catalog:read`,
`pull_requests:read`. A key created without `--scope` is read-only across
every resource, and scopes can never be widened — only replaced by a new key.

## Core patterns

### Start a session and follow it to its outcome

```bash
hepha repos --json                      # repoFullName must exist in the catalog
hepha run "Add rate limiting to the public API" \
  --repo impulse-studio/hephaistos \
  --task-type implement \
  --follow
```

`run --follow` creates the session, streams the transcript, and returns the
moment the run finishes or needs a person. The exit code carries the outcome
so nothing has to be parsed:

| Code | Meaning                                                  |
| ---- | -------------------------------------------------------- |
| `0`  | Session completed                                        |
| `1`  | Failed, killed or abstained (message on stderr)          |
| `2`  | Usage error (unknown flag, missing argument, bad value)  |
| `3`  | **Blocked on a human** — a question round or a guardrail |

### Branch a script on the outcome

```bash
result=$(hepha run "$PROMPT" --repo "$REPO" --follow --json)
status=$?
session=$(jq -r '.id // .result.id // empty' <<<"$result")

case "$status" in
  0) hepha sessions pr "$session" --json ;;
  3) hepha questions list --session "$session" --json ;;
  *) exit "$status" ;;
esac
```

Under `--json` the session is the document on success and rides in `.result`
on a non-zero exit, so `.id // .result.id` names the session either way — the
id is never printed anywhere else.

Exit `3` is not a failure: it is the platform asking for input. Answer the
round (or escalate the guardrail), then keep following.

### Answer a question round

```bash
hepha questions list --json                    # batches waiting on you
hepha questions show <batchId>                 # ids, options, recommendation
hepha questions answer <batchId> \
  --option q_01=2 \
  --text q_02="Use Postgres, not SQLite"
```

`--option <questionId>=<optionId>[,<optionId>]` selects; `--text
<questionId>=…` adds free text; both may target the same question. For an
agent, one flag carries the whole round:

```bash
hepha questions answer <batchId> \
  --answers '[{"questionId":"q_01","selectedOptionIds":["2"]},{"questionId":"q_02","freeText":"Use Postgres"}]'
```

Then resume following: `hepha sessions logs <id> --follow`.

### Poll a transcript incrementally

```bash
hepha sessions logs <id> --after 128 --limit 200 --json
```

`--after <sequence>` returns only what came after that event. The page carries
`nextCursor` — pass it back as `--after` — and `hasMore`, which is `false`
when you are caught up. `--follow` blocks until the session finishes or parks.

### Collect the delivery

```bash
hepha sessions get <id> --json        # status, prUrl, playbook
hepha sessions verify <id> --json     # test report and recorded proof
hepha sessions pr <id> --json         # CI status of the delivered PR
hepha sessions checks <id>            # re-run the repo's checks
```

### Hand a guardrail to a human

```bash
hepha approvals list --json           # readable with approvals:read
hepha approvals show <id> --json
```

Approving or rejecting requires a human session (`hepha login`), so an agent
stops here and reports the pending approval to a person.

## Command reference

| Command                                                             | Notes                                                                    |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `hepha run "<prompt>"`                                              | `--repo --branch --task-type --model --harness --skill --title --follow` |
| `hepha sessions list`                                               | `--status active\|waiting\|done --repo --search --page --limit --all`    |
| `hepha sessions get\|logs\|send\|pause\|resume\|kill\|archive <id>` | `logs --after <sequence> --follow`, `kill --reason "…"`                  |
| `hepha sessions checks\|pr\|verify <id>`                            | Re-run checks · CI status · proof artifacts                              |
| `hepha questions list\|show\|answer\|cancel`                        | `list --session <id>`                                                    |
| `hepha approvals list\|show\|approve\|reject`                       | `approve`/`reject` are human-session only                                |
| `hepha repos \| branches <owner/repo> \| models \| skills`          | The catalog to read before creating a session                            |
| `hepha keys list\|create\|revoke`                                   | Human-session only                                                       |
| `hepha login \| logout \| whoami`                                   | `login` is interactive                                                   |

Global flags: `--json` on every command that returns data, `--quiet` to drop
the streamed transcript while following, `--version`, `--help`.

## Common Mistakes

### [CRITICAL] Retrying `hepha run` after exit 3

Wrong:

```bash
hepha run "$PROMPT" --repo "$REPO" --follow || hepha run "$PROMPT" --repo "$REPO" --follow
```

Correct:

```bash
hepha run "$PROMPT" --repo "$REPO" --follow
status=$?

case "$status" in
  0) ;;                              # delivered
  3) hepha questions list --json ;;  # answer it, then follow again
  *) exit "$status" ;;               # 1 failed · 2 usage — never remapped
esac
```

Exit 3 means the existing session is parked on a question or a guardrail.
Re-running creates a second session on the same repo while the first one waits
forever.

Source: apps/cli/src/lib/session-outcome.ts

### [HIGH] Approving a guardrail with an API key

Wrong:

```bash
export HEPHA_API_KEY=hepha_xxx
hepha approvals approve "$GUARDRAIL_ID"
```

Correct:

```bash
hepha approvals show "$GUARDRAIL_ID" --json   # read it, then tell a human
```

`approvals:decide` is not a scope that exists — the route answers `403` to any
key. `HEPHA_API_KEY` also takes precedence over a stored login, so a human
whose shell exports it gets `403` on every human-only command until it is
unset.

Source: packages/server/src/routers/v1/approvals/router.ts (humanSessionMiddleware)

### [HIGH] Parsing the human output instead of `--json`

Wrong:

```bash
hepha sessions get "$ID" | grep -i "pr:" | awk '{print $2}'
```

Correct:

```bash
hepha sessions get "$ID" --json | jq -r '.prUrl // empty'
```

The aligned text is padded and coloured for a terminal. Under `--json` stdout
is one parseable document, including failures — `{"ok":false,"error":{…}}`,
with the exit code unchanged.

Source: apps/cli/src/lib/cli-output.ts

### [HIGH] Re-reading the whole transcript on every poll

Wrong:

```bash
while :; do hepha sessions logs "$ID" --json; sleep 5; done
```

Correct:

```bash
after=0
while :; do
  page=$(hepha sessions logs "$ID" --after "$after" --limit 200 --json)
  after=$(jq -r "[$after, .nextCursor // 0, (.items[]?.sequence)] | max" <<<"$page")
  [ "$(jq -r '.hasMore' <<<"$page")" = "true" ] || sleep 3
done
```

Without `--after`, every poll re-downloads the transcript from the start and
the consumer re-processes events it has already seen. Advance on the highest of
both signals: `nextCursor` is `null` on the last page even when that page
carried events, and an event the API cannot render is dropped from `items`
while still consuming a sequence — following either one alone stalls the
cursor or spins on the same page.

Source: apps/cli/src/lib/follow.ts · packages/server/src/services/session-events-query.service.ts

### [MEDIUM] Sending `--answers` as an object

Wrong:

```bash
hepha questions answer "$BATCH" --answers '{"q_01":"2"}'
```

Correct:

```bash
hepha questions answer "$BATCH" --answers '[{"questionId":"q_01","selectedOptionIds":["2"]}]'
```

`--answers` takes a non-empty JSON **array** of `{questionId,
selectedOptionIds?, freeText?}`, and an entry carrying neither an option nor
free text answers nothing — both fail with exit 2 before any request is sent.

Source: apps/cli/src/commands/collect-answers.ts

### [MEDIUM] Creating a key without scopes, then writing with it

Wrong:

```bash
hepha keys create "ci bot"
export HEPHA_API_KEY=hepha_xxx
hepha run "…" --repo "$REPO"      # 403
```

Correct:

```bash
hepha keys create "ci bot" --scope sessions:read --scope sessions:write \
  --scope questions:read --scope questions:answer \
  --scope approvals:read --scope catalog:read
```

A key created without `--scope` is read-only, and passing any scope **replaces**
that default instead of adding to it — so the list has to carry everything the
loop touches: write on sessions, both question scopes (a round nobody can answer
parks forever) and `approvals:read`, without which the agent cannot even read
the guardrail it is supposed to escalate.

Source: packages/database/constants.ts · packages/server/src/routers/api-keys/mutations/create-api-key.ts
