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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install
- run: bun install --frozen-lockfile
- run: bun run check:versions
- run: bun run typecheck
- run: bun run test
- run: bun run build
Expand Down
24 changes: 15 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
workflow_dispatch:
inputs:
version:
description: "Version to build (without leading v), e.g. 0.1.0"
description: "Must match package.json, e.g. 2.30.0 (SDK 0.30.0)"
required: true

permissions:
Expand All @@ -25,21 +25,27 @@ jobs:

- name: Resolve version
id: version
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
echo "version=$INPUT_VERSION" >> "$GITHUB_OUTPUT"
else
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
fi

- name: Stamp version into the binary
# package.json is the single source of truth — src/lib/constants.ts reads
# pkg.version, which tsup/bun inline at build time. This MUST match the tag
# because the Homebrew `test do` block asserts `agent --version` == version.
run: npm version "${{ steps.version.outputs.version }}" --no-git-tag-version --allow-same-version

- name: Install dependencies
run: bun install
run: bun install --frozen-lockfile

- name: Check CLI, SDK, and release versions
env:
RELEASE_VERSION: ${{ steps.version.outputs.version }}
run: bun run check:versions "$RELEASE_VERSION"

- name: Validate CLI
run: |
bun run typecheck
bun run test

- name: Build platform binaries
run: |
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,22 @@ only inside their automation — dashboard starts, cron runs — do not
match a repo filter, so `"repo": "any"` is the way to see those alongside the
rest.

## SDK 0.30 configuration

Session configs select one native block. Put the first message inside it:

```yaml
session:
claude_code:
prompt: Fix the failing tests.
# Or: codex: {model: gpt-6-astra, prompt: Fix the failing tests.}
```

The old `harness`, `instructions`, and top-level `prompt` fields are no longer
accepted. `--system` now reports an error; put task instructions in the prompt
or a repository `AGENTS.md` file. Environment build scripts use
`hooks.build_base` and `hooks.after_checkout`; the old `image` block is removed.

## Develop

