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 @@ -6,6 +6,7 @@ All notable changes will be documented here. This project follows Semantic Versi

### Changed

- Added strict versioned `.agentskit-review.json` policy with lens coverage, budgets, thresholds, context, and safe CI precedence; incomplete profiles require explicit local opt-in.
- 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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ In shortened examples, replace `...` with `npx --yes github:AgentsKit-io/code-re
| `--block <severity>` | CI gate floor; default `blocker` |
| `--no-fail` | Keep findings advisory |
| `--conventions <path>` | Inject project conventions |
| `--allow-incomplete` | Local-only exception for a config that declares incomplete lens coverage |
| `--api` | Back-compatible alias for `--provider anthropic` |
| `doctor --provider <name>` | Offline provider diagnostics; no model request |
| `doctor --live` | Explicit provider smoke-test mode |
Expand All @@ -267,6 +268,33 @@ In shortened examples, replace `...` with `npx --yes github:AgentsKit-io/code-re

When no conventions path is supplied, the CLI looks for `CONVENTIONS.md`, `CONTRIBUTING.md`, `.cursorrules`, or `AGENTS.md`.

### Versioned configuration

The repository may contain one strict `.agentskit-review.json` file. It must use
`configVersion: 1`; unknown fields, secrets, unsupported values, and unsafe lens
policies fail before provider execution with exit `2`. Every built-in lens is
enabled by default, with `correctness`, `security`, and `tests` required. Flags
override file values. A required lens may only be disabled in an explicitly
declared `incompleteProfile`, which requires `--allow-incomplete` locally and is
never accepted in CI.

```json
{
"configVersion": 1,
"lenses": {
"performance": { "enabled": false, "required": false }
},
"votes": 3,
"budget": { "maxFiles": 20, "maxCalls": 200, "concurrency": 1 },
"thresholds": { "minSeverity": "med", "minConfidence": 0.7 },
"context": { "mode": "prompt", "patterns": ["src/**"] }
}
```

Provider, model, transport, context trust, redaction, and permissions are
trusted execution inputs; a project config cannot set them in CI. Put provider
credentials only in the environment or provider login, never in this file.

### Doctor

