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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ All notable changes will be documented here. This project follows Semantic Versi
- Added strict versioned `.agentskit-review.json` policy with lens coverage, budgets, thresholds, context, and safe CI precedence; incomplete profiles require explicit local opt-in.
- Hardened the shared local CLI worker with cancellation, process-tree cleanup, isolated temporary environments, bounded output, and redacted diagnostics.
- Added bounded source snapshots with infrastructure/configuration file support, denylisted sensitive paths, symlink checks, input limits, and data-boundary-aware secret redaction.
- Added provider-free `--plan`/`--dry-run` preflight with explicit file/byte/call budgets, bounded retries, CLI concurrency defaults, and fail-closed required-lens coverage.
- Made reviews fail closed when any reviewable file has no successful primary lens or cannot be ingested; advisory mode now suppresses finding-based failures only, never source/provider/execution failures.
- Added primary-lens execution coverage to review summaries so partial provider degradation is visible.
- Repositioned the CLI and GitHub Action as provider-neutral.
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ AgentsKit Code Review is built around a different contract:
- **Low noise by design.** Findings are challenged by independent verification votes before they survive.
- **Local first, CI ready.** Review a diff before pushing, inspect complete paths, read stdin, or comment directly on a GitHub PR.
- **Control cost and policy.** Set file budgets, concurrency, thresholds, project conventions, and blocking severity.
- **See the cost before execution.** Use `--plan --json` to inspect files, lenses, retries, concurrency, and estimated provider calls without a model request.

## Run your first review

Expand All @@ -62,6 +63,8 @@ The CLI reviews the current repository's diff against `origin/main` and prints t

Local `codex-cli` and `claude-cli` subprocesses have a 120-second deadline per model call. Set `AGENTSKIT_REVIEW_SUBPROCESS_TIMEOUT_MS` to a positive integer when a provider needs a different limit; timed-out lenses fail explicitly and cannot turn an unreviewed file into an approval.

Preflight refuses an over-budget run before the first provider call. `--dry-run` and `--plan` print the refusal and concrete reductions; `--json` makes the plan machine-readable. CLI providers default to concurrency `1`, while API providers retain concurrency `4`. Required-lens or source coverage failures always exit `2`, even with `--no-fail`.

![AgentsKit Code Review showing an APPROVE result after seven review lenses complete](docs/assets/code-review-terminal.png)