```sh
Expand Down Expand Up @@ -251,7 +267,7 @@ tarballs, and regenerates the formula in
[`ellipsis-dev/homebrew-cli`](https://github.com/ellipsis-dev/homebrew-cli).

```sh
git tag v0.1.0 && git push origin v0.1.0
git tag v2.30.0 && git push origin v2.30.0
```

The cross-repo push to the tap uses a write-scoped **deploy key**: the public
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.

38 changes: 20 additions & 18 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,35 @@

The CLI ships only as a Homebrew formula from the `ellipsis-dev/homebrew-cli`
tap. It is never published to npm: `package.json` is `private`, has no `bin`,
and there is no `publishConfig`. The single `npm` call in the release workflow
(`npm version --no-git-tag-version`) is just a local tool to rewrite the
version field, not a registry publish.
and there is no `publishConfig`.

CLI **2.X.Y** always uses SDK **0.X.Y**. For example, CLI **2.30.0** uses
`@ellipsis-dev/sdk` **0.30.0**. Keep the CLI version and exact SDK dependency
in `package.json` up to date together, and commit the regenerated `bun.lock`.
Version ranges such as `^0.30.0` are not allowed. CLI-only changes also need
a matching SDK release when advancing the CLI version.

Publishing is fully automated by `.github/workflows/release.yml`, triggered by
pushing a `vX.Y.Z` git tag (there is also a `workflow_dispatch` fallback that
pushing a `v2.X.Y` git tag (there is also a `workflow_dispatch` fallback that
takes a version input in the Actions UI). On a tag push it:

1. Stamps the version into `package.json` (the single source of truth:
`src/lib/constants.ts` reads `pkg.version`, which bun inlines into the
binary, so `agent --version` never drifts).
1. Installs dependencies with the frozen lockfile, checks that the release
version matches `package.json` and the installed SDK follows the version
rule, then runs typechecking and tests. Mismatches stop the release.
2. Cross-compiles four binaries (`darwin-arm64`, `darwin-x64`, `linux-x64`,
`linux-arm64`) with `bun build --compile`, tars each, and computes SHA-256
checksums.
3. Creates the GitHub release with the tarballs and `checksums.txt`.
4. Regenerates `Formula/agent.rb` in the tap repo from the template and pushes
it, so `brew install ellipsis-dev/cli/agent` picks up the new version.

The only manual steps (Hunter cuts releases) are: ensure CI is green, then
create and push the `vX.Y.Z` tag on the main commit to release. No
`package.json` bump commit is needed — the workflow's `npm version` step
stamps the tag's version into `package.json` before building, so the field on
main goes stale by design (releases since v1.4.0 tag main directly).
The manual steps (Hunter cuts releases) are: commit the version updates and
SDK migration, ensure CI is green, then create and push the matching `v2.X.Y`
tag on the main commit to release. For CLI 2.30.0, use `v2.30.0`. The workflow
validates the committed version; it does not rewrite it.

Because the field is stale, local builds don't read their version from it:
`bun run compile` (scripts/compile.sh) stamps the binary from
`git describe --tags`, so `./agent --version` reports exactly what it was
built from — `1.6.0` on a clean tagged checkout, `1.6.0-2-g08ea24d-dirty` two
commits past the tag with uncommitted changes. Only `tsx` dev runs fall back
to the stale `package.json` field.
`package.json` is the version source for development, local compiled binaries,
and releases. `bun run compile` checks the CLI/SDK pair before building;
`./agent --version` reports `2.30.0` for this version, including local builds.
Run `bun run check:versions` to check the pair without building, or
`bun run check:versions 2.30.0` to also validate an intended release version.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ellipsis/cli",
"version": "1.3.0",
"version": "2.30.0",
"description": "Ellipsis agent CLI: drive the Ellipsis cloud from your terminal",
"license": "MIT",
"type": "module",
Expand All @@ -13,14 +13,15 @@
"scripts": {
"dev": "tsx src/cli.tsx",
"build": "tsup",
"check:versions": "bun scripts/check-versions.mjs",
"typecheck": "tsc --noEmit",
"compile": "./scripts/compile.sh",
"start": "node dist/cli.js",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@ellipsis-dev/sdk": "^0.28.0",
"@ellipsis-dev/sdk": "0.30.0",
"chalk": "^5.6.2",
"cli-table3": "^0.6.5",
"commander": "^12.1.0",
Expand Down
18 changes: 18 additions & 0 deletions scripts/check-versions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { readFileSync } from 'node:fs'

const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
const sdk = JSON.parse(readFileSync(new URL('../node_modules/@ellipsis-dev/sdk/package.json', import.meta.url), 'utf8'))
const match = /^2\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(pkg.version)
if (!match) throw new Error('CLI version must be 2.X.Y')

const expectedSdk = `0.${match[1]}.${match[2]}`
if (pkg.dependencies['@ellipsis-dev/sdk'] !== expectedSdk || sdk.version !== expectedSdk) {
throw new Error(`CLI ${pkg.version} requires SDK ${expectedSdk}, pinned exactly and installed`)
}

const releaseVersion = process.argv[2]
if (releaseVersion !== undefined && releaseVersion !== pkg.version) {
throw new Error(`Release version ${releaseVersion} must match package.json version ${pkg.version}`)
}

console.log(`CLI ${pkg.version} / SDK ${sdk.version}`)
13 changes: 3 additions & 10 deletions scripts/compile.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
#!/usr/bin/env bash
# Local/dev compile. Stamps the binary's version from `git describe` so
# `./agent --version` reports exactly what it was built from, e.g.
# "1.6.0-2-g08ea24d-dirty" = 2 commits past v1.6.0 at 08ea24d with
# uncommitted changes (a tagged, clean checkout reads the bare "1.6.0").
# package.json's version field is NOT the truth for local builds: releases
# stopped bumping it (the release workflow rewrites it at tag time and
# builds directly, without this script), so it goes stale on main by design.
# Local builds use the same package.json version as release builds.
set -euo pipefail
cd "$(dirname "$0")/.."
version="$(git describe --tags --always --dirty | sed 's/^v//')"
exec bun build src/cli.tsx --compile --outfile agent \
--define "BUILD_GIT_VERSION=\"${version}\""
bun run check:versions
exec bun build src/cli.tsx --compile --outfile agent
49 changes: 24 additions & 25 deletions skills/ellipsis/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ and `web`:
```yaml
ellipsis:
version: v1
kind: agent
name: Recent work summary
description: Summarizes the week's merged work across api and web

Expand All @@ -106,14 +107,13 @@ trigger:
schedule: "0 9 * * 1"

