@zuke/ai
`@zuke/ai` — AI-powered code review for Zuke builds.
@zuke/ai on JSR ↗ 49 symbols
Symbols
interface AgentContext The failure context handed to an AgentRunner, plus a ready prompt.
| Member | Signature | Doc |
|---|---|---|
target | target: string | The name of the failed target. |
attempt | attempt: number | The 1-based recovery attempt. |
command? | command?: string | The command line that failed, if known. |
output | output: string | The captured error output (stderr, or the error message). |
conventions? | conventions?: string | Project conventions (CLAUDE.md / AGENTS.md), if found. |
prompt | prompt: string | A ready-to-use prompt assembled from the fields above. |
function agentFixer(run: AgentRunner, configure?: Configure<AgentFixer>): AgentFixer Construct an AgentFixer from an AgentRunner and apply the configuration lambda. Plug the result into a target with .recoverWith(...).
class AgentFixer A fluent agent fixer. Construct one via agentFixer with a runner, and attach it to a target with .recoverWith(...). Diagnose/report defaults are on; file changes happen through the agent, gated to local runs unless .runOnly(...) says otherwise.
| Member | Signature | Doc |
|---|---|---|
runOnly | runOnly(scope: RunScope): this | Where the agent may run. Defaults to "local" — run on a developer's machine, refuse on CI. "ci" is the inverse, and the shape a repository's own build wants: an agent that heals a pull request must not be turned loose on a working tree someone is in the middle of editing. Off CI the fixer then reports the skip and leaves the failure standing, without ever starting the agent. "both" permits either host. One setter on one axis, so the effective scope never depends on the order two flags were called in. |
allowCI | allowCI(): this | Permit the agent to run (and edit files) on CI as well as locally. |
suggest | suggest(): this | Propose the agent's changes as committable inline suggestions on the pull request (from its git diff) instead of committing them. The build stays failed — the human applies the suggestions to fix it. Mutually exclusive with commitFixes (suggest takes precedence). GitHub only; elsewhere it falls back to the overview comment. |
commitFixes | commitFixes(): this | After the agent runs, stage all its changes, commit, and push to the current branch so a healed PR carries the fix. Requires a checkout that can push; a failed push is reported, not fatal. |
commitMessage | commitMessage(message: string): this | Override the commit subject used by commitFixes. |
noPush | noPush(): this | Commit the fix but do not push it. |
comment | comment(): this | Also post what the agent did as a PR comment (on by default). |
noComment | noComment(): this | Do not post a PR comment (the job summary is still written). |
commentToken | commentToken(token: AnyParameter | string): this | The token used to post the PR comment (defaults to the host's env var). |
criteria | criteria(criteria: string): this | Project-specific notes appended to the agent's prompt. |
conventions | conventions(text: string): this | Supply the project conventions text directly, instead of letting the fixer read CLAUDE.md/AGENTS.md from the working tree. Pass an empty string to send none. This is a trust pin, not only a convenience. By default the conventions are read from the tree under repair, which on a contributor's branch is content that contributor wrote — so the model is told to respect a document the author of the failing change controls. The prompt fences it as untrusted data and says so, but a fence is defence-in-depth against a model that can still be coaxed, not a guarantee. Pinning the text here — or passing "" — takes the branch out of the loop entirely, and is worth doing wherever the fixer is authorized to write: an AgentFixer runs an agent that edits files and runs commands with no path allow-list at all. |
quiet | quiet(): this | Suppress the console printout (the summary/comment are still written). |
env | env(reader: EnvReader): this | The environment reader used to detect CI and the comment host (test seam). |
exec | exec(run: unknown): this | The git runner used for committing (test seam). |
readFile | readFile(impl: unknown): this | The convention-file reader (test seam). |
fetch | fetch(impl: fetch): this | The fetch implementation used to post the PR comment (test seam). |
remediate | remediate(context: RemediationContext): Promise<RemediationResult> | Run the agent against the failure, optionally commit its changes, and ask the executor to re-run the target as the verifier. Skips (no retry) outside the hosts runOnly permits, or if the agent run itself fails. |
name | name: string | A name for diagnostics — "agent fix". |
type AgentResult = CommandOutput | string | void What an AgentRunner may resolve to: a CommandOutput (its stdout is captured for the report), a string, or nothing.
type AgentRunner = unknown Runs the coding agent against the assembled AgentContext. The agent is expected to edit files in place; the executor then re-runs the target. Throw (or let the underlying command throw) to signal the agent could not run.
function aiCache(configure?: Configure<AiCache>): AiCache Construct an AiCache, applying an optional configure lambda so it can be set up inline — e.g. aiCache((c) => c.dir(".cache").ttl(3600)).
class AiCache A best-effort cache of AI provider responses, keyed by a stableHash of each call's salient parts and persisted through a CacheStore (the default writes JSON files under AiCache.dir). Configure it inline with aiCache: set the AiCache.dir, the AiCache.ttl, or AiCache.disable it entirely, then read with AiCache.get_ and write with AiCache.put_.
| Member | Signature | Doc |
|---|---|---|
dir | dir(path: string): this | Directory for the default file store (default ".zuke/ai-cache"). |
ttl | ttl(seconds: number): this | Entries older than this many seconds are ignored (default 604800 = 7 days; 0 = never expire). |
disable | disable(): this | Turn the cache off programmatically (get_ misses, put_ is a no-op). |
store | store(custom: CacheStore): this | Inject a custom backing store (test seam; overrides the file store). |
now | now(clock: unknown): this | Clock seam for createdAt and TTL checks (default Date.now). |
enabled_ | enabled_(): boolean | INTERNAL: whether the cache is active. |
key_ | key_(parts: string[]): string | INTERNAL: derive a stable key from the given parts. |
get_ | get_(key: string): Promise<CacheEntry | undefined> | INTERNAL: fetch a live (non-expired) entry, or undefined. |
put_ | put_(key: string, text: string, usage?: Usage): Promise<void> | INTERNAL: store a response under key. |
function aiFixer(configure?: Configure<AiFixer>): AiFixer Construct an AiFixer and apply the configuration lambda. Plug the result into a target with .recoverWith(...):
test = target()
.executes(() => DenoTasks.test((s) => s.allowAll()))
.recoverWith(aiFixer((f) => f.provider("claude").apiKey(this.key)));class AiFixer A fluent AI fixer. Construct one via aiFixer, configure it, and attach it to a target with .recoverWith(...). Only .provider(...) and .apiKey(...) are required; everything else defaults.
| Member | Signature | Doc |
|---|---|---|
provider | provider(provider: Provider): this | Set the model provider (required). |
apiKey | apiKey(apiKey: AnyParameter | string): this | Set the API key, from a secret parameter or a literal string (required). |
model | model(model: string): this | Override the model (default: the provider's recommended model). |
effort | effort(effort: Effort): this | Set the thinking-effort hint (honoured by Claude; ignored elsewhere). |
criteria | criteria(criteria: string): this | Project-specific notes appended to the prompt (idioms, constraints). |
conventions | conventions(text: string): this | Supply the project conventions text directly, instead of letting the fixer read CLAUDE.md/AGENTS.md from the working tree. Pass an empty string to send none. This is a trust pin, not only a convenience. By default the conventions are read from the tree under repair, which on a contributor's branch is content that contributor wrote — so the model is told to respect a document the author of the failing change controls. The prompt fences it as untrusted data and says so, but a fence is defence-in-depth against a model that can still be coaxed, not a guarantee. Pinning the text here — or passing "" — takes the branch out of the loop entirely, and is worth doing wherever the fixer is authorized to write: AiFixer.autoApply or AiFixer.commitFixes. |
diff | diff(configure: Configure<DiffSettings>): this | Configure the diff source used for context (default: the working tree). |
include | include(...globs: string[]): this | Only include diff sections matching these globs in the prompt context. |
exclude | exclude(...globs: string[]): this | Exclude diff sections matching these globs from the prompt context. |
maxDiffTokens | maxDiffTokens(tokens: number): this | Cap the context diff at roughly this many tokens (default 16000). |
autoApply | autoApply(): this | Apply the proposed fix to the working tree and ask the executor to re-run the target. Off by default (the fixer only diagnoses). Writes are confined by allowPaths, the built-in exclusions, and maxEdits, and are confined to the hosts runOnly permits. |
allowPaths | allowPaths(...globs: string[]): this | Restrict applied edits to paths matching these globs (default: all). |
excludePaths | excludePaths(...globs: string[]): this | Exclude paths matching these globs from edits, on top of the built-ins. |
maxEdits | maxEdits(count: number): this | Cap how many files a single applied fix may touch (default 10). |
runOnly | runOnly(scope: RunScope): this | Where the fixer may run. Defaults to "local" — apply on a developer's machine, refuse on CI. "ci" is the inverse, and the shape a repository's own build wants: self-heal a pull request without ever rewriting a working tree someone is in the middle of editing. Off CI the fixer then returns before the model is called, so it costs no tokens and the underlying failure stands unchanged. That is a stronger refusal than the default scope makes on CI, where the fixer still diagnoses and only declines to write: a diagnosis on a pull request is worth reading, whereas one on a local run is an unasked-for charge against the developer's own key. "both" permits either host. One setter on one axis, so the effective scope never depends on the order two flags were called in. ``ts aiFixer((f) => f.provider("openai").apiKey(key).autoApply().runOnly("ci")); `` |
allowCI | allowCI(): this | Permit auto-apply (and committing) on CI as well as locally. |
commitFixes | commitFixes(): this | After applying a fix, stage it, commit it, and push to the current branch — so a healed pull request carries the fix as a commit. Implies autoApply. Requires a checkout that can push (a non-detached branch with credentials); a failed push is reported, not fatal. |
commitMessage | commitMessage(message: string): this | Override the commit subject used by commitFixes. |
noPush | noPush(): this | Commit the fix but do not push it (leave it staged in a local commit). |
comment | comment(): this | Also post the diagnosis/fix as a PR comment (on by default). |
noComment | noComment(): this | Do not post a PR comment (the job summary is still written). |
suggest | suggest(): this | On GitHub, post each code location as an inline review comment with a committable suggestion block (the Copilot-style suggestion) instead of a single overview comment. On by default; a no-op off GitHub or when the model reports no specific locations, where the overview comment is used. |
noSuggest | noSuggest(): this | Post a single overview comment instead of inline GitHub suggestions. |
commentToken | commentToken(token: AnyParameter | string): this | The token used to post the PR comment (defaults to the host's env var). |
retry | retry(options?: RetryOptions): this | Retry the provider call on transient failures (see RetryOptions). |
quiet | quiet(): this | Suppress the console printout (the summary/comment are still written). |
fetch | fetch(impl: fetch): this | The fetch implementation for the API call (test seam). |
exec | exec(run: unknown): this | The git runner used for the diff and commit (test seam). |
write | write(impl: unknown): this | The file writer used when applying edits (test seam). |
readFile | readFile(impl: unknown): this | The convention-file reader (test seam). |
env | env(reader: EnvReader): this | The environment reader used to detect CI and the comment host (test seam). |
budget | budget(budget: Budget): this | Attach a shared Budget that caps spend by an exact token count (a USD cap is opt-in, computed from prices you supply to the budget). Once the cap is reached the fixer skips the model call (and reports it) rather than running up the bill; share one budget across reviewers and fixers to bound a whole build. |
cache | cache(cache: AiCache): this | Reuse a prior fix for an identical failure (same provider, model, and prompt) instead of calling the API again — see AiCache. Helpful when the same failure recurs across CI re-runs; a hit does not draw down the budget. |
remediate | remediate(context: RemediationContext): Promise<RemediationResult> | Diagnose the failure and, when permitted, apply (and commit) the fix. Always reports; returns { retry: true } only when it changed the working tree so the executor re-runs the target as the verifier. |
name | name: string | A name for diagnostics — "AI fix". |
class AiReviewError extends Error Raised when a reviewer is misconfigured, the API fails, or the gate trips.
| Member | Signature | Doc |
|---|---|---|
name | name: string | The error name. |
function aiReviewWorkflow(spec: AiReviewWorkflowSpec): CiFile Declare a generated AI-review workflow on the build. The returned CiFile is automatically discovered by discoverCiFiles and kept on disk by syncCiFiles. Default host: "github"; pass "gitlab" or "azure" to target those hosts.
class Pipeline extends Build {
openaiKey = parameter("OpenAI key").secret().env("OPENAI_API_KEY");
review = securityReviewer((r) =>
r.provider("openai").apiKey(this.openaiKey).comment()
);
reviewTarget = target().validateBefore(this.review).executes(() => {});
ghWorkflow = aiReviewWorkflow({ reviewers: [this.review] });
glWorkflow = aiReviewWorkflow({ host: "gitlab", reviewers: [this.review] });
}interface AiReviewWorkflowSpec What to generate — only reviewers is required.
| Member | Signature | Doc |
|---|---|---|
reviewers | reviewers: unknown | The reviewers whose key env vars and .comment() setting drive the generated workflow. Each reviewer's .apiKey(param) parameter becomes an env: entry (or its host equivalent) that maps the secret in; any reviewer with .comment() causes the workflow to grant the right commenting scope and pass the host's token env var. |
host? | host?: CiProvider | The CI host the workflow targets. Defaults to "github". Use "gitlab", "azure", or "bitbucket" to generate the equivalent for those hosts. |
target? | target?: string | The build target the workflow runs. Defaults to "review". |
baseBranch? | baseBranch?: string | The base branch the diff is taken against (used by the GitHub workflow's fetch step). Defaults to "master". |
fetchBase? | fetchBase?: boolean | Emit the git fetch step that makes the base branch available, and point the reviewers at what it fetched. Defaults to true, because a pull-request checkout is shallow and has no base to diff against. Set it to false when the build's review target fetches its own base — then the workflow drops the step, and the reviewers use their own configured base rather than the fetched FETCH_HEAD. Preferable where it applies: the same zuke review then works locally, where no workflow step exists to run. |
hardenRunner? | hardenRunner?: string | The pinned step-security/harden-runner@<sha> to harden the runner with. Supplying this — or checkout — renders the two separate steps instead of the prelude action, because naming an action means those specific actions were asked for. Leave both unset for the default, which is the one action that does both and carries its own pin. |
checkout? | checkout?: string | The pinned actions/checkout@<sha> to check the repository out with. Like hardenRunner, supplying it renders the separate steps. |
pins? | pins?: CiPinResolver | Resolves each action's pinned reference by name, exactly as cicd's own option does. Supply it when the build sources pins from somewhere that stays current. Without it the prelude action falls back to the reference baked into the core this package resolved against — which is a release behind as soon as the action is released again, and silently so: this file would name a different commit from every other generated workflow in the repository, and only a diff would show it. |
path? | path?: string | Output path. Defaults to the host's conventional location. |
name? | name?: string | Workflow name shown in the host's UI. Defaults to "AI Review". |
timeoutMinutes? | timeoutMinutes?: number | Per-job timeout in minutes. Defaults to 15. |
command? | command?: Configure<ReviewCommandSettings> | Run the review on demand when a maintainer comments a command on a pull request — any pull request, a fork's included. GitHub only; see ReviewCommandSettings for the job it adds and the gate it runs behind. ``ts aiReviewWorkflow({ reviewers: [this.security], command: (c) => c.text("@zuke-build review") .secrets("ZUKE_BUILD_APP_ID", "ZUKE_BUILD_APP_KEY"), }); `` |
interface Assessment The structured result of a review.
| Member | Signature | Doc |
|---|---|---|
score | score: number | Overall risk score, 0 (none) to 10 (severe). |
severity | severity: Severity | The overall severity. |
summary | summary: string | A one-line summary of the assessment. |
findings | findings: AssessmentFinding[] | The individual findings. |
interface AssessmentFinding A single issue reported by the model.
| Member | Signature | Doc |
|---|---|---|
title | title: string | A short title for the issue. |
severity | severity: Severity | The issue's severity. |
id? | id?: string | A stable fingerprint for the finding, assigned by the reviewer (see "./suppress.ts".findingFingerprint). Copy it into the suppress list to dismiss a recurring false positive. |
file? | file?: string | The file the issue is in, if the model attributed one. |
line? | line?: number | The line the issue is at, if the model attributed one. |
detail? | detail?: string | A longer explanation, if provided. |
verification? | verification?: confirmed | uncertain | The verify pass's verdict on this finding, when .verify() ran and the verifier answered for it: "confirmed" means the concrete failure path was traced, "uncertain" means the evidence neither confirmed nor refuted it. Both stay reported and gate — only a refutation (which removes the finding and lists it in the report's refuted table) mutes a candidate. Absent when verify did not run or returned no verdict for the finding. |
type AssessmentType = generic | security | secrets | correctness | license The kind of review an assessment performs.
function budget(configure?: Configure<Budget>): Budget Construct a Budget, applying an optional configure lambda so caps can be set inline — e.g. budget((b) => b.maxTokens(100_000).maxCost(1)).
class Budget A running token and cost budget for AI provider calls. Build one with budget, set caps with the fluent setters, then fold each call's usage in with Budget.record_ and gate further calls on Budget.exhausted_.
| Member | Signature | Doc |
|---|---|---|
maxTokens | maxTokens(total: number): this | Cap total tokens (input + output across all recorded calls). |
maxCost | maxCost(usd: number): this | Cap estimated USD cost. Only takes effect for models you've priced via prices — no prices ship by default — so the estimate uses your own current rates. Pair it with maxTokens for a guaranteed hard cap. |
prices | prices(table: Record<string, ModelPrice>): this | Supply per-model prices (USD per 1,000,000 tokens, keyed by model id) so a maxCost cap and the cost estimate can be computed. Merges across calls. Nothing is priced until you call this — provider pricing changes too often to bake a table in, so the rates here are yours to keep current. |
exhausted_ | exhausted_(): boolean | INTERNAL: whether a configured cap has already been reached. |
record_ | record_(usage: Usage | undefined, model: string): void | INTERNAL: fold one provider call's usage into the running totals. |
spend_ | spend_(): BudgetSpend | INTERNAL: a snapshot of consumption so far. |
remainingTokens_ | remainingTokens_(): number | undefined | INTERNAL: remaining tokens before the cap, or undefined when no token cap. |
describe_ | describe_(): string | INTERNAL: a one-line human summary, e.g. "1,234 tokens (~$0.01) of 10,000 / $1.00". |
interface BudgetSpend A snapshot of what a Budget has consumed so far.
| Member | Signature | Doc |
|---|---|---|
calls | calls: number | How many provider calls were recorded. |
inputTokens | inputTokens: number | Total input (prompt) tokens across all recorded calls. |
outputTokens | outputTokens: number | Total output (completion) tokens across all recorded calls. |
totalTokens | totalTokens: number | Total tokens (input + output) across all recorded calls. |
cost? | cost?: number | Estimated USD cost, when at least one recorded call had a known price. |
interface CacheEntry A cached provider response.
| Member | Signature | Doc |
|---|---|---|
text | text: string | The model's raw text response. |
usage? | usage?: Usage | Token usage reported for the original call, if any. |
createdAt | createdAt: number | Epoch milliseconds when the entry was written (for TTL). |
interface CacheStore A pluggable backing store for the cache (the default is file-backed).
| Member | Signature | Doc |
|---|---|---|
get | get(key: string): Promise<CacheEntry | undefined> | Fetch the entry stored under key, or undefined when absent. |
set | set(key: string, entry: CacheEntry): Promise<void> | Store entry under key, replacing any prior value. |
type CommentTokenSource = AnyParameter | string | unknown Where a reviewer's comment-posting token comes from: a secret parameter (for its env var), a literal, or a function that produces the token when a post first needs it — the shape for a token minted by the build itself, such as a GitHub App installation token, so the comments carry the app's identity.
type Confidence = low | medium | high The model's confidence that a fix is correct.
function correctnessReviewer(configure?: Configure<Reviewer>): Reviewer A reviewer that scores the diff for correctness bugs and regressions.
interface DiffFetch The base-branch fetch a DiffSettings.fetchBase requested — the remote to fetch from and the branch (auto-detected from CI when unset).
| Member | Signature | Doc |
|---|---|---|
branch? | branch?: string | The branch to fetch, or undefined to auto-detect it from the CI env. |
remote | remote: string | The remote to fetch from (default "origin"). |
class DiffSettings Fluent diff source configuration passed to "./reviewer.ts".Reviewer.diff.
| Member | Signature | Doc |
|---|---|---|
base | base(ref: string): this | Review the diff against ref (e.g. "origin/main"). |
staged | staged(): this | Review the staged changes (git diff --cached). |
text | text(diff: string): this | Review a diff supplied directly, bypassing git (useful in tests). |
fetchBase | fetchBase(branch?: string, remote?: string): this | Fetch the base branch (a shallow, tag-less git fetch) before diffing, and diff against it — so CI needs no manual git fetch step. With no branch, the base is auto-detected from the CI environment (GitHub's GITHUB_BASE_REF — the pull request's base branch). Honoured by the "./fixer.ts".AiFixer; if the fetch fails it falls back to the working-tree diff. |
text_ | text_(): string | undefined | The literal diff text supplied via DiffSettings.text, if any. |
base_ | base_(): string | undefined | The base ref supplied via DiffSettings.base, if any. |
fetch_ | fetch_(): DiffFetch | undefined | The base-branch fetch requested via DiffSettings.fetchBase, or undefined when none was requested. |
argv_ | argv_(): string[] | The git argv this diff source resolves to. |
class DiscussionSettings Fluent settings for "./reviewer.ts".Reviewer.discussion — who the reviewer listens to on the PR thread, and how much of it the model may see.
| Member | Signature | Doc |
|---|---|---|
trustAssociations | trustAssociations(...associations: string[]): this | Replace the trusted author_association set (default OWNER, MEMBER, COLLABORATOR). Comments from authors outside it (and outside trustAuthors) are dropped before the model sees them. |
trustAuthors | trustAuthors(...logins: string[]): this | Trust these author logins in addition to the association rule — e.g. an outside collaborator whose review the project wants the reviewer to engage with. |
maxCommentTokens | maxCommentTokens(tokens: number): this | Cap the total comment text sent to the model at roughly this many tokens (default 4000), newest comments kept first — so a wall of text cannot crowd the diff and the rubric out of the context window. |
associations_ | associations_(): string[] | INTERNAL: the trusted association set. |
authors_ | authors_(): string[] | INTERNAL: the extra trusted author logins. |
threads | threads(): this | Also anchor each finding to a file/line review thread on the pull request, so a maintainer contests it by replying in that thread instead of quoting its id somewhere on the PR. The summary comment is posted either way and stays the one source of truth: it lists every finding — anchored or not — and carries the state block, so a thread that cannot be posted never hides a finding. The reviewer replies into the thread with the outcome and resolves it once the finding is fixed or dismissed. GitHub only; on other hosts the reviewer notes that and posts the summary alone. |
maxTokens_ | maxTokens_(): number | INTERNAL: the total comment-token cap. |
threads_ | threads_(): boolean | INTERNAL: whether findings are also anchored to review threads. |
type Effort = low | medium | high | xhigh | max The thinking-depth hint passed to providers that support it (Claude).
type EnvReader = unknown Read an environment variable, tolerating an absent --allow-env permission.
interface FileEdit A whole-file edit: the complete new contents of one file.
| Member | Signature | Doc |
|---|---|---|
path | path: string | Repository-relative path of the file to write. |
content | content: string | The complete new contents of the file (not a patch). |
function findingFingerprint(assessment: AssessmentType, finding: AssessmentFinding): string A stable fingerprint for a finding: hash of the assessment kind, the normalised title (trimmed, lowercased, whitespace collapsed), and the file. Independent of line number so a finding keeps its id as code shifts.
interface Fix The structured result of a fix attempt.
| Member | Signature | Doc |
|---|---|---|
diagnosis | diagnosis: string | A one-line explanation of what failed and why. |
rootCause | rootCause: string | The underlying root cause the fix addresses. |
confidence | confidence: Confidence | The model's confidence that the edits resolve the failure. |
locations | locations: FixLocation[] | The specific code locations the fix targets, with verbatim source. |
edits | edits: FileEdit[] | The whole-file edits that, applied together, should fix the failure. |
interface FixLocation A specific code location the fix targets: the exact offending source quoted verbatim, its file and line(s), and the suggested replacement. Rendered as a diff in the report so the comment shows real code, not just prose.
| Member | Signature | Doc |
|---|---|---|
file | file: string | Repository-relative path of the file. |
line | line: number | The 1-based line where the offending code starts. |
endLine? | endLine?: number | The 1-based line where it ends, when it spans more than one line. |
code | code: string | The exact offending source line(s), quoted verbatim. |
suggestion? | suggestion?: string | The suggested replacement for code (empty means delete it). |
type GateRule = unknown | unknown A configured rule for "./reviewer.ts".Reviewer.failWhen.
class GateSettings Fluent gate configuration passed to "./reviewer.ts".Reviewer.failWhen.
| Member | Signature | Doc |
|---|---|---|
scoreAbove | scoreAbove(value: number): this | Fail when the assessed risk score is strictly above value (0–10). |
severityAtLeast | severityAtLeast(value: Severity): this | Fail when the overall severity is at least value. |
rules_ | rules_(): GateRule[] | The configured gate rules, in the order they were added. |
function genericReviewer(configure?: Configure<Reviewer>): Reviewer A general-purpose reviewer scored on code quality and maintainability. Pair with .criteria(...) to add project-specific notes (idioms, conventions, a coding-style document); the built-in rubric is sufficient without them.
function licenseReviewer(configure?: Configure<Reviewer>): Reviewer A reviewer that scores the diff for license and compliance risk.
interface ModelPrice Per-model price in USD per 1,000,000 tokens.
| Member | Signature | Doc |
|---|---|---|
input | input: number | USD per 1,000,000 input (prompt) tokens. |
output | output: number | USD per 1,000,000 output (completion) tokens. |
type Provider = claude | openai | gemini A supported model provider.
interface RetryInfo What happened before a retry, for RetryOptions.onRetry.
| Member | Signature | Doc |
|---|---|---|
attempt | attempt: number | The attempt that just failed (1-based). |
attempts | attempts: number | The total number of attempts that will be made. |
delayMs | delayMs: number | How long the helper will wait before the next attempt, in milliseconds. |
reason | reason: string | Why the attempt failed — e.g. "HTTP 503" or "timed out after 60000ms". |
interface RetryOptions Configurable knobs for retryingFetch.
| Member | Signature | Doc |
|---|---|---|
attempts? | attempts?: number | Total attempts (first try + retries). Defaults to DEFAULT_ATTEMPTS. |
baseDelayMs? | baseDelayMs?: number | Backoff for the first retry; doubles each subsequent retry. |
timeoutMs? | timeoutMs?: number | Per-attempt timeout in milliseconds (default 60s). 0 disables it. |
onRetry? | onRetry?: unknown | Invoked before each retry, so a caller can report progress. |
sleep? | sleep?: unknown | Sleep seam — overridden in tests so retries don't take real time. |
class ReviewCommandSettings The comment command that runs the review on demand, configured through AiReviewWorkflowSpec.command.
The job it adds runs on issue_comment, which GitHub delivers for comments on pull requests too, always from the default branch and always with the repository's secrets — so its if: is the whole access control. It fires only when the comment is on a pull request, starts with text, was written by a human (not a bot account), and its author's author_association is OWNER, MEMBER or COLLABORATOR. The comment body is matched in the expression and never interpolated into a run: line. Then, before the review runs, the job asks the collaborators API whether the commenter has push access, and stops if not — an association alone admits read-only members and collaborators.
What runs is the default branch's build, never the pull request's: the job passes ZUKE_REVIEW_PR, and the reviewers fetch that pull request's merge as data and diff it. That is what makes the flow safe for a fork — the code under review is read, not executed — and it is why a pull request cannot change the rules it is judged by from this flow: its zuke.ts, suppressions and criteria never load. The maintainer's comment is the human gate, as it is for Dependabot's @dependabot commands.
| Member | Signature | Doc |
|---|---|---|
text | text(command: string): this | The command, e.g. "@zuke-build review". Matched case-insensitively at the start of the comment, so a reply that quotes it (> @zuke-build review) does not start a run. Letters, digits, @/_.:- and single spaces only. |
secrets | secrets(...names: string[]): this | Pass these repository secrets to the command job's review step as env vars of the same name — e.g. the GitHub App credentials the build mints its commentToken from, so the review posts as the app. Only the command job receives them; the pull_request job is unchanged. |
text_? | text_?: string | The command a comment must start with. Set by text. |
secrets_ | secrets_: string[] | Secrets the command job's review step receives beyond the reviewers' own keys and the host token. Set by secrets. |
class Reviewer A fluent AI reviewer. Construct one via securityReviewer (and the sibling factories), configure it, and attach it to a target with .validateBefore(...) / .validateAfter(...). .provider(...) and .apiKey(...) are required; everything else has a default.
| Member | Signature | Doc |
|---|---|---|
provider_ | provider_(): Provider | undefined | The model provider, once .provider(...) has been called. |
apiKey_ | apiKey_(): AnyParameter | string | undefined | The configured API key (a parameter — for its env var — or a literal). |
commentEnabled_ | commentEnabled_(): boolean | Whether .comment() is set — i.e. this reviewer posts to the PR. |
commentToken_ | commentToken_(): CommentTokenSource | undefined | The configured comment-posting token, if .commentToken(...) was called. |
provider | provider(provider: Provider): this | Set the model provider (required). |
apiKey | apiKey(apiKey: AnyParameter | string): this | Set the API key, from a secret parameter or a literal string (required). |
model | model(model: string): this | Override the model (default: the provider's recommended model). |
effort | effort(effort: Effort): this | Set the thinking-effort hint (honoured by Claude; ignored elsewhere). |
criteria | criteria(criteria: string): this | Optional project-specific notes appended above the diff in the user prompt — framing that fine-tunes the built-in rubric (e.g. "strict TypeScript, no any/as"). Works for every reviewer; the assessment's own system prompt already covers what to look for, so this is purely additive. These notes are read from the head under review, and cannot be anything else: they are build code, evaluated by the very build the pull request changed, so there is no base copy to read short of running the base's build. A branch can therefore widen what its own review overlooks, which is real but bounded — the change is first-party code in the diff a maintainer reads, and a build that could not configure its own reviewer from its own source would be a different feature. Reviewer.criteriaFile is the base-anchored form for notes that should not be editable by the change they judge. |
diff | diff(configure: Configure<DiffSettings>): this | Configure the diff source (default: the working-tree diff, git diff). |
include | include(...globs: string[]): this | Only review files matching these globs (default: all files). |
exclude | exclude(...globs: string[]): this | Exclude files matching these globs (in addition to lockfiles). |
maxDiffTokens | maxDiffTokens(tokens: number): this | Cap the diff at roughly this many tokens, truncating the rest. |
failWhen | failWhen(configure: Configure<GateSettings>): this | Choose the gate that breaks the build (default: score above 7). |
onError | onError(mode: fail | warn): this | What to do when the review itself fails (API error, refusal, bad JSON): "fail" breaks the build (default), "warn" logs and passes. |
retry | retry(options?: RetryOptions): this | Retry the provider call on transient failures (HTTP 408/429/500/502/503/ 504 and network errors). The default is on — three attempts with exponential backoff and Retry-After honoured. Pass an object to override: { attempts: 5 } to retry more, or { attempts: 1 } to disable. |
skipIfKeyMissing | skipIfKeyMissing(): this | Skip the review (instead of failing) when the API key is missing — handy when the key is a CI-only secret. The skip is announced on the console and in the job summary so the gap is visible. |
comment | comment(mode?: update | append): this | Also post the review to the pull/merge request as a comment. Works on every supported CI host — GitHub Actions, GitLab CI, Azure Pipelines, Bitbucket Pipelines — dispatched at runtime by detectCiHost. A no-op outside a PR context (e.g. local runs). On each host the workflow must grant the right scope: GitHub pull-requests: write, GitLab a token with the api scope, Azure System.AccessToken, Bitbucket an app password. mode chooses how re-runs post: "update" (default) keeps a single comment per reviewer, edited in place; "append" posts a fresh comment every run, so earlier assessments — and their finding ids — stay on the thread as history. The discussion feature works with both: its state block rides on every comment, and the newest one is read back. |
commentToken | commentToken(token: CommentTokenSource): this | The token used to post the PR/MR comment. Defaults to the active host's conventional env var: GITHUB_TOKEN (GitHub), GITLAB_TOKEN (GitLab), SYSTEM_ACCESSTOKEN (Azure), BITBUCKET_TOKEN (Bitbucket). A function is called each time a post needs the token — for a token that does not exist until the build mints it, such as a GitHub App installation token narrowed to pull_requests: write, which makes the comments the app's rather than github-actions[bot]'s. A function that mints should remember its result, since a review posts more than once. The generated workflow cannot name a secret for a function, so it passes the host's default token alongside, for the function to fall back to. |
githubToken | githubToken(token: CommentTokenSource): this | Backwards-compatible alias for commentToken. |
quiet | quiet(): this | Suppress the findings printout and the job-summary section. |
fetch | fetch(impl: fetch): this | The fetch implementation for the API call (test seam). |
exec | exec(run: unknown): this | The git runner used to produce the diff (test seam). |
env | env(reader: EnvReader): this | Environment reader used to auto-detect the base branch for "./diff.ts".DiffSettings.fetchBase (reads GITHUB_BASE_REF). A test seam; defaults to the process environment. |
budget | budget(budget: Budget): this | Attach a shared Budget that caps spend by an exact token count (a USD cap is opt-in, computed from prices you supply to the budget). Pass the same budget to several reviewers and a fixer to bound the whole build: once the cap is reached, further reviews are skipped (not failed) with a note, rather than running up the bill. |
cache | cache(cache: AiCache): this | Reuse a prior model response for an identical review (same provider, model, and prompt) instead of calling the API again — see AiCache. A cache hit costs nothing and does not draw down the budget. |
suppress | suppress(suppressions: Suppressions): this | Hide findings whose stable ID is in a Suppressions list — a learned set of dismissed false positives. Every finding is fingerprinted and its ID surfaced in the report, so dismissing one is a copy-paste into the list. Like Reviewer.criteria, the list is read at the head under review — inline entries are build code, and the file is read from disk — so a change can suppress a finding on its own run. That is deliberate: every suppressed finding is still listed in the report, so the muting is visible on the thread rather than silent, and the entry is in the diff a maintainer reads. Nothing here reads the base, and nothing should be assumed to. |
conventionsFile | conventionsFile(path: string, maxTokens?: number): this | Feed the project's conventions document (e.g. AGENTS.md) to the model as reference material, so the review judges the change against the project's documented rules instead of generic taste. When the diff has a base ref (.diff((d) => d.base(...)) or a successful .fetchBase()), the file is read from that base via git show — never from the head under review, so a pull request cannot rewrite the rules it is judged by. Without a base (a local working-tree review) it is read from disk. Truncated at roughly maxTokens (default 8000). |
criteriaFile | criteriaFile(path: string, maxTokens?: number): this | Project-specific notes read from a file, appended to whatever Reviewer.criteria set inline — the base-anchored half of the same slot. Read exactly as Reviewer.conventionsFile is: from the diff's base ref via git show when there is one, from disk otherwise, truncated at roughly maxTokens (default 8000). That is the point of it. A note that tells the reviewer a design is accepted — and so suppresses the findings that restate it — is a rule the review is judged by, and a pull request should not be able to add one to its own run. Keeping it in a file the base supplies costs one merge of lag, which is the same lag the conventions document already accepts, and is arguably the point: a newly accepted design should be agreed before it starts muting findings. Use it for what a branch should not be able to widen; keep Reviewer.criteria for the framing that travels with the build. What it does not buy: the build still decides *whether* to read a criteria document at all, and that decision is head code, so a branch can drop the call as easily as it can edit a string. This bounds what a change can add to the rules of its own review, not whether the reviewer is configured — the same limit Reviewer.conventionsFile has, and the reason the diff a maintainer reads remains the control that matters. |
fileContext | fileContext(maxTokens?: number): this | Also send the full post-image contents of the changed files (read via git show HEAD:<path>), bounded at roughly maxTokens (default 12000) — so the model can check a finding against the surrounding code (an existing guard, a validation a few lines away) instead of judging hunks in isolation. Skipped silently for a literal .diff((d) => d.text(...)) source with no repository behind it. |
verify | verify(): this | Add an adversarial verification pass: after the review produces candidate findings, a second model call re-checks each against the diff (and the fileContext, when enabled) and refutes any whose failure path it cannot concretely trace. Refuted candidates are listed in the report but neither posted as findings nor gated on. Costs one extra API call per review with findings; if the pass itself errors, the unverified findings are kept (fail toward reporting, never toward silence). |
discussion | discussion(configure?: Configure<DiscussionSettings>): this | Engage with the pull-request discussion instead of repeating findings: the reviewer reads the PR's comments, and when a trusted commenter (by the host's own author metadata — see DiscussionSettings) contests a finding by quoting its ID, an adjudication pass weighs the rebuttal on technical merit and either upholds the finding (with the gap named) or dismisses it. Dismissals persist across runs in a state block inside the reviewer's own PR comment, so a dismissed finding — or a rewording of it — does not resurface without new evidence. Requires comment (the comment is where state lives) and a host that can list comments (GitHub currently); elsewhere the discussion is skipped with a console note. Untrusted comments are dropped in code before any prompt is built — the model never sees them, so a drive-by "the maintainer approved this" comment cannot influence the review. |
validate | validate(context: ValidationContext): Promise<void> | Run the review and gate the build. Throws an AiReviewError when the gate trips (or on a configuration/API error with onError: "fail"). |
name | name: string | A name for diagnostics — "<assessment> review". |
type RunScope = local | ci | both Where a fixer may run, set with .runOnly(...).
- "local" — apply on a developer's machine, refuse on CI. The default, and what a fixer has when .runOnly(...) is never called. - "ci" — apply on CI, and do not run at all off it. The scope a repository's own build wants: heal a pull request without ever rewriting a working tree someone is editing. - "both" — apply on either host. What .allowCI() selects.
"On CI" means any CI system, not only the four Zuke names it can generate pipelines for: isCI recognises GitHub Actions, GitLab CI, Azure Pipelines and Bitbucket Pipelines by their own variables, and Jenkins, Buildkite, CircleCI, Travis, TeamCity and the generic CI convention by theirs.
Reading one of those as a developer's machine would fail in the dangerous direction for both of the other scopes, which is why the broader test is the right one: "ci" would silently skip every run, and "local" — the default — would apply and commit changes to what it believed was a working tree someone was editing. Jenkins is the case that matters most, since it sets none of the four and does not set CI either.
function secretsReviewer(configure?: Configure<Reviewer>): Reviewer A reviewer that scans the diff for leaked secrets and credentials.
function securityReviewer(configure?: Configure<Reviewer>): Reviewer A reviewer that scores the diff for security vulnerabilities.
type Severity = none | low | medium | high | critical A severity level, ordered none < low < medium < high < critical.
function suppressions(configure?: Configure<Suppressions>): Suppressions Construct a Suppressions, applying an optional configure lambda so the file and inline fingerprints can be set inline — e.g. suppressions((s) => s.file(".zuke/suppress.json").add("abc")).
class Suppressions A file-backed set of suppressed finding fingerprints. The effective set is the union of the fingerprints read from Suppressions.file and any added inline with Suppressions.add; the reviewer drops a finding whose findingFingerprint is in that set.
| Member | Signature | Doc |
|---|---|---|
file | file(path: string): this | Path of the JSON suppress list (default ".zuke/ai-suppress.json"). |
add | add(...fingerprints: string[]): this | Add fingerprints inline (in addition to any from the file). |
reader | reader(read: unknown): this | Reader seam for the suppress file (default reads from disk, missing -> undefined). |
load_ | load_(): Promise<Set<string>> | INTERNAL: the effective set of suppressed fingerprints (file ∪ inline). |
interface Usage Token counts a provider reported for a review call, when the response carries them. Each field is optional because not every provider reports every count.
| Member | Signature | Doc |
|---|---|---|
inputTokens? | inputTokens?: number | Tokens in the prompt / input. |
outputTokens? | outputTokens?: number | Tokens in the model's output / completion. |
totalTokens? | totalTokens?: number | Total tokens, reported by the provider or derived from input + output. |