The current command runs directly from GitHub. After the first npm release, the shorter form will be:
Expand Down Expand Up @@ -252,8 +255,10 @@ In shortened examples, replace `...` with `npx --yes github:AgentsKit-io/code-re
| `--votes <n>` | Adversarial verification votes; default `3` |
| `--min-severity <level>` | Minimum reported severity |
| `--min-confidence <n>` | Minimum reported confidence |
| `--max-files <n>` | Positive file budget |
| `--concurrency <n>` | Parallel model calls; default `4` |
| `--max-files <n>` | Positive file budget; over-budget runs are refused before the provider |
| `--max-calls <n>` | Provider-call budget; absolute ceiling `1000` |
| `--concurrency <n>` | Parallel model calls; default `1` for CLI providers, `4` for API providers |
| `--plan`, `--dry-run` | Print provider-free preflight; add `--json` for machine output |
| `--validate-patch` | Run `git apply --check` on suggested patches |
| `--block <severity>` | CI gate floor; default `blocker` |
| `--no-fail` | Keep findings advisory |
Expand Down
167 changes: 138 additions & 29 deletions agents/code-review/agent.ts

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,16 @@ Then require the workflow check in branch protection. CLI exit codes are:

A model response that is malformed may drop one lens while other lenses continue; progress output and the final summary report successful and failed primary-lens counts. If any reviewable file cannot be ingested or has zero successful primary lenses, the pipeline stops before reporters run and exits `2`, including in advisory mode. Treat missing output or exit `2` as unavailable review, not approval.

Use `--plan --json` (or `--dry-run`) to run the source and budget preflight without a model request. The plan reports files, bytes, enabled and required lenses, votes, retries, concurrency, estimated provider calls, and concrete reductions when a limit would be exceeded. The preflight refuses before the provider starts; `maxCalls` is capped at 1000 and unlimited mode is not supported. A required-lens failure is `INCOMPLETE` and exits `2`, including with `--no-fail`.

## Cost and latency controls

Seven lenses fan out over selected files; candidate findings then receive adversarial votes. The primary controls are:

- `--max-files`: positive hard file budget;
- `--max-calls`: bounded provider-call budget (absolute ceiling 1000);
- `--votes`: verification depth and cost;
- `--concurrency`: simultaneous model/subprocess calls;
- `--concurrency`: simultaneous model/subprocess calls (default 1 for CLI providers, 4 for API providers);
- `--paths` or workflow path filters: narrow scope;
- `--min-severity` and `--min-confidence`: output noise, not input-token cost.

Expand Down
15 changes: 12 additions & 3 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ AgentsKit Code Review is built around a different contract:
- **Low noise by design.** Findings are challenged by independent verification votes before they survive.
- **Local first, CI ready.** Review a diff before pushing, inspect complete paths, read stdin, or comment directly on a GitHub PR.
- **Control cost and policy.** Set file budgets, concurrency, thresholds, project conventions, and blocking severity.
- **See the cost before execution.** Use `--plan --json` to inspect files, lenses, retries, concurrency, and estimated provider calls without a model request.

## Run your first review

Expand All @@ -80,6 +81,8 @@ The CLI reviews the current repository's diff against `origin/main` and prints t

Local `codex-cli` and `claude-cli` subprocesses have a 120-second deadline per model call. Set `AGENTSKIT_REVIEW_SUBPROCESS_TIMEOUT_MS` to a positive integer when a provider needs a different limit; timed-out lenses fail explicitly and cannot turn an unreviewed file into an approval.

Preflight refuses an over-budget run before the first provider call. `--dry-run` and `--plan` print the refusal and concrete reductions; `--json` makes the plan machine-readable. CLI providers default to concurrency `1`, while API providers retain concurrency `4`. Required-lens or source coverage failures always exit `2`, even with `--no-fail`.

![AgentsKit Code Review showing an APPROVE result after seven review lenses complete](https://raw.githubusercontent.com/AgentsKit-io/code-review-cli/main/docs/assets/code-review-terminal.png)

The current command runs directly from GitHub. After the first npm release, the shorter form will be:
Expand Down Expand Up @@ -270,8 +273,10 @@ In shortened examples, replace `...` with `npx --yes github:AgentsKit-io/code-re
| `--votes <n>` | Adversarial verification votes; default `3` |
| `--min-severity <level>` | Minimum reported severity |
| `--min-confidence <n>` | Minimum reported confidence |
| `--max-files <n>` | Positive file budget |
| `--concurrency <n>` | Parallel model calls; default `4` |
| `--max-files <n>` | Positive file budget; over-budget runs are refused before the provider |
| `--max-calls <n>` | Provider-call budget; absolute ceiling `1000` |
| `--concurrency <n>` | Parallel model calls; default `1` for CLI providers, `4` for API providers |
| `--plan`, `--dry-run` | Print provider-free preflight; add `--json` for machine output |
| `--validate-patch` | Run `git apply --check` on suggested patches |
| `--block <severity>` | CI gate floor; default `blocker` |
| `--no-fail` | Keep findings advisory |
Expand Down Expand Up @@ -538,13 +543,16 @@ Then require the workflow check in branch protection. CLI exit codes are:

A model response that is malformed may drop one lens while other lenses continue; progress output and the final summary report successful and failed primary-lens counts. If any reviewable file cannot be ingested or has zero successful primary lenses, the pipeline stops before reporters run and exits `2`, including in advisory mode. Treat missing output or exit `2` as unavailable review, not approval.