session:
harness:
type: claude_code
claude_code:
model: claude-haiku-4-5-20251001
instructions: |
Summarize the pull requests merged in api and web over the last 7
days. Group them by theme, lead with user-facing changes, and return
the summary as your answer. Ground every line in a real PR. Never
invent activity.
prompt: |
Summarize the pull requests merged in api and web over the last 7
days. Group them by theme, lead with user-facing changes, and return
the summary as your answer. Ground every line in a real PR. Never
invent activity.

environment:
repositories:
Expand Down Expand Up @@ -183,6 +183,7 @@ Actions and filters live inside the surface block:
```yaml
ellipsis:
version: v1
kind: agent
name: Migration reviewer
description: Flags unsafe database migrations on pull requests

Expand All @@ -195,14 +196,13 @@ trigger:
paths: ["migrations/**"]

session:
harness:
type: claude_code
instructions: |
Review the database migrations in this pull request for production
safety: locking that blocks writes on large tables, missing backfills
for new non-null columns, and rollout ordering that breaks if the
migration and the code deploy out of order. Comment on the pull
request with what you find.
claude_code:
prompt: |
Review the database migrations in this pull request for production
safety: locking that blocks writes on large tables, missing backfills
for new non-null columns, and rollout ordering that breaks if the
migration and the code deploy out of order. Comment on the pull
request with what you find.

environment:
repositories:
Expand Down Expand Up @@ -487,7 +487,7 @@ Top-level keys, all optional except `ellipsis`:

| Key | Purpose |
| --- | --- |
| `ellipsis` | `version: v1`, `name`, `description`, `metadata`, and the `enabled`, `interactive`, `ide` flags. Its presence marks the file as a config. |
| `ellipsis` | `kind: agent`, `version: v1`, `name`, `description`, `metadata`, and `enabled`. Its presence marks the file as a config. |
| `trigger` | One trigger, or omit for a manual-only agent. |
| `input` | A JSON Schema for the payload `agent automation run` passes, and the message template it renders into. |
| `session` | What every session runs on; the keys below. The same keys, flattened, are the body of `agent` / `POST /v1/sessions`. |
Expand All @@ -496,9 +496,8 @@ Under `session`:

| Key | Purpose |
| --- | --- |
| `harness` | Required: `type: claude_code` or `type: codex`, plus that harness's native `model` and `effort` options. Claude Code also accepts `fallback_model`, `max_turns`, and `settings`. |
| `instructions` | Text or repository file references appended to the harness prompt. |
| `environment` | A saved environment by name, or an inline block: `repositories`, `variables`, `ports`, `compute`, `image`, `hooks`, `mcp_servers`. |
| `claude_code` or `codex` | Exactly one native block, with `prompt`, `model`, and `effort`. Claude Code also accepts `fallback_model`, `max_turns`, and `settings`. |
| `environment` | A saved environment by name, or an inline block: `repositories`, `variables`, `compute`, `hooks`, `mcp_servers`. |
| `permissions` | What it may do: `github` scopes its GitHub token, `ellipsis` its API token. |
| `skills` | Claude Code skills beyond what the cloned repositories provide. |
| `output` | A JSON Schema contract, so downstream automation gets typed data. |
Expand All @@ -507,10 +506,10 @@ Under `session`:
The schema is strict, so an unknown or misplaced key fails validation rather
than being silently dropped. Points that decide whether a config works:

- `session.instructions` takes inline text, a `{file: path}` reference to a repository
file, or an ordered list of both, joined at session start. It is appended to
the selected harness's default prompt. 64 KiB per file.
- `session.harness.model` selects a model for the chosen harness. Claude Code
- `session.claude_code.prompt` or `session.codex.prompt` supplies the first user
message verbatim. Put repository guidance in `AGENTS.md`. The former `harness`
and `instructions` keys are rejected.
- `session.claude_code.model` or `session.codex.model` selects a model. Claude Code
inherits the organization default when omitted. `agent model list` reports
the available ids and the harness certified for each. Digest and
summary jobs run well on `claude-haiku-4-5-20251001`; judgment jobs earn the
Expand All @@ -525,8 +524,8 @@ than being silently dropped. Points that decide whether a config works:
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
together with a mention trigger.
- `ellipsis.interactive: false` opts sessions out of messages entirely, for
fire-and-forget automations. `ellipsis.ide: false` locks the sandbox shut.
- Raw session starts accept `lifecycle.interactive: false` to run once. The
returned `lifecycle.prompting` describes whether direct messages are accepted.

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
Expand Down
Loading