diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index dcf32685f..65791ee40 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -26,7 +26,11 @@ jobs: review: name: Review if: > - (github.event_name == 'pull_request' && github.event.pull_request.draft == false) + (github.event_name == 'pull_request' + && github.event.pull_request.draft == false + && github.event.pull_request.user.login != 'AlemTuzlak' + && github.event.pull_request.user.login != 'tombeckenham' + && github.event.pull_request.user.login != 'jherr') || github.event_name == 'workflow_dispatch' || (github.event_name == 'issue_comment' && github.event.issue.pull_request @@ -44,7 +48,7 @@ jobs: - name: Setup Tools uses: TanStack/config/.github/setup@190f659075ff0845850e330883eb26d7ffd0671f # main - name: Build packages - run: pnpm exec nx run @tanstack/ai-grok:build + run: pnpm exec nx run @tanstack/ai-grok-build:build - name: Add PR head worktree env: AI_REVIEW_TOKEN: ${{ secrets.AI_REVIEW_TOKEN }} diff --git a/agent-scripts/ai-review/README.md b/agent-scripts/ai-review/README.md index 8269bf8f6..2d83dd41e 100644 --- a/agent-scripts/ai-review/README.md +++ b/agent-scripts/ai-review/README.md @@ -1,6 +1,6 @@ # Grok PR review bot -A GitHub Action that reviews open pull requests with TanStack AI (`chat()` + `grokText('grok-4.6')` + high reasoning). It comments, sets one `ai-*` label, and can push listed polish commits. +A GitHub Action that reviews open pull requests with TanStack AI (`chat()` + `grokBuildText('grok-4.6')`). It comments, sets one `ai-*` label, and can push listed polish commits. The first lines of every bot comment say the comment is automated. It is not a maintainer review. The bot never GitHub-approves and never merges. @@ -15,11 +15,13 @@ Until both secrets exist, the job fails with `missing AI_REVIEW_TOKEN or XAI_API ## How a run starts -- Auto: `pull_request` opened, synchronize, or ready_for_review (non-draft). +- Auto: `pull_request` opened, synchronize, or ready_for_review, when the PR is not a draft. +- Auto does not start when the PR author is AlemTuzlak, tombeckenham, or jherr. +- Keep those logins in sync with `.github/maintainers.json`. GitHub then shows a skipped check, not a cancelled check. - Manual: Actions `workflow_dispatch` with a PR number. - Manual: a login in `.github/maintainers.json` comments `/ai-review` on the PR. -Auto skips drafts, bot PRs, the machine user's own head commit, and a head SHA this bot already reviewed. Manual still runs on those, except it never executes PR code. +Auto also skips drafts, bot PRs, roster-maintainer PRs, the machine user's own head commit, and a head SHA this bot already reviewed. Manual still runs on those. The bot never executes PR code. ## Labels @@ -46,6 +48,7 @@ Open the **AI review** workflow log. Common causes: - Missing `AI_REVIEW_TOKEN` or `XAI_API_KEY` - Fork with maintainer edits off (comment is posted, label is `ai-needs-work`, no push) - `chat()` did not return a valid verdict object +- Workspace setup failed to install the Grok CLI ## Layout diff --git a/agent-scripts/ai-review/run.test.ts b/agent-scripts/ai-review/run.test.ts index 126ade4b1..806efb98a 100644 --- a/agent-scripts/ai-review/run.test.ts +++ b/agent-scripts/ai-review/run.test.ts @@ -23,6 +23,7 @@ function samplePull( overrides: { draft?: boolean maintainer_can_modify?: boolean + login?: string } = {}, ) { return { @@ -31,7 +32,7 @@ function samplePull( body: 'Handle empty messages.', html_url: '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/TanStack/ai/pull/42', draft: overrides.draft ?? false, - user: { login: 'alice' }, + user: { login: overrides.login ?? 'alice' }, head: { sha: SHA, ref: HEAD_REF, @@ -293,6 +294,16 @@ describe('runReviewJob', () => { expect(gitCalls).toEqual([]) }) + it('skips an auto run from a roster maintainer and does not post a comment', async () => { + const { result, comments, gitCalls } = await runJob({ + pull: samplePull({ login: 'alem' }), + }) + + expect(result).toEqual({ skipped: true, reason: 'maintainer-author' }) + expect(comments).toEqual([]) + expect(gitCalls).toEqual([]) + }) + it('skips an issue_comment that is not the /ai-review command', async () => { const { result, comments, gitCalls } = await runJob({ eventName: 'issue_comment', diff --git a/agent-scripts/ai-review/run.ts b/agent-scripts/ai-review/run.ts index 606432b33..031060772 100644 --- a/agent-scripts/ai-review/run.ts +++ b/agent-scripts/ai-review/run.ts @@ -3,12 +3,24 @@ */ import { spawn } from 'node:child_process' +import { homedir } from 'node:os' import { readFile } from 'node:fs/promises' -import { resolve } from 'node:path' +import { join, resolve } from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' import { chat } from '@tanstack/ai' -import { grokText } from '@tanstack/ai-grok' +import { + GROK_CLI_INSTALL_COMMAND, + grokBuildText, +} from '@tanstack/ai-grok-build' +import { + createSecrets, + defineSandbox, + defineWorkspace, + localSource, + withSandbox, +} from '@tanstack/ai-sandbox' +import { localProcessSandbox } from '@tanstack/ai-sandbox-local-process' import { loadConfig, isRosterMaintainer, @@ -119,21 +131,44 @@ type ReviewInput = { /** * Production Grok review step for `runReviewJob`. * - * Tools run first (read/edit files). Then `outputSchema` returns the verdict. - * Do not call this from unit tests. + * `grokBuildText` streams tools first, then a `structured-output.complete` + * event. Do not call this from unit tests. */ export function createGrokReview() { return async (input: ReviewInput) => { + const xaiKey = process.env.XAI_API_KEY + const sandbox = defineSandbox({ + id: 'ai-review', + provider: localProcessSandbox({ + dir: input.worktreeRoot, + removeOnDestroy: false, + }), + workspace: defineWorkspace({ + source: localSource(input.worktreeRoot), + setup: ({ serial }) => serial(GROK_CLI_INSTALL_COMMAND), + ...(xaiKey !== undefined && xaiKey.length > 0 + ? { secrets: createSecrets({ XAI_API_KEY: xaiKey }) } + : {}), + }), + lifecycle: { reuse: 'none', destroyOnComplete: false }, + }) const result = await chat({ - adapter: grokText('grok-4.6'), - modelOptions: { reasoning: { effort: 'high' } }, + adapter: grokBuildText('grok-4.6', { + authMode: 'api-key', + protocol: 'streaming-json', + cwd: input.worktreeRoot, + grokExecutable: join(homedir(), '.grok', 'bin', 'grok'), + }), tools: createReviewTools({ worktreeRoot: input.worktreeRoot }), outputSchema: reviewVerdictSchema, + middleware: [withSandbox(sandbox)], + threadId: `ai-review-${input.pr.number}`, messages: [ { role: 'user', content: [ 'Review this pull request.', + 'Read the changed source files before you choose a verdict.', 'If it is a bug fix and does not fix the claimed root cause, verdict is reject.', 'If it is useful and needs listed bug or suggestion edits, apply those with edit_file, then verdict polish.', 'If it is useful and clean, verdict is ready.', diff --git a/agent-scripts/ai-review/skip.test.ts b/agent-scripts/ai-review/skip.test.ts index 41226755f..5c027f054 100644 --- a/agent-scripts/ai-review/skip.test.ts +++ b/agent-scripts/ai-review/skip.test.ts @@ -49,6 +49,13 @@ describe('shouldSkip', () => { }) }) + it('skips a roster maintainer PR author', () => { + expect(reviewSkip({ authorLogin: 'alem' })).toEqual({ + skip: true, + reason: 'maintainer-author', + }) + }) + it('skips when the head commit is from the machine user', () => { expect(reviewSkip({ headCommitAuthorLogin: 'TanStack-AI-Bot' })).toEqual({ skip: true, @@ -100,6 +107,15 @@ describe('shouldSkip', () => { ).toEqual({ skip: true, reason: 'bot-author' }) }) + it('prefers maintainer-author over bot-head-commit', () => { + expect( + reviewSkip({ + authorLogin: 'alem', + headCommitAuthorLogin: MACHINE, + }), + ).toEqual({ skip: true, reason: 'maintainer-author' }) + }) + it('prefers bot-head-commit over same-sha', () => { expect( reviewSkip({ @@ -111,12 +127,12 @@ describe('shouldSkip', () => { }) describe('manual', () => { - it('never skips for draft, bot author, bot head commit, or same SHA', () => { + it('never skips for draft, bot author, maintainer author, bot head commit, or same SHA', () => { expect( reviewSkip({ mode: 'manual', isDraft: true, - authorLogin: 'renovate', + authorLogin: 'alem', headCommitAuthorLogin: MACHINE, alreadyReviewedSha: SHA, }), diff --git a/agent-scripts/ai-review/skip.ts b/agent-scripts/ai-review/skip.ts index 1362a5445..d016449f8 100644 --- a/agent-scripts/ai-review/skip.ts +++ b/agent-scripts/ai-review/skip.ts @@ -1,4 +1,7 @@ -import { isBotLogin } from '../../scripts/maintainer/config.ts' +import { + isBotLogin, + isRosterMaintainer, +} from '../../scripts/maintainer/config.ts' import type { ToolsetConfig } from '../../scripts/maintainer/types.ts' type SkipInput = { @@ -15,8 +18,9 @@ type SkipInput = { /** * Decide whether this AI review run should skip, and why. * - * Auto mode skips drafts, bot PR authors, machine-user head commits, and a - * head SHA that was already reviewed, in that order. Manual mode never skips. + * Auto mode skips drafts, bot PR authors, roster maintainers, machine-user + * head commits, and a head SHA that was already reviewed, in that order. + * Manual mode never skips. */ export function shouldSkip(input: SkipInput) { if (input.mode === 'manual') { @@ -31,6 +35,10 @@ export function shouldSkip(input: SkipInput) { return { skip: true, reason: 'bot-author' } } + if (isRosterMaintainer(input.authorLogin, input.config)) { + return { skip: true, reason: 'maintainer-author' } + } + const isBotHeadCommit = input.headCommitAuthorLogin !== null && input.headCommitAuthorLogin.toLowerCase() === diff --git a/package.json b/package.json index b5087bf7e..77d9e58bd 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,9 @@ "@faker-js/faker": "^10.1.0", "@stylistic/eslint-plugin": "^5.10.0", "@tanstack/ai": "workspace:*", - "@tanstack/ai-grok": "workspace:*", + "@tanstack/ai-grok-build": "workspace:*", + "@tanstack/ai-sandbox": "workspace:*", + "@tanstack/ai-sandbox-local-process": "workspace:*", "@tanstack/typedoc-config": "0.3.4", "@tanstack/vite-config": "0.6.0", "@types/node": "^24.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76f8cf462..a05386a96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,9 +34,15 @@ importers: '@tanstack/ai': specifier: workspace:* version: link:packages/ai - '@tanstack/ai-grok': + '@tanstack/ai-grok-build': + specifier: workspace:* + version: link:packages/ai-grok-build + '@tanstack/ai-sandbox': + specifier: workspace:* + version: link:packages/ai-sandbox + '@tanstack/ai-sandbox-local-process': specifier: workspace:* - version: link:packages/ai-grok + version: link:packages/ai-sandbox-local-process '@tanstack/typedoc-config': specifier: 0.3.4 version: 0.3.4