Use `--plan --json` (or `--dry-run`) to run the source and budget preflight without a model request. The plan reports files, bytes, enabled and required lenses, votes, retries, concurrency, estimated provider calls, and concrete reductions when a limit would be exceeded. The preflight refuses before the provider starts; `maxCalls` is capped at 1000 and unlimited mode is not supported. A required-lens failure is `INCOMPLETE` and exits `2`, including with `--no-fail`.

## Cost and latency controls

Seven lenses fan out over selected files; candidate findings then receive adversarial votes. The primary controls are:

- `--max-files`: positive hard file budget;
- `--max-calls`: bounded provider-call budget (absolute ceiling 1000);
- `--votes`: verification depth and cost;
- `--concurrency`: simultaneous model/subprocess calls;
- `--concurrency`: simultaneous model/subprocess calls (default 1 for CLI providers, 4 for API providers);
- `--paths` or workflow path filters: narrow scope;
- `--min-severity` and `--min-confidence`: output noise, not input-token cost.

Expand Down Expand Up @@ -816,6 +824,7 @@ All notable changes will be documented here. This project follows Semantic Versi
- Added strict versioned `.agentskit-review.json` policy with lens coverage, budgets, thresholds, context, and safe CI precedence; incomplete profiles require explicit local opt-in.
- Hardened the shared local CLI worker with cancellation, process-tree cleanup, isolated temporary environments, bounded output, and redacted diagnostics.
- Added bounded source snapshots with infrastructure/configuration file support, denylisted sensitive paths, symlink checks, input limits, and data-boundary-aware secret redaction.
- Added provider-free `--plan`/`--dry-run` preflight with explicit file/byte/call budgets, bounded retries, CLI concurrency defaults, and fail-closed required-lens coverage.
- Made reviews fail closed when any reviewable file has no successful primary lens or cannot be ingested; advisory mode now suppresses finding-based failures only, never source/provider/execution failures.
- Added primary-lens execution coverage to review summaries so partial provider degradation is visible.
- Repositioned the CLI and GitHub Action as provider-neutral.
Expand Down
2 changes: 1 addition & 1 deletion readme-standard-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@
"docs/OPERATIONS.md",
"test/cli-smoke.test.mjs"
],
"sourceHash": "sha256:608f0984d346b0defcbfb81efd7b962fdba0bdb45a4be7c52ed95de3bea7fc0b"
"sourceHash": "sha256:aae7093f7641e5be3cec91d5a24bee31311bfe474395895e95923f91d6b33b5a"
},
"exceptions": []
}
Expand Down
45 changes: 36 additions & 9 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import type { AdapterFactory } from '@agentskit/core'
import { createProgressObserver } from '@agentskit/ink'
import { readFileSync } from 'node:fs'
import { builtInLenses, createCodeReviewAgent, type Category, type CodeReviewConfig, type Reporter, type Severity } from '../agents/code-review/agent.js'
import { builtInLenses, createCodeReviewAgent, type Category, type CodeReviewConfig, type Reporter, type ReviewPlan, type Severity } from '../agents/code-review/agent.js'
import { githubInlineReporter, githubSummaryReporter, markdownReporter, sarifReporter } from '../agents/code-review/reporters.js'
import { claudeCode } from './claude-code-adapter.js'
import { codexCli } from './codex-adapter.js'
Expand Down Expand Up @@ -52,14 +52,17 @@ Review options:
--min-confidence <n> Minimum finding confidence
--block <level> CI gate floor (default: blocker)
--max-files <n> Positive file budget
--concurrency <n> Parallel model calls (default: 4)
--max-calls <n> Provider-call budget (absolute ceiling: 1000)
--concurrency <n> Parallel model calls (default: 1 for CLI, 4 for API)
--conventions <path> Project conventions file
--allow-incomplete Local-only exception for an explicitly incomplete profile
--allow-unredacted Local-only exception for secret redaction
--validate-patch Validate suggested patches with git apply --check
--sarif <file> Also write a SARIF report
--post Post a PR review (with --pr)
--no-fail Report findings without failing the process
--dry-run, --plan Print the provider-free preflight plan without model calls
--json Emit machine-readable plan output with --plan/--dry-run

