Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ ellipsis session start --config-file f.json # ...or from an inline config
ellipsis session start --template ellipsis-helper # ...or from a maintained template
ellipsis session start --budget 5 "..." # cap this session's spend, in dollars
ellipsis session start --image shot.png "..." # the agent sees the picture on its first turn
ellipsis session start --watch "..." # start and immediately stream it
ellipsis session start --watch "..." # start and stream it until its opening turn ends
ellipsis session list --limit 20 # list recent sessions (filter by --source, --author, --since, …)
ellipsis session get <session-id> # inspect one session (prints a dashboard link)
ellipsis session get <session-id> --watch # follow a session until it finishes
ellipsis session get <session-id> --watch # follow the turn in progress until it ends
ellipsis session record <session-id> # read a session's stored transcript, one line per record
ellipsis session stop <session-id> # stop an in-flight session
ellipsis session stop <session-id> # stop a session's turn in progress

ellipsis review 123 # review a pull request now, instead of waiting for a push
ellipsis review get <review-id> # a review's findings, scope, and whether it posted
Expand Down Expand Up @@ -108,11 +108,6 @@ ellipsis slack members # workspace members, with linked GitHub ide
ellipsis linear teams # teams in the connected Linear organization
ellipsis sentry orgs # connected Sentry organizations

ellipsis file upload shot.png # store a PNG; prints an org-gated link to paste into a PR comment
ellipsis file list # list stored files (--session <id> scopes to one run's uploads)
ellipsis file get <file-id> -o shot.png # show one file, or download its bytes with -o
ellipsis file delete <file-id> # delete a file (it disappears from list/get and its link stops resolving)

ellipsis variable list # list sandbox env variable names (values are write-only)
ellipsis variable set A=1 B=2 # create/update variables (or --from-file .env/.json)
ellipsis variable delete K # delete a variable
Expand All @@ -139,9 +134,10 @@ the public REST API. Point it at a different instance durably with
legacy `ELLIPSIS_API_BASE`).

`--watch` (on both `session start` and `session get`) streams the session's
output live over WebSocket until it reaches a terminal status, falling back to
periodic status polling if the live stream is unavailable. Either way it first
prints a clickable dashboard link. How the stream works is described in
output live over WebSocket until the turn it is waiting on ends (`completed`,
`failed`, `stopped`, or `cancelled`), falling back to polling the turn if the
live stream is unavailable. It exits 0 only for `completed`. Either way it
first prints a clickable dashboard link. How the stream works is described in
[`docs/SESSION_STREAMING.md`](docs/SESSION_STREAMING.md).

### Auth
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 33 additions & 18 deletions docs/SESSION_STREAMING.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# Session streaming: how `--watch` follows a session
# Session streaming: how `--watch` follows a turn

`ellipsis session start --watch` and `ellipsis session get --watch` follow a
session's output live until it reaches a terminal status. The stream is
read-only: the CLI never sends anything to the session. Stopping one is
`ellipsis session stop`.
session's output live until the turn they are waiting on ends. For `start`
that is the opening turn the start created (a promptless start has none, so
there is nothing to wait for). For `get` it is the turn in progress: the
running turn, else the pending one. A session with no turn in progress has
nothing to wait for either: `get --watch` prints the latest turn's status and
exits. The stream is read-only: the CLI never sends anything to the session.
Stopping a turn is `ellipsis session stop`.

The WebSocket client is `streamSession` from `@ellipsis-dev/sdk/stream`. This
repo owns only the transport adapter (`src/lib/stream.ts`) and the rendering
(`watchSessionStreaming` in `src/commands/session.ts`).
(`streamTurn` in `src/commands/session.ts`).

## Endpoint

Expand Down Expand Up @@ -35,17 +39,29 @@ the SDK package.

| Frame | Payload | What the watch log does with it |
| --- | --- | --- |
| `snapshot` | `session`, `messages`, `earliest_feed_seq`, `protocol` | prints the status word when it changes |
| `session` | `session`, the whole row, resent on any change | same: collapsed to status-word transitions |
| `records_append` | `records`, feed-ordered `SessionRecord`s | one line per transcript item, via `recordToItems` from `@ellipsis-dev/sdk/store` |
| `snapshot` | `session`, `messages`, `earliest_feed_seq`, `protocol` | prints the awaited turn's status when it changes |
| `session` | `session`, the whole object, resent on any change | same: collapsed to the awaited turn's status transitions; a final status ends the watch |
| `records_append` | `records`, feed-ordered `SessionRecord`s | one line per transcript item, via `recordToItems` from `@ellipsis-dev/sdk/store`; a `turn_ended` record for the awaited turn ends the watch |
| `delta` | ephemeral partial output for a turn | skipped: the committed record supersedes it |
| `heartbeat` | `ts` | skipped: liveness only |
| `error` | `message` | printed to stderr; the watch ends with exit code 1 |
| `done` | none | ends the watch; the last seen status decides the exit code |
| `done` | none | the conversation closed, which happens only after its turn ended; the watch ends |

