From 914f2bc83a2472e21fae775cd18ab51b466f35ff Mon Sep 17 00:00:00 2001 From: hbrooks Date: Fri, 31 Jul 2026 11:38:16 -0400 Subject: [PATCH] review: add 'agent review init' to scaffold a code review pipeline 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. --- README.md | 7 +++ skills/ellipsis/SKILL.md | 1 + src/commands/review.ts | 93 ++++++++++++++++++++++++++++++++++++++++ test/review.test.ts | 24 +++++++++++ 4 files changed, 125 insertions(+) diff --git a/README.md b/README.md index 8103efa..0cf1f9b 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,13 @@ agent session connect # connect to a session: transcript + live agent session connect # inside an Ellipsis sandbox: connects to the running session agent session stop # 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 # set the account default pipeline (--repo [owner/name] for one repo) + agent config list # list saved agent configs agent config get # show one config as YAML (--json for JSON) agent config init [path] # scaffold a starter config (default: agents/my_agent.yaml) diff --git a/skills/ellipsis/SKILL.md b/skills/ellipsis/SKILL.md index 95eba16..5aef181 100644 --- a/skills/ellipsis/SKILL.md +++ b/skills/ellipsis/SKILL.md @@ -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 # the config a bare start runs (--repo for one repo) agent template list # browse maintained templates diff --git a/src/commands/review.ts b/src/commands/review.ts index a7161e5..b99c2e8 100644 --- a/src/commands/review.ts +++ b/src/commands/review.ts @@ -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' @@ -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 @@ -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 diff --git a/test/review.test.ts b/test/review.test.ts index e17fec9..9ce8669 100644 --- a/test/review.test.ts +++ b/test/review.test.ts @@ -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' @@ -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 + 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') + }) +})