Provider options:
--model <id> Model id (required for API/local-server providers)
Expand Down Expand Up @@ -148,14 +151,12 @@ async function main() {
minSeverity: flag('min-severity') as Severity | undefined,
minConfidence: flag('min-confidence') === undefined ? undefined : Number(flag('min-confidence')),
maxFiles: flag('max-files') === undefined ? undefined : Number(flag('max-files')),
maxCalls: flag('max-calls') === undefined ? undefined : Number(flag('max-calls')),
concurrency: flag('concurrency') === undefined ? undefined : Number(flag('concurrency')),
conventions: flag('conventions'),
},
})
const source = await resolveSource(reviewConfig)
await preflightProvider(reviewConfig)
const adapter = buildAdapter(reviewConfig)

const reporters: Reporter[] = [markdownReporter()]
const sarif = flag('sarif')
if (sarif) reporters.push(sarifReporter({ file: sarif }))
Expand All @@ -165,7 +166,6 @@ async function main() {
}

const config: CodeReviewConfig = {
adapter,
source,
reporters,
observers: [createProgressObserver()],
Expand All @@ -174,15 +174,27 @@ async function main() {
auditVotes: reviewConfig.votes,
validatePatch: has('validate-patch'),
blockingSeverity: (flag('block') as Severity) ?? 'blocker',
budget: { maxFiles: reviewConfig.budget.maxFiles, concurrency: reviewConfig.budget.concurrency },
requiredLenses: Object.entries(reviewConfig.lenses).filter(([, policy]) => policy.required).map(([key]) => key as Category),
retries: reviewConfig.retries,
budget: { maxFiles: reviewConfig.budget.maxFiles, maxBytes: reviewConfig.budget.maxBytes, maxCalls: reviewConfig.budget.maxCalls, concurrency: reviewConfig.budget.concurrency },
conventions: reviewConfig.conventions ? { path: reviewConfig.conventions } : autoConventions(),
thresholds: reviewConfig.thresholds,
}

const review = await createCodeReviewAgent(config).run()
const agent = createCodeReviewAgent(config)
const plan = await agent.plan()
if (has('dry-run') || has('plan')) {
if (has('json')) console.log(JSON.stringify(plan))
else console.log(formatPlan(plan))
if (plan.overBudget.length) process.exitCode = 2
return
}
await preflightProvider(reviewConfig)
agent.setAdapter(buildAdapter(reviewConfig))
const review = await agent.run()
// --no-fail = advisory: post the review but never fail the job (exit 0). Real errors
// still surface via the catch below (exit 2).
process.exit(reviewConfig.incompleteProfile ? 2 : review.blocking && !has('no-fail') ? 1 : 0)
process.exit(review.incomplete ? 2 : review.blocking && !has('no-fail') ? 1 : 0)
}

