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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ agent session connect <session-id> # connect to a session: transcript + live
agent session connect # inside an Ellipsis sandbox: connects to the running session
agent session stop <session-id> # stop an in-flight session

agent review 123 # review a pull request now, instead of waiting for a push
agent review # review the work in your tree; findings print here
agent review list --repo api # list a repository's reviews, newest first
agent review init [path] # scaffold a starter review pipeline (default: agents/code_review.yaml)
agent review default # the effective review pipeline for the repo you are standing in
agent review default set <config-id> # set the account default pipeline (--repo [owner/name] for one repo)

agent config list # list saved agent configs
agent config get <config-id> # show one config as YAML (--json for JSON)
agent config init [path] # scaffold a starter config (default: agents/my_agent.yaml)
Expand Down
1 change: 1 addition & 0 deletions skills/ellipsis/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ Author and inspect agents:

```sh
agent config init # scaffold agents/my_agent.yaml
agent review init # scaffold agents/code_review.yaml
agent config create --template code-reviewer --repo api # deploy via PR
agent config default set <config-id> # the config a bare start runs (--repo for one repo)
agent template list # browse maintained templates
Expand Down
93 changes: 93 additions & 0 deletions src/commands/review.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { type Command } from 'commander'
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
import { basename, dirname, extname } from 'node:path'
import { ApiClient, ApiError } from '../lib/api'
import { alsoKnownAs, apiRoutes } from '../lib/help'
import { createWipCommit, currentBranch, pushReviewBranch, repoFromCwd } from '../lib/laptop'
Expand Down Expand Up @@ -31,6 +33,9 @@ import type {
// How long to wait between REST polls when the stream isn't available.
const FALLBACK_POLL_INTERVAL_SECONDS = 3

// The conventional path for a pipeline file. Convention only: `kind:` decides.
const DEFAULT_PIPELINE_PATH = 'agents/code_review.yaml'

export function registerReview(program: Command): void {
const review = alsoKnownAs(
program
Expand Down Expand Up @@ -176,9 +181,97 @@ export function registerReview(program: Command): void {
})
})

registerReviewInit(review)
registerReviewDefaults(review)
}

// `agent review init`: the code review twin of `agent config init`. Scaffolds a
// starter pipeline YAML locally; you commit it and Ellipsis syncs it from
// GitHub. No API call and no pull request, because `agent config create` posts
// an agent config and a pipeline is a different kind of file.
function registerReviewInit(review: Command): void {
review
.command('init [path]')
.description(
`Scaffold a starter code review pipeline YAML locally (default: ${DEFAULT_PIPELINE_PATH})`,
)
// No `-f` short: CLI-wide, `-f` means an input file.
.option('--force', 'overwrite the file if it already exists')
.action((path: string | undefined, opts: { force?: boolean }) => {
const target = path ?? DEFAULT_PIPELINE_PATH
if (existsSync(target) && !opts.force) {
console.error(`error: ${target} already exists (use --force to overwrite)`)
process.exitCode = 1
return
}
const name = basename(target, extname(target))
mkdirSync(dirname(target), { recursive: true })
writeFileSync(target, starterPipeline(name, repoNameFromCwd()))
console.log(`✓ wrote ${target}`)
console.log(
'Commit it to your default branch. Ellipsis syncs code review pipelines from GitHub.',
)
})
}

// The repository this pipeline should watch, as the bare name the schema takes
// (`repositories:` is scoped to your account, so it never carries the owner).
function repoNameFromCwd(): string | undefined {
const repo = repoFromCwd(process.cwd())
return repo ? repo.split('/')[1] : undefined
}

// A minimal valid pipeline. `ellipsis.kind` is the only field the schema
// requires; every stage left unset runs the platform's default reviewers.
// Exported for tests.
export function starterPipeline(name: string, repository: string | undefined): string {
return `# Ellipsis code review pipeline. Commit this to your default branch; Ellipsis
# syncs it from GitHub. Valid locations: agents/, .agents/, ellipsis/, .ellipsis/
# (any depth). The kind line is what makes this a review pipeline, not an agent.
ellipsis:
version: v1
kind: code_review
name: ${name}
description: What this pipeline reviews.

# Which pull requests this pipeline watches. Only one enabled pipeline may watch
# a given pull request, so name your repositories here. Leaving this out watches
# every repository in the account.
pull_requests:
repositories:
- ${repository ?? 'my-repo'}
# base: [main]
# draft: false
# paths: ["src/**"]
# for: { bots: false }

# Every stage is optional. With all of them unset, reviews run the platform
# default pipeline: three reviewer lenses (correctness, security, regression)
# and a gatekeeper that judges what they found.
#
# review:
# - name: migration-safety
# claude:
# system: |
# Review SQL migrations for locks that block writes on a large table.
# pull_requests:
# paths: ["sql/migrations/**"]
#
# include_default_reviewers: true # run the built-in lenses as well
#
# filter:
# name: gatekeeper
# claude:
# system: |
# Drop any finding that is not worth the author's time.

budget:
run: 2.00
day: 20.00
week: 75.00
`
}

// `agent review default`: which code review pipeline runs when an explicit
// review names none — a two-rung ladder (account default + per-repo defaults,
// repo wins) mirroring `agent config default`, with the same --repo
Expand Down
24 changes: 24 additions & 0 deletions test/review.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
import {
buildCreateRequest,
formatFinding,
parsePullRequest,
splitRepo,
starterPipeline,
} from '../src/commands/review'
import { currentBranch, reviewBranchName } from '../src/lib/laptop'
import type { Finding } from '../src/lib/types'
Expand Down Expand Up @@ -224,3 +226,25 @@ describe('formatFinding', () => {
expect(out).toContain('outside the diff')
})
})

describe('starterPipeline', () => {
it('marks the file as a pipeline, not an agent', () => {
expect(starterPipeline('code_review', 'cli')).toContain('kind: code_review')
})

it('parses as YAML and only sets keys the schema allows', () => {
const parsed = parse(starterPipeline('code_review', 'cli')) as Record<string, unknown>
expect(Object.keys(parsed).sort()).toEqual(['budget', 'ellipsis', 'pull_requests'])
expect(parsed.ellipsis).toMatchObject({ version: 'v1', kind: 'code_review' })
expect(parsed.pull_requests).toEqual({ repositories: ['cli'] })
})

it('names the pipeline after the file, so a second file does not collide', () => {
const parsed = parse(starterPipeline('backend', 'cli')) as { ellipsis: { name: string } }
expect(parsed.ellipsis.name).toBe('backend')
})

it('falls back to a placeholder repository outside a git checkout', () => {
expect(starterPipeline('code_review', undefined)).toContain('- my-repo')
})
})