The stream itself stays open for the whole conversation. A watch wants one
turn of it, so it closes the socket as soon as that turn's end arrives (its
`turn_ended` record, or a `session` frame carrying the turn's final status),
and resolves the turn with `GET /v1/sessions/{id}/turns/{turn_id}` if the
stream ended first.

Platform records render with plain wording: environment preparation
(`environment_phase`), the customer's own hook output (`environment_output`),
`Environment ready`, how a turn ended (`turn_ended`), and `Conversation
closed`. Records whose type the SDK has no copy for, including types the
platform no longer emits, render nothing.

`--json` with `--watch` prints one JSON object per frame (NDJSON) with the
same filtering: `heartbeat` and `delta` are dropped, and `snapshot` and
`session` frames are printed only when the status word changes.
`session` frames are printed only when the awaited turn's status changes.

## Liveness, reconnect, fallback

Expand All @@ -63,15 +79,14 @@ All of this is inside `streamSession`; the CLI configures none of it.
fallback. `1002` and `1003` mean the protocol is unsupported and give up at
once. Every other code is retried.
- Fallback: giving up throws `StreamUnavailableError`. The CLI prints
`live stream unavailable (...); falling back to status polling` on stderr
and polls `GET /v1/sessions/{id}` every 2 seconds, printing status
transitions until a terminal status (`watchSession`). `--watch --quiet`
`live stream unavailable (...); falling back to polling the turn` on stderr
and polls `GET /v1/sessions/{id}/turns/{turn_id}` every 2 seconds, printing
status transitions until a final status (`pollTurn`). `--watch --quiet`
takes this polling path directly, with no live output.

## Exit code