/**
Expand Down Expand Up @@ -259,6 +271,21 @@ function formatDoctor(report: DoctorReport): string {
return lines.join('\n')
}

function formatPlan(plan: ReviewPlan): string {
const status = plan.overBudget.length ? 'REFUSED' : 'READY'
const lines = [
`Review preflight — ${status}`,
`Files: ${plan.files} · Bytes: ${plan.bytes} · Unreviewed: ${plan.unreviewedFiles}`,
`Lenses: ${plan.enabledLenses.join(', ') || 'none'}`,
`Required: ${plan.requiredLenses.join(', ') || 'none'}`,
`Votes: ${plan.votes} · Retries: ${plan.retries} · Concurrency: ${plan.concurrency}`,
`Estimated provider calls: ${plan.estimatedProviderCalls}/${plan.maxCalls}`,
]
for (const reason of plan.overBudget) lines.push(`Refusal: ${reason}`)
for (const suggestion of plan.suggestions) lines.push(`Suggestion: ${suggestion}`)
return lines.join('\n')
}

/** Best-effort: feed a conventions doc to every lens if one exists. */
function autoConventions(): string | undefined {
for (const f of ['CONVENTIONS.md', 'CONTRIBUTING.md', '.cursorrules', 'AGENTS.md']) {
Expand Down
15 changes: 11 additions & 4 deletions src/review-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type FileConfig = z.infer<typeof ReviewConfigSchema>

export interface ReviewConfigOverrides {
provider?: string; model?: string; transport?: string; votes?: number; retries?: number
minSeverity?: Severity; minConfidence?: number; maxFiles?: number; concurrency?: number; conventions?: string
minSeverity?: Severity; minConfidence?: number; maxFiles?: number; maxCalls?: number; concurrency?: number; conventions?: string
}

export interface ResolvedReviewConfig {
Expand Down Expand Up @@ -126,11 +126,18 @@ export function resolveReviewConfig(
if (incompleteProfile && !options.allowIncomplete) throw new ReviewConfigError('incomplete profile requires explicit --allow-incomplete for a local run')

const thresholds = { ...file?.thresholds, ...(overrides.minSeverity === undefined ? {} : { minSeverity: overrides.minSeverity }), ...(overrides.minConfidence === undefined ? {} : { minConfidence: overrides.minConfidence }) }
const budget = { ...file?.budget, ...(overrides.maxFiles === undefined ? {} : { maxFiles: overrides.maxFiles }), ...(overrides.concurrency === undefined ? {} : { concurrency: overrides.concurrency }) }
const budget = {
...file?.budget,
...(overrides.maxFiles === undefined ? {} : { maxFiles: overrides.maxFiles }),
...(overrides.maxCalls === undefined ? {} : { maxCalls: overrides.maxCalls }),
...(overrides.concurrency === undefined ? {} : { concurrency: overrides.concurrency }),
}
const provider = overrides.provider ?? file?.provider
const defaultConcurrency = provider?.endsWith('-cli') ? 1 : 4
const effective = {
configVersion: 1 as const, lenses, incompleteProfile,
votes: overrides.votes ?? file?.votes ?? 3, retries: overrides.retries ?? file?.retries ?? 1,
thresholds, budget: { ...budget, concurrency: budget.concurrency ?? 4 },
thresholds, budget: { ...budget, concurrency: budget.concurrency ?? defaultConcurrency, maxCalls: budget.maxCalls ?? 1000 },
worker: { timeoutMs: file?.worker?.timeoutMs ?? localCliTimeoutMs(), maxOutputBytes: file?.worker?.maxOutputBytes ?? DEFAULT_LOCAL_CLI_OUTPUT_BYTES },
conventions: overrides.conventions ?? file?.conventions,
context: { mode: file?.context?.mode ?? 'prompt', patterns: file?.context?.patterns ?? [] },
Expand All @@ -142,7 +149,7 @@ export function resolveReviewConfig(
const validation = z.object({
votes: positiveInt.max(25), retries: nonNegativeInt.max(1),
thresholds: z.object({ minSeverity: z.enum(['blocker', 'high', 'med', 'nit']).optional(), minConfidence: z.number().min(0).max(1).optional() }),
budget: z.object({ maxFiles: positiveInt.max(500).optional(), concurrency: positiveInt.max(32) }),
budget: z.object({ maxFiles: positiveInt.max(500).optional(), maxBytes: positiveInt.max(25 * 1024 * 1024).optional(), maxCalls: positiveInt.max(1000), concurrency: positiveInt.max(32) }),
}).safeParse(effective)
if (!validation.success) throw new ReviewConfigError(`invalid effective review config: ${diagnostic(validation.error)}`)
return effective
Expand Down
Loading