Skip to content

Commit 5eca08d

Browse files
authored
review: add 'agent review init' to scaffold a code review pipeline (#97)
Authoring a pipeline meant knowing that `kind: code_review` is what separates it from an agent config, and that Ellipsis syncs it from GitHub rather than reading it from the API. `agent config init` covers agents; this is its twin for reviews.
1 parent cdf8274 commit 5eca08d

4 files changed

Lines changed: 125 additions & 0 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ agent session connect <session-id> # connect to a session: transcript + live
6767
agent session connect # inside an Ellipsis sandbox: connects to the running session
6868
agent session stop <session-id> # stop an in-flight session
6969

70+
agent review 123 # review a pull request now, instead of waiting for a push
71+
agent review # review the work in your tree; findings print here
72+
agent review list --repo api # list a repository's reviews, newest first
73+
agent review init [path] # scaffold a starter review pipeline (default: agents/code_review.yaml)
74+
agent review default # the effective review pipeline for the repo you are standing in
75+
agent review default set <config-id> # set the account default pipeline (--repo [owner/name] for one repo)
76+
7077
agent config list # list saved agent configs
7178
agent config get <config-id> # show one config as YAML (--json for JSON)
7279
agent config init [path] # scaffold a starter config (default: agents/my_agent.yaml)

skills/ellipsis/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ Author and inspect agents:
204204

205205
```sh
206206
agent config init # scaffold agents/my_agent.yaml
207+
agent review init # scaffold agents/code_review.yaml
207208
agent config create --template code-reviewer --repo api # deploy via PR
208209
agent config default set <config-id> # the config a bare start runs (--repo for one repo)
209210
agent template list # browse maintained templates

src/commands/review.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { type Command } from 'commander'
2+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
3+
import { basename, dirname, extname } from 'node:path'
24
import { ApiClient, ApiError } from '../lib/api'
35
import { alsoKnownAs, apiRoutes } from '../lib/help'
46
import { createWipCommit, currentBranch, pushReviewBranch, repoFromCwd } from '../lib/laptop'
@@ -31,6 +33,9 @@ import type {
3133
// How long to wait between REST polls when the stream isn't available.
3234
const FALLBACK_POLL_INTERVAL_SECONDS = 3
3335

36+
// The conventional path for a pipeline file. Convention only: `kind:` decides.
37+
const DEFAULT_PIPELINE_PATH = 'agents/code_review.yaml'
38+
3439
export function registerReview(program: Command): void {
3540
const review = alsoKnownAs(
3641
program
@@ -176,9 +181,97 @@ export function registerReview(program: Command): void {
176181
})
177182
})
178183

184+
registerReviewInit(review)
179185
registerReviewDefaults(review)
180186
}
181187

188+
// `agent review init`: the code review twin of `agent config init`. Scaffolds a
189+
// starter pipeline YAML locally; you commit it and Ellipsis syncs it from
190+
// GitHub. No API call and no pull request, because `agent config create` posts
191+
// an agent config and a pipeline is a different kind of file.
192+
function registerReviewInit(review: Command): void {
193+
review
194+
.command('init [path]')
195+
.description(
196+
`Scaffold a starter code review pipeline YAML locally (default: ${DEFAULT_PIPELINE_PATH})`,
197+
)
198+
// No `-f` short: CLI-wide, `-f` means an input file.
199+
.option('--force', 'overwrite the file if it already exists')
200+
.action((path: string | undefined, opts: { force?: boolean }) => {
201+
const target = path ?? DEFAULT_PIPELINE_PATH
202+
if (existsSync(target) && !opts.force) {
203+
console.error(`error: ${target} already exists (use --force to overwrite)`)
204+
process.exitCode = 1
205+
return
206+
}
207+
const name = basename(target, extname(target))
208+
mkdirSync(dirname(target), { recursive: true })
209+
writeFileSync(target, starterPipeline(name, repoNameFromCwd()))
210+
console.log(`✓ wrote ${target}`)
211+
console.log(
212+
'Commit it to your default branch. Ellipsis syncs code review pipelines from GitHub.',
213+
)
214+
})
215+
}
216+
217+
// The repository this pipeline should watch, as the bare name the schema takes
218+
// (`repositories:` is scoped to your account, so it never carries the owner).
219+
function repoNameFromCwd(): string | undefined {
220+
const repo = repoFromCwd(process.cwd())
221+
return repo ? repo.split('/')[1] : undefined
222+
}
223+
224+
// A minimal valid pipeline. `ellipsis.kind` is the only field the schema
225+
// requires; every stage left unset runs the platform's default reviewers.
226+
// Exported for tests.
227+
export function starterPipeline(name: string, repository: string | undefined): string {
228+
return `# Ellipsis code review pipeline. Commit this to your default branch; Ellipsis
229+
# syncs it from GitHub. Valid locations: agents/, .agents/, ellipsis/, .ellipsis/
230+
# (any depth). The kind line is what makes this a review pipeline, not an agent.
231+
ellipsis:
232+
version: v1
233+
kind: code_review
234+
name: ${name}
235+
description: What this pipeline reviews.
236+
237+
# Which pull requests this pipeline watches. Only one enabled pipeline may watch
238+
# a given pull request, so name your repositories here. Leaving this out watches
239+
# every repository in the account.
240+
pull_requests:
241+
repositories:
242+
- ${repository ?? 'my-repo'}
243+
# base: [main]
244+
# draft: false
245+
# paths: ["src/**"]
246+
# for: { bots: false }
247+
248+
# Every stage is optional. With all of them unset, reviews run the platform
249+
# default pipeline: three reviewer lenses (correctness, security, regression)
250+
# and a gatekeeper that judges what they found.
251+
#
252+
# review:
253+
# - name: migration-safety
254+
# claude:
255+
# system: |
256+
# Review SQL migrations for locks that block writes on a large table.
257+
# pull_requests:
258+
# paths: ["sql/migrations/**"]
259+
#
260+
# include_default_reviewers: true # run the built-in lenses as well
261+
#
262+
# filter:
263+
# name: gatekeeper
264+
# claude:
265+
# system: |
266+
# Drop any finding that is not worth the author's time.
267+
268+
budget:
269+
run: 2.00
270+
day: 20.00
271+
week: 75.00
272+
`
273+
}
274+
182275
// `agent review default`: which code review pipeline runs when an explicit
183276
// review names none — a two-rung ladder (account default + per-repo defaults,
184277
// repo wins) mirroring `agent config default`, with the same --repo

test/review.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { mkdtempSync, writeFileSync } from 'node:fs'
33
import { tmpdir } from 'node:os'
44
import { join } from 'node:path'
55
import { describe, expect, it } from 'vitest'
6+
import { parse } from 'yaml'
67
import {
78
buildCreateRequest,
89
formatFinding,
910
parsePullRequest,
1011
splitRepo,
12+
starterPipeline,
1113
} from '../src/commands/review'
1214
import { currentBranch, reviewBranchName } from '../src/lib/laptop'
1315
import type { Finding } from '../src/lib/types'
@@ -224,3 +226,25 @@ describe('formatFinding', () => {
224226
expect(out).toContain('outside the diff')
225227
})
226228
})
229+
230+
describe('starterPipeline', () => {
231+
it('marks the file as a pipeline, not an agent', () => {
232+
expect(starterPipeline('code_review', 'cli')).toContain('kind: code_review')
233+
})
234+
235+
it('parses as YAML and only sets keys the schema allows', () => {
236+
const parsed = parse(starterPipeline('code_review', 'cli')) as Record<string, unknown>
237+
expect(Object.keys(parsed).sort()).toEqual(['budget', 'ellipsis', 'pull_requests'])
238+
expect(parsed.ellipsis).toMatchObject({ version: 'v1', kind: 'code_review' })
239+
expect(parsed.pull_requests).toEqual({ repositories: ['cli'] })
240+
})
241+
242+
it('names the pipeline after the file, so a second file does not collide', () => {
243+
const parsed = parse(starterPipeline('backend', 'cli')) as { ellipsis: { name: string } }
244+
expect(parsed.ellipsis.name).toBe('backend')
245+
})
246+
247+
it('falls back to a placeholder repository outside a git checkout', () => {
248+
expect(starterPipeline('code_review', undefined)).toContain('- my-repo')
249+
})
250+
})

0 commit comments

Comments
 (0)