A watch exits 0 when the session ended in `completed`, `closed`, or `idle`,
and 1 otherwise (`exitCodeForStatus`). When a conversation closes, the
execution outcome (`lifecycle.last_execution_result.completion_reason`, for
example `budget_hit`) stands in for the lifecycle status, so a closed session
that hit its budget still exits 1.
A watch exits 0 when the turn ended `completed`, and 1 when it ended `failed`,
`stopped`, or `cancelled` (`exitCodeForStatus`). Its last line names the
outcome with the turn's `reason` and `detail`, for example
`✗ session session_1 turn failed (budget_hit): The session reached its budget.`
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ellipsis/cli",
"version": "2.30.0",
"version": "2.31.0",
"description": "Ellipsis CLI: drive the Ellipsis cloud from your terminal",
"license": "MIT",
"type": "module",
Expand All @@ -21,7 +21,7 @@
"test:watch": "vitest"
},
"dependencies": {
"@ellipsis-dev/sdk": "0.30.0",
"@ellipsis-dev/sdk": "0.31.0",
"commander": "^12.1.0",
"ws": "^8.18.0",
"yaml": "^2.9.0"
Expand Down
30 changes: 14 additions & 16 deletions skills/cli-conventions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,43 @@ route text.
Singular nouns, one verb per action.

```
ellipsis file list ellipsis file delete <file-id>
ellipsis environment list ellipsis environment delete <environment-id>
ellipsis session start ellipsis automation edit <automation-id>
```

- **The noun is singular, always.** `file`, not `files`. `hook`, not
- **The noun is singular, always.** `environment`, not `environments`. `hook`, not
`hooks`. `analytics` is the sole exception: it is a mass noun with no
singular form.
- **The plural still works, hidden.** Register it with `alsoKnownAs`, which
keeps it callable but strips it from every help surface. `ellipsis files list`
keeps it callable but strips it from every help surface. `ellipsis environments list`
runs and prints nothing extra.
- **A renamed command keeps its old name, hidden.** `ellipsis file` was `agent
asset`, so it registers `asset` and `assets` alongside `files`. A caller who
learned the old spelling is never told it is wrong.
- **A renamed command keeps its old name, hidden.** Register the old spelling
with `alsoKnownAs` beside the new one. A caller who learned the old spelling
is never told it is wrong.
- **Read-only integration browsers use a bare plural leaf**: `github repos`,
`slack channels`, `linear teams`, `sentry orgs`. They have no
get/create/delete to disambiguate against, so the extra `list` is noise.
Anything with more than one verb gets `<noun> <verb>`: `file list`,
`file get`, `file upload`, `file delete`.
Anything with more than one verb gets `<noun> <verb>`: `environment list`,
`environment get`, `environment create`, `environment delete`.
- **`delete` is the shown verb**, with `rm` as a hidden alias. Never the
reverse.
- **`list` is the shown verb**, with `ls` hidden.

```ts
const file = alsoKnownAs(
program.command('file').description('...'),
'files',
'asset',
'assets',
const environment = alsoKnownAs(
program.command('environment').description('...'),
'environments',
)

apiRoutes(
alsoKnownAs(file.command('delete <file-id>').description('...'), 'rm'),
'DELETE /v1/files/{id}',
alsoKnownAs(environment.command('delete <environment-id>').description('...'), 'rm'),
'DELETE /v1/environments/{id}',
)
```

## Arguments

Kebab-case placeholders: `<session-id>`, `<config-id>`, `<file-id>`,
Kebab-case placeholders: `<session-id>`, `<config-id>`, `<environment-id>`,
`<api-url>`, `<owner/name>`. Never camelCase, and never a bare `<id>` when the
type matters.

Expand Down
42 changes: 21 additions & 21 deletions skills/ellipsis/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ fee. There are no seats.
reviewed, or commit a pipeline file to scope and customize it.
- **Delegation from scripts or CI**: `ellipsis session start` or
`POST /v1/sessions`. With `--watch` it streams into the log and exits nonzero
unless the session completes, so it works as a gate.
unless the turn completes, so it works as a gate.

Things teams actually build: screenshot every pull request that touches the
frontend so reviewers see the change; investigate Sentry alerts when they fire
Expand Down Expand Up @@ -236,9 +236,9 @@ session:
conversation, so an alert storm produces one investigation, not dozens of
duplicates. Webhook deliveries are deduplicated, so a replay never
double-runs an agent.
- React and cron sessions are single-shot. Mention and on-demand sessions are
- React and cron sessions run once. Mention and on-demand sessions are
durable conversations: follow-ups keep the whole exchange and the working
tree, and an idle conversation costs near nothing between turns.
tree, and a conversation costs near nothing between turns.

## Code review

Expand Down Expand Up @@ -389,7 +389,7 @@ ellipsis session start "triage the failing CI on api" # a bare ad-hoc session
ellipsis automation run <automation-id> --input '{...}' # invoke an automation as defined
ellipsis session start --config-file agents/my_agent.yaml --watch
ellipsis session start --template ellipsis-helper --watch
ellipsis session get <session-id> --watch # follow a running session
ellipsis session get <session-id> --watch # follow the turn in progress
ellipsis session stop <session-id>
```

Expand All @@ -399,9 +399,9 @@ prompt is the sole instruction. The CLI also sends the repository you are standi
the server clones it. Per-session overrides need no config edit: `--model`,
`--system`, `--repo`, `--cpu`, `--memory`, `--timeout`, `--budget`, and
`--override` for a full partial config patch. `--rebuild` skips the image
cache. `--detach` returns immediately. `--watch --quiet` prints only status
transitions and the result, and either watch form exits `0` only when the session
completes.
cache. `--detach` returns immediately. `--watch --quiet` prints only the turn's
status transitions and how it ended, and either watch form exits `0` only when
the turn completes.

List and audit what agents have done:

Expand Down Expand Up @@ -467,7 +467,6 @@ ellipsis variable list # names and timestamps only
ellipsis integration # what is connected, in one table
ellipsis github repos # also github members, slack channels,
# linear teams, sentry orgs
ellipsis file upload shot.png # store a PNG, print an org-gated link
```

Most singular commands accept the plural spelling as a hidden alias, and
Expand Down Expand Up @@ -510,19 +509,21 @@ than being silently dropped. Points that decide whether a config works:
- `session.budget.session` defaults to $250, which is also the platform maximum, so it
can only be lowered. `day`, `week`, and `month` are trailing 1, 7, and 28 day
caps on this agent, with ceilings of $1,000, $10,000, and $40,000. A session
that reaches a cap stops mid-task and records `budget_hit`, which is a distinct
exit status from an error. Accounts also have their own trailing caps, plus
that reaches a cap stops mid-task and its turn fails with reason `budget_hit`,
distinct from an error. Accounts also have their own trailing caps, plus
opt-in per-developer caps.
- `session.output` makes an agent a function with a contract: it exits through
your JSON Schema, so downstream automation gets typed data instead of prose to
parse. Schema failures exit loudly as `tool_call_failed`. It does not go
parse. Schema failures fail the turn with reason `tool_call_failed`. It does not go
together with a mention trigger.
- Raw session starts accept `lifecycle.interactive: false` to run once. The
returned `lifecycle.prompting` describes whether direct messages are accepted.
- Raw session starts accept `conversation.interactive: false` to run once. The
returned `conversation.prompting` describes whether direct messages are
accepted, and `turn` is the turn to wait on: a message is answered when its
turn's status is `completed`, `failed`, `stopped`, or `cancelled`.

Validation surfaces on push to the default branch, on config pull requests, in
the dashboard editor, and at session start for checks that need the session's
own commit. Session-start failures record an exit status that names the cause:
own commit. A turn that cannot start fails with a reason that names the cause:
`lifecycle_hook_failed`, `missing_repo_access`, `missing_token_permissions`,
`missing_sandbox_variables`, `tool_call_failed`, `budget_hit`.

Expand All @@ -545,7 +546,7 @@ Three `environment` fields define the sandbox, each with a different lifetime:
never cached, for session-scoped setup such as authenticating a CLI. Capped at
5 minutes each.

A non-zero exit from any of them fails the session with
A non-zero exit from any of them fails the turn with reason
`lifecycle_hook_failed`. The image is cached per repository set, commit, and
image definition, so repeat sessions start in seconds instead of reinstalling
dependencies. `ellipsis session start --config-file <path> --rebuild --watch`
Expand Down Expand Up @@ -589,9 +590,9 @@ print, so keep `image.setup` and hooks from echoing a value.
Every session outlives its sandbox, which is what makes agent work reviewable
rather than a black box.

- The live feed interleaves the agent's own output with lifecycle events, and
streams with lossless resume, so you can watch an agent work and catch a wrong
turn before it compounds.
- The live feed interleaves the agent's own output with environment and turn
events, and streams with lossless resume, so you can watch an agent work and
catch a wrong turn before it compounds.
- Every turn and tool call is recorded, with the config version it ran and the
exact instructions it launched with, so "what did the agent do" and "what was
the agent told" are both reads rather than reconstructions.
Expand Down Expand Up @@ -641,11 +642,10 @@ npx skills add ellipsis-dev/cli
If `ELLIPSIS_SANDBOX_ID` is set in the environment, you are the agent in an
Ellipsis session. The `ellipsis` CLI is pre-installed and pre-authenticated with a
session-scoped token, so you can start child sessions, list the team's sessions,
read analytics, and upload screenshots as org-gated links
(`ellipsis file upload shot.png`) with no login.
and read analytics with no login.

That token is deliberately narrower than a human's. It can list variable names
but not set or delete them, cannot delete a file, and cannot repoint an
but not set or delete them, and cannot repoint an
account or repository default. An agent cannot overwrite the team's credentials
or destroy the evidence it posted.

Expand Down
2 changes: 0 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { registerReview } from './commands/review'
import { registerAutomation } from './commands/automation'
import { registerEnvironment } from './commands/environment'
import { registerVariable } from './commands/variable'
import { registerFile } from './commands/file'
import { registerTemplate } from './commands/template'
import { registerModel } from './commands/model'
import { registerIntegration } from './commands/integrations'
Expand Down Expand Up @@ -42,7 +41,6 @@ registerReview(program)
registerAutomation(program)
registerEnvironment(program)
registerVariable(program)
registerFile(program)
registerTemplate(program)
registerModel(program)
registerIntegration(program)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function renderIdentity(me: WhoAmI): void {
if (me.gh_user) console.log(`user: ${me.gh_user.login} (${me.user_id})`)
else if (me.user_id) console.log(`user: ${me.user_id}`)
if (me.api_key_id) console.log(`api key: ${me.api_key_id}`)
if (me.sandbox_id) console.log(`sandbox: ${me.sandbox_id}`)
if (me.session_id) console.log(`session: ${me.session_id}`)
}

export function registerAuth(program: Command): void {
Expand Down
Loading
Loading