Run `doctor` before a review to check a registered provider’s executable, version, transport, model requirement, configuration mode, and credential presence. It is offline by default: API credentials are checked only for presence and values are never printed; local CLI login is represented as login-managed until a provider-specific live check is available. Unknown local CLI versions warn locally and fail when `CI=true`. Exit `0` means healthy, `1` means a failed diagnostic, and `2` means invalid CLI usage.
Expand Down
12 changes: 10 additions & 2 deletions agents/code-review/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export interface CodeReviewConfig {
source: SourceConfig
/** Defaults to the 7 built-in lenses. Pass a subset to disable, or add custom lenses. */
lenses?: Lens[]
/** A declared incomplete profile is reported as COMMENT, never APPROVE. */
incompleteProfile?: boolean
/** Project conventions injected into every lens — a string, or a file to read. */
conventions?: string | { path: string }
thresholds?: { minSeverity?: Severity; minConfidence?: number; maxPerFile?: number; suppressNits?: boolean }
Expand Down Expand Up @@ -165,7 +167,7 @@ const Consolidation = z.object({ duplicateGroups: z.array(z.array(z.number())) }
const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7
const SEV_RANK: Record<Severity, number> = { blocker: 0, high: 1, med: 2, nit: 3 }

const DEFAULT_LENSES: Lens[] = [
export const DEFAULT_LENSES: Lens[] = [
{ key: 'correctness', skill: correctnessLens },
{ key: 'security', skill: securityLens },
{ key: 'performance', skill: performanceLens },
Expand All @@ -175,6 +177,11 @@ const DEFAULT_LENSES: Lens[] = [
{ key: 'conventions', skill: conventionsLens, severityCeiling: 'nit' },
]

export function builtInLenses(enabled: readonly Category[]): Lens[] {
const selected = new Set(enabled)
return DEFAULT_LENSES.filter((lens) => selected.has(lens.key))
}

type Limiter = <T>(fn: () => Promise<T>) => Promise<T>

/**
Expand Down Expand Up @@ -416,14 +423,15 @@ export function createCodeReviewAgent(config: CodeReviewConfig) {
): ReviewResult {
const counts = (['blocker', 'high', 'med', 'nit'] as Severity[]).map((s) => ({ s, n: kept.filter((f) => f.severity === s).length }))
const worst = kept.length ? Math.min(...kept.map((f) => SEV_RANK[f.severity])) : 3
const verdict: Verdict = !kept.length ? 'APPROVE' : worst <= SEV_RANK.high ? 'REQUEST CHANGES' : 'COMMENT'
const verdict: Verdict = config.incompleteProfile ? 'COMMENT' : !kept.length ? 'APPROVE' : worst <= SEV_RANK.high ? 'REQUEST CHANGES' : 'COMMENT'
const blocking = kept.some((f) => SEV_RANK[f.severity] <= SEV_RANK[blockingSeverity])
const breakdown = counts.filter((c) => c.n).map((c) => `${c.n} ${c.s}`).join(', ') || 'no findings'
const executionSummary =
`${execution.succeeded}/${execution.attempted} lens executions succeeded` +
(execution.failed ? `; ${execution.failed} failed` : '')
const summary =
`${kept.length} finding(s) (${breakdown}) across ${reviewed} file(s)` +
(config.incompleteProfile ? ' Incomplete profile; this review is not an approval.' : '') +
(droppedFiles ? `, ${droppedFiles} file(s) skipped for budget` : '') +
`. ${executionSummary}.`
return { verdict, blocking, findings: kept, dropped, execution, summary }
Expand Down
19 changes: 19 additions & 0 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ Consumer configuration must select a provider through `args`. Keep credentials i

The default diff base remains `origin/main`. A pre-commit invocation does not mean the input is limited to the Git staging area. Set `--base` explicitly when the repository uses another integration branch.

## Versioned review configuration

Use a strict `.agentskit-review.json` at the repository root for review policy.
It requires `configVersion: 1` and supports lens policy (`enabled` and
`required` per built-in lens), votes, retries, thresholds, file/byte/call and
concurrency budgets, conventions, and context selection. All built-in lenses
are enabled by default; correctness, security, and tests are required.

Flags override file values. The file cannot contain credentials or executable
plugins. Provider, model, transport, trust mode, redaction, permissions, and
other execution inputs are rejected when supplied by the project config in CI.
An intentionally incomplete profile must say `incompleteProfile: true` and be
run locally with `--allow-incomplete`; it is rejected in CI and cannot become an
approval. Malformed, unknown, or unsafe configuration exits `2` before a model
request and diagnostics do not print config values.

Keep policy-only configuration in the file. Use trusted workflow flags or the
runner environment for provider selection, credentials, and execution mode.

## Local Ollama review

Ollama serves its local API at `http://localhost:11434` by default. Verify the service without sending repository content:
Expand Down
48 changes: 48 additions & 0 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ In shortened examples, replace `...` with `npx --yes github:AgentsKit-io/code-re
| `--block <severity>` | CI gate floor; default `blocker` |
| `--no-fail` | Keep findings advisory |
| `--conventions <path>` | Inject project conventions |
| `--allow-incomplete` | Local-only exception for a config that declares incomplete lens coverage |
| `--api` | Back-compatible alias for `--provider anthropic` |
| `doctor --provider <name>` | Offline provider diagnostics; no model request |
| `doctor --live` | Explicit provider smoke-test mode |
Expand All @@ -285,6 +286,33 @@ In shortened examples, replace `...` with `npx --yes github:AgentsKit-io/code-re

When no conventions path is supplied, the CLI looks for `CONVENTIONS.md`, `CONTRIBUTING.md`, `.cursorrules`, or `AGENTS.md`.

### Versioned configuration

The repository may contain one strict `.agentskit-review.json` file. It must use
`configVersion: 1`; unknown fields, secrets, unsupported values, and unsafe lens
policies fail before provider execution with exit `2`. Every built-in lens is
enabled by default, with `correctness`, `security`, and `tests` required. Flags
override file values. A required lens may only be disabled in an explicitly
declared `incompleteProfile`, which requires `--allow-incomplete` locally and is
never accepted in CI.

```json
{
"configVersion": 1,
"lenses": {
"performance": { "enabled": false, "required": false }
},
"votes": 3,
"budget": { "maxFiles": 20, "maxCalls": 200, "concurrency": 1 },
"thresholds": { "minSeverity": "med", "minConfidence": 0.7 },
"context": { "mode": "prompt", "patterns": ["src/**"] }
}
```

Provider, model, transport, context trust, redaction, and permissions are
trusted execution inputs; a project config cannot set them in CI. Put provider
credentials only in the environment or provider login, never in this file.

### Doctor

Run `doctor` before a review to check a registered provider’s executable, version, transport, model requirement, configuration mode, and credential presence. It is offline by default: API credentials are checked only for presence and values are never printed; local CLI login is represented as login-managed until a provider-specific live check is available. Unknown local CLI versions warn locally and fail when `CI=true`. Exit `0` means healthy, `1` means a failed diagnostic, and `2` means invalid CLI usage.
Expand Down Expand Up @@ -397,6 +425,25 @@ Consumer configuration must select a provider through `args`. Keep credentials i

The default diff base remains `origin/main`. A pre-commit invocation does not mean the input is limited to the Git staging area. Set `--base` explicitly when the repository uses another integration branch.

## Versioned review configuration

Use a strict `.agentskit-review.json` at the repository root for review policy.
It requires `configVersion: 1` and supports lens policy (`enabled` and
`required` per built-in lens), votes, retries, thresholds, file/byte/call and
concurrency budgets, conventions, and context selection. All built-in lenses
are enabled by default; correctness, security, and tests are required.

Flags override file values. The file cannot contain credentials or executable
plugins. Provider, model, transport, trust mode, redaction, permissions, and
other execution inputs are rejected when supplied by the project config in CI.
An intentionally incomplete profile must say `incompleteProfile: true` and be
run locally with `--allow-incomplete`; it is rejected in CI and cannot become an
approval. Malformed, unknown, or unsafe configuration exits `2` before a model
request and diagnostics do not print config values.

Keep policy-only configuration in the file. Use trusted workflow flags or the
runner environment for provider selection, credentials, and execution mode.

## Local Ollama review

Ollama serves its local API at `http://localhost:11434` by default. Verify the service without sending repository content:
Expand Down Expand Up @@ -747,6 +794,7 @@ All notable changes will be documented here. This project follows Semantic Versi

### Changed

- Added strict versioned `.agentskit-review.json` policy with lens coverage, budgets, thresholds, context, and safe CI precedence; incomplete profiles require explicit local opt-in.
- 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:9e43fecef7502fabffa1bc976a9febebdf14d7522431cd18e8f7bb4553078e68"
"sourceHash": "sha256:1a2be3167c85699211c0b9ba45b870d8d88d0841a5d8d99b2c4bfe211a2b22fc"
},
"exceptions": []
}
Expand Down
54 changes: 35 additions & 19 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
import type { AdapterFactory } from '@agentskit/core'
import { createProgressObserver } from '@agentskit/ink'
import { readFileSync } from 'node:fs'
import { createCodeReviewAgent, type CodeReviewConfig, type Reporter, type Severity } from '../agents/code-review/agent.js'
import { builtInLenses, createCodeReviewAgent, type Category, type CodeReviewConfig, type Reporter, 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'
import { ollamaReview } from './ollama-adapter.js'
import type { SourceConfig } from '../agents/code-review/sources.js'
import { diagnoseProvider, factoryFor, providerEntry, providerRegistry, resolveProviderId, type DoctorReport, type ProviderEntry } from './provider-registry.js'
import { loadReviewConfig, type ResolvedReviewConfig } from './review-config.js'

const HELP = `AgentsKit Code Review — deep, low-noise review with your model

Expand Down Expand Up @@ -53,6 +54,7 @@ Review options:
--max-files <n> Positive file budget
--concurrency <n> Parallel model calls (default: 4)
--conventions <path> Project conventions file
--allow-incomplete Local-only exception for an explicitly incomplete profile
--validate-patch Validate suggested patches with git apply --check
--sarif <file> Also write a SARIF report
--post Post a PR review (with --pr)
Expand Down Expand Up @@ -124,9 +126,24 @@ async function main() {
await runDoctor()
return
}
const reviewConfig = loadReviewConfig(process.cwd(), {
ci: has('ci') || process.env.CI === 'true' || process.env.CI === '1',
allowIncomplete: has('allow-incomplete'),
overrides: {
provider: flag('provider') ?? (has('api') ? 'anthropic' : undefined),
model: flag('model'),
transport: flag('transport'),
votes: flag('votes') === undefined ? undefined : Number(flag('votes')),
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')),
concurrency: flag('concurrency') === undefined ? undefined : Number(flag('concurrency')),
conventions: flag('conventions'),
},
})
const source = await resolveSource()
await preflightProvider()
const adapter = buildAdapter()
await preflightProvider(reviewConfig)
const adapter = buildAdapter(reviewConfig)

const reporters: Reporter[] = [markdownReporter()]
const sarif = flag('sarif')
Expand All @@ -141,33 +158,32 @@ async function main() {
source,
reporters,
observers: [createProgressObserver()],
auditVotes: flag('votes') ? Number(flag('votes')) : undefined,
lenses: builtInLenses(Object.entries(reviewConfig.lenses).filter(([, policy]) => policy.enabled).map(([key]) => key as Category)),
incompleteProfile: reviewConfig.incompleteProfile,
auditVotes: reviewConfig.votes,
validatePatch: has('validate-patch'),
blockingSeverity: (flag('block') as Severity) ?? 'blocker',
budget: { maxFiles: flag('max-files') ? Number(flag('max-files')) : undefined, concurrency: flag('concurrency') ? Number(flag('concurrency')) : 4 },
conventions: flag('conventions') ? { path: flag('conventions')! } : autoConventions(),
thresholds: {
minSeverity: flag('min-severity') as Severity | undefined,
minConfidence: flag('min-confidence') ? Number(flag('min-confidence')) : undefined,
},
budget: { maxFiles: reviewConfig.budget.maxFiles, concurrency: reviewConfig.budget.concurrency },
conventions: reviewConfig.conventions ? { path: reviewConfig.conventions } : autoConventions(),
thresholds: reviewConfig.thresholds,
}

const review = await createCodeReviewAgent(config).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(review.blocking && !has('no-fail') ? 1 : 0)
process.exit(reviewConfig.incompleteProfile ? 2 : review.blocking && !has('no-fail') ? 1 : 0)
}

/**
* Provider-neutral adapter selection. Local CLIs are explicit choices; any other
* name resolves to a `@agentskit/adapters` factory and is given
* `{ apiKey, model, baseUrl? }`. `--api` is a back-compat alias for `--provider anthropic`.
*/
function buildAdapter(): AdapterFactory {
const requestedProvider = flag('provider') ?? (has('api') ? 'anthropic' : undefined)
function buildAdapter(reviewConfig: ResolvedReviewConfig): AdapterFactory {
const requestedProvider = reviewConfig.provider
const provider = requestedProvider && resolveProviderId(requestedProvider)
if (!provider) throw new Error(requestedProvider ? `unknown --provider "${requestedProvider}" (run --list-providers for common options)` : 'choose a provider with --provider <name> (run --list-providers for common options)')
const model = flag('model') ?? (has('api') ? 'claude-opus-4-8' : undefined)
const model = reviewConfig.model ?? (has('api') ? 'claude-opus-4-8' : undefined)
if (provider === 'claude-cli') return claudeCode({ model })
if (provider === 'codex-cli') return codexCli({ model })
if (provider === 'ollama') {
Expand All @@ -184,16 +200,16 @@ function buildAdapter(): AdapterFactory {
return make({ apiKey, model, ...(baseUrl ? { baseUrl } : {}) })
}

async function preflightProvider(): Promise<void> {
const requested = flag('provider') ?? (has('api') ? 'anthropic' : undefined)
async function preflightProvider(reviewConfig: ResolvedReviewConfig): Promise<void> {
const requested = reviewConfig.provider
const id = requested && resolveProviderId(requested)
const entry = id && providerEntry(id)
if (!entry || entry.kind === 'api') return
const report = await diagnoseProvider({
provider: entry.id,
model: flag('model'),
transport: flag('transport'),
mode: flag('mode'),
model: reviewConfig.model,
transport: reviewConfig.transport,
mode: flag('mode') ?? reviewConfig.trustMode,
apiKey: flag('api-key'),
ci: has('ci'),
})
Expand Down
Loading