From d1aba65cb03263030de804e580646625f614e0cc Mon Sep 17 00:00:00 2001 From: mkorwel Date: Fri, 13 Mar 2026 18:40:14 -0700 Subject: [PATCH] feat(deep-review): allow choice between pre-existing and isolated instances for Gemini and GH CLI --- .gemini/skills/deep-review/policy.toml | 148 ++++++++++++++++++ .../skills/deep-review/scripts/entrypoint.ts | 16 +- .gemini/skills/deep-review/scripts/review.ts | 24 +-- .gemini/skills/deep-review/scripts/setup.ts | 76 +++++++-- .gemini/skills/deep-review/scripts/worker.ts | 26 ++- 5 files changed, 237 insertions(+), 53 deletions(-) create mode 100644 .gemini/skills/deep-review/policy.toml diff --git a/.gemini/skills/deep-review/policy.toml b/.gemini/skills/deep-review/policy.toml new file mode 100644 index 0000000000..dd26fd772c --- /dev/null +++ b/.gemini/skills/deep-review/policy.toml @@ -0,0 +1,148 @@ +# --- CORE TOOLS --- +[[rule]] +toolName = "read_file" +decision = "allow" +priority = 100 + +[[rule]] +toolName = "write_file" +decision = "allow" +priority = 100 + +[[rule]] +toolName = "grep_search" +decision = "allow" +priority = 100 + +[[rule]] +toolName = "glob" +decision = "allow" +priority = 100 + +[[rule]] +toolName = "list_directory" +decision = "allow" +priority = 100 + +[[rule]] +toolName = "codebase_investigator" +decision = "allow" +priority = 100 + +# --- SHELL COMMANDS --- + +# Git (Safe/Read-only) +[[rule]] +toolName = "run_shell_command" +commandPrefix = [ + "git blame", + "git show", + "git grep", + "git show-ref", + "git ls-tree", + "git ls-remote", + "git reflog", + "git remote -v", + "git diff", + "git rev-list", + "git rev-parse", + "git merge-base", + "git cherry", + "git fetch", + "git status", + "git st", + "git branch", + "git br", + "git log", + "git --version" +] +decision = "allow" +priority = 100 + +# GitHub CLI (Read-only) +[[rule]] +toolName = "run_shell_command" +commandPrefix = [ + "gh workflow list", + "gh auth status", + "gh checkout view", + "gh run view", + "gh run job view", + "gh run list", + "gh run --help", + "gh issue view", + "gh issue list", + "gh label list", + "gh pr diff", + "gh pr check", + "gh pr checks", + "gh pr view", + "gh pr list", + "gh pr status", + "gh repo view", + "gh job view", + "gh api", + "gh log" +] +decision = "allow" +priority = 100 + +# Node.js/NPM (Generic Tests, Checks, and Build) +[[rule]] +toolName = "run_shell_command" +commandPrefix = [ + "npm run start", + "npm install", + "npm run", + "npm test", + "npm ci", + "npm list", + "npm --version" +] +decision = "allow" +priority = 100 + +# Core Utilities (Safe) +[[rule]] +toolName = "run_shell_command" +commandPrefix = [ + "sleep", + "env", + "break", + "xargs", + "base64", + "uniq", + "sort", + "echo", + "which", + "ls", + "find", + "tail", + "head", + "cat", + "cd", + "grep", + "ps", + "pwd", + "wc", + "file", + "stat", + "diff", + "lsof", + "date", + "whoami", + "uname", + "du", + "cut", + "true", + "false", + "readlink", + "awk", + "jq", + "rg", + "less", + "more", + "tree" +] +decision = "allow" +priority = 100 diff --git a/.gemini/skills/deep-review/scripts/entrypoint.ts b/.gemini/skills/deep-review/scripts/entrypoint.ts index b832e5878a..2cc01161e3 100644 --- a/.gemini/skills/deep-review/scripts/entrypoint.ts +++ b/.gemini/skills/deep-review/scripts/entrypoint.ts @@ -12,11 +12,12 @@ import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const prNumber = process.argv[2]; const branchName = process.argv[3]; +const policyPath = process.argv[4]; const ISOLATED_CONFIG = process.env.GEMINI_CLI_HOME || path.join(process.env.HOME || '', '.gemini-deep-review'); async function main() { - if (!prNumber || !branchName) { - console.error('Usage: tsx entrypoint.ts '); + if (!prNumber || !branchName || !policyPath) { + console.error('Usage: tsx entrypoint.ts '); process.exit(1); } @@ -27,9 +28,9 @@ async function main() { const tsxBin = path.join(workDir, 'node_modules/.bin/tsx'); const geminiBin = path.join(workDir, 'node_modules/.bin/gemini'); - // 1. Run the Parallel Worker + // 1. Run the Parallel Reviewer console.log('šŸš€ Launching Parallel Review Worker...'); - const workerResult = spawnSync(tsxBin, [path.join(__dirname, 'worker.ts'), prNumber, branchName], { + const workerResult = spawnSync(tsxBin, [path.join(__dirname, 'worker.ts'), prNumber, branchName, policyPath], { stdio: 'inherit', env: { ...process.env, GEMINI_CLI_HOME: ISOLATED_CONFIG } }); @@ -41,12 +42,7 @@ async function main() { // 2. Launch the Interactive Gemini Session (Local Nightly) console.log('\n✨ Verification complete. Joining interactive session...'); - // Use the mirrored policy if available - const policyFile = path.join(ISOLATED_CONFIG, 'policies/policy.toml'); - const geminiArgs = []; - if (fs.existsSync(policyFile)) { - geminiArgs.push('--policy', policyFile); - } + const geminiArgs = ['--policy', policyPath]; geminiArgs.push('-p', `Review for PR #${prNumber} is complete. Read the logs in .gemini/logs/review-${prNumber}/ and synthesize your findings.`); process.chdir(targetDir); diff --git a/.gemini/skills/deep-review/scripts/review.ts b/.gemini/skills/deep-review/scripts/review.ts index 70682d0482..a91125d3cb 100644 --- a/.gemini/skills/deep-review/scripts/review.ts +++ b/.gemini/skills/deep-review/scripts/review.ts @@ -38,7 +38,7 @@ async function main() { config = settings.maintainer.deepReview; } - const { remoteHost, remoteWorkDir, terminalType, syncAuth } = config; + const { remoteHost, remoteWorkDir, terminalType, syncAuth, geminiSetup, ghSetup } = config; console.log(`šŸ” Fetching metadata for PR #${prNumber}...`); const ghView = spawnSync('gh', ['pr', 'view', prNumber, '--json', 'headRefName', '-q', '.headRefName'], { shell: true }); @@ -50,11 +50,17 @@ async function main() { const sessionName = `${prNumber}-${branchName.replace(/[^a-zA-Z0-9]/g, '_')}`; - // 2. Sync Configuration Mirror (Isolated Profile) - const ISOLATED_CONFIG = '~/.gemini-deep-review'; - console.log(`šŸ“” Mirroring environment to ${remoteHost}...`); - spawnSync('ssh', [remoteHost, `mkdir -p ${remoteWorkDir}/.gemini/skills/deep-review/scripts/ ${ISOLATED_CONFIG}/policies/`]); + // 2. Sync Configuration Mirror (Isolated Profiles) + const ISOLATED_GEMINI = geminiSetup === 'isolated' ? '~/.gemini-deep-review' : '~/.gemini'; + const ISOLATED_GH = ghSetup === 'isolated' ? '~/.gh-deep-review' : '~/.config/gh'; + const remotePolicyPath = `${ISOLATED_GEMINI}/policies/deep-review-policy.toml`; + console.log(`šŸ“” Mirroring environment to ${remoteHost}...`); + spawnSync('ssh', [remoteHost, `mkdir -p ${remoteWorkDir}/.gemini/skills/deep-review/scripts/ ${ISOLATED_GEMINI}/policies/`]); + + // Sync the policy file specifically + spawnSync('rsync', ['-avz', path.join(REPO_ROOT, '.gemini/skills/deep-review/policy.toml'), `${remoteHost}:${remotePolicyPath}`]); + spawnSync('rsync', ['-avz', '--delete', path.join(REPO_ROOT, '.gemini/skills/deep-review/scripts/'), `${remoteHost}:${remoteWorkDir}/.gemini/skills/deep-review/scripts/`]); if (syncAuth) { @@ -63,19 +69,17 @@ async function main() { const syncFiles = ['google_accounts.json', 'settings.json']; for (const f of syncFiles) { const lp = path.join(localGeminiDir, f); - if (fs.existsSync(lp)) spawnSync('rsync', ['-avz', lp, `${remoteHost}:${ISOLATED_CONFIG}/${f}`]); + if (fs.existsSync(lp)) spawnSync('rsync', ['-avz', lp, `${remoteHost}:${ISOLATED_GEMINI}/${f}`]); } const localPolicies = path.join(localGeminiDir, 'policies/'); - if (fs.existsSync(localPolicies)) spawnSync('rsync', ['-avz', '--delete', localPolicies, `${remoteHost}:${ISOLATED_CONFIG}/policies/`]); + if (fs.existsSync(localPolicies)) spawnSync('rsync', ['-avz', '--delete', localPolicies, `${remoteHost}:${ISOLATED_GEMINI}/policies/`]); const localEnv = path.join(REPO_ROOT, '.env'); if (fs.existsSync(localEnv)) spawnSync('rsync', ['-avz', localEnv, `${remoteHost}:${remoteWorkDir}/.env`]); } // 3. Construct Clean Command - // We use a single entrypoint script to avoid quoting nightmares const envLoader = 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh"'; - // Use the locally installed tsx binary - const entryCmd = `cd ${remoteWorkDir} && ${envLoader} && export GEMINI_CLI_HOME=${ISOLATED_CONFIG} && ./node_modules/.bin/tsx .gemini/skills/deep-review/scripts/entrypoint.ts ${prNumber} ${branchName}`; + const entryCmd = `cd ${remoteWorkDir} && ${envLoader} && export GEMINI_CLI_HOME=${ISOLATED_GEMINI} && export GH_CONFIG_DIR=${ISOLATED_GH} && ./node_modules/.bin/tsx .gemini/skills/deep-review/scripts/entrypoint.ts ${prNumber} ${branchName} ${remotePolicyPath}`; const tmuxCmd = `$SHELL -ic ${q(entryCmd)}; exec $SHELL`; const sshInternal = `tmux attach-session -t ${sessionName} 2>/dev/null || tmux new-session -s ${sessionName} -n ${q(branchName)} ${q(tmuxCmd)}`; diff --git a/.gemini/skills/deep-review/scripts/setup.ts b/.gemini/skills/deep-review/scripts/setup.ts index d87229a5b7..d3e506f9a3 100644 --- a/.gemini/skills/deep-review/scripts/setup.ts +++ b/.gemini/skills/deep-review/scripts/setup.ts @@ -39,21 +39,32 @@ async function main() { const remoteWorkDir = await prompt('Remote Work Directory', '~/gcli/deepreview'); console.log(`šŸ” Checking state of ${remoteHost}...`); - const ghCheck = spawnSync('ssh', [remoteHost, 'command -v gh'], { shell: true }); + // 1. Gemini CLI Isolation Choice + const geminiChoice = await prompt('\nGemini CLI Setup: Use [p]re-existing instance or [i]solated sandbox instance? (Isolated is recommended)', 'i'); + const geminiSetup = geminiChoice.toLowerCase() === 'p' ? 'preexisting' : 'isolated'; + + // 2. GitHub CLI Isolation Choice + const ghChoice = await prompt('GitHub CLI Setup: Use [p]re-existing instance or [i]solated sandbox instance? (Isolated is recommended)', 'i'); + const ghSetup = ghChoice.toLowerCase() === 'p' ? 'preexisting' : 'isolated'; + + const ISOLATED_GEMINI_CONFIG = '~/.gemini-deep-review'; + const ISOLATED_GH_CONFIG = '~/.gh-deep-review'; + + // System Requirements Check + const ghCheck = spawnSync('ssh', [remoteHost, 'sh -lc "command -v gh"'], { stdio: 'pipe' }); if (ghCheck.status !== 0) { console.log('\nšŸ“„ System Requirements Check:'); console.log(' āŒ GitHub CLI (gh) is not installed on remote.'); const shouldProvision = await confirm('\nWould you like Gemini to automatically provision gh?'); - if (shouldProvision) { console.log(`šŸš€ Attempting to install gh on ${remoteHost}...`); - const osCheck = spawnSync('ssh', [remoteHost, 'uname -s'], { shell: true }); + const osCheck = spawnSync('ssh', [remoteHost, 'uname -s'], { stdio: 'pipe' }); const os = osCheck.stdout.toString().trim(); let installCmd = os === 'Linux' ? 'sudo apt update && sudo apt install -y gh' : (os === 'Darwin' ? 'brew install gh' : ''); if (installCmd) { - spawnSync('ssh', ['-t', remoteHost, installCmd], { stdio: 'inherit', shell: true }); + spawnSync('ssh', ['-t', remoteHost, installCmd], { stdio: 'inherit' }); } } else { console.log('āš ļø Please ensure gh is installed before running again.'); @@ -61,21 +72,55 @@ async function main() { } } - // Ensure remote work dir exists - spawnSync('ssh', [remoteHost, `mkdir -p ${remoteWorkDir}`], { shell: true }); + // Ensure remote work dir and isolated config dirs exist + spawnSync('ssh', [remoteHost, `mkdir -p ${remoteWorkDir} ${ISOLATED_GEMINI_CONFIG}/policies/ ${ISOLATED_GH_CONFIG}`], { stdio: 'pipe' }); // Identity Synchronization Onboarding console.log('\nšŸ” Identity & Authentication:'); - const homeDir = process.env.HOME || ''; - const localAuth = path.join(homeDir, '.gemini/google_accounts.json'); - const localEnv = path.join(REPO_ROOT, '.env'); - const hasAuth = fs.existsSync(localAuth); - const hasEnv = fs.existsSync(localEnv); + + // GH Auth Check + const ghAuthCmd = ghSetup === 'isolated' ? `export GH_CONFIG_DIR=${ISOLATED_GH_CONFIG} && gh auth status` : 'gh auth status'; + const remoteGHAuth = spawnSync('ssh', [remoteHost, `sh -lc "${ghAuthCmd}"`], { stdio: 'pipe' }); + const isGHAuthRemote = remoteGHAuth.status === 0; + + if (isGHAuthRemote) { + console.log(` āœ… GitHub CLI is already authenticated on remote (${ghSetup}).`); + } else { + console.log(` āŒ GitHub CLI is NOT authenticated on remote (${ghSetup}).`); + // If it's isolated but global is authenticated, offer to sync + if (ghSetup === 'isolated') { + const globalGHAuth = spawnSync('ssh', [remoteHost, 'sh -lc "gh auth status"'], { stdio: 'pipe' }); + if (globalGHAuth.status === 0) { + if (await confirm(' Global GH auth found. Sync it to isolated instance?')) { + spawnSync('ssh', [remoteHost, `cp -r ~/.config/gh/* ${ISOLATED_GH_CONFIG}/`]); + console.log(' āœ… GH Auth synced.'); + } + } + } + if (!isGHAuthRemote) console.log(' You may need to run "gh auth login" on the remote machine later.'); + } + + // Gemini Auth Check + const geminiAuthCheck = geminiSetup === 'isolated' + ? `[ -f ${ISOLATED_GEMINI_CONFIG}/google_accounts.json ]` + : '[ -f ~/.gemini/google_accounts.json ]'; + const remoteGeminiAuth = spawnSync('ssh', [remoteHost, `sh -lc "${geminiAuthCheck}"`], { stdio: 'pipe' }); + const isGeminiAuthRemote = remoteGeminiAuth.status === 0; let syncAuth = false; - if (hasAuth || hasEnv) { - console.log(` šŸ” Found local identity files: ${[hasAuth ? 'Google Account' : '', hasEnv ? '.env' : ''].filter(Boolean).join(', ')}`); - syncAuth = await confirm(' Would you like Gemini to automatically sync your local identity to the remote workstation for seamless authentication?'); + if (isGeminiAuthRemote) { + console.log(` āœ… Gemini CLI is already authenticated on remote (${geminiSetup}).`); + } else { + const homeDir = process.env.HOME || ''; + const localAuth = path.join(homeDir, '.gemini/google_accounts.json'); + const localEnv = path.join(REPO_ROOT, '.env'); + const hasAuth = fs.existsSync(localAuth); + const hasEnv = fs.existsSync(localEnv); + + if (hasAuth || hasEnv) { + console.log(` šŸ” Found local Gemini CLI credentials: ${[hasAuth ? 'Google Account' : '', hasEnv ? '.env' : ''].filter(Boolean).join(', ')}`); + syncAuth = await confirm(' Would you like Gemini to automatically sync your local credentials to the remote workstation for seamless authentication?'); + } } const terminalType = await prompt('\nTerminal Automation (iterm2 / terminal / none)', 'iterm2'); @@ -84,7 +129,6 @@ async function main() { const envLoader = 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh"'; console.log(`\nšŸ“¦ Checking isolated dependencies in ${remoteWorkDir}...`); - // Use a single string for ssh command to avoid quoting issues with spawnSync shell:true const checkCmd = `ssh ${remoteHost} ${q(`${envLoader} && [ -x ${remoteWorkDir}/node_modules/.bin/tsx ] && [ -x ${remoteWorkDir}/node_modules/.bin/gemini ]`)}`; const depCheck = spawnSync(checkCmd, { shell: true }); @@ -103,7 +147,7 @@ async function main() { try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (e) {} } settings.maintainer = settings.maintainer || {}; - settings.maintainer.deepReview = { remoteHost, remoteWorkDir, terminalType, syncAuth }; + settings.maintainer.deepReview = { remoteHost, remoteWorkDir, terminalType, syncAuth, geminiSetup, ghSetup }; fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); console.log('\nāœ… Onboarding complete! Settings saved to .gemini/settings.json'); diff --git a/.gemini/skills/deep-review/scripts/worker.ts b/.gemini/skills/deep-review/scripts/worker.ts index 1beb6794fa..dd824b56a9 100644 --- a/.gemini/skills/deep-review/scripts/worker.ts +++ b/.gemini/skills/deep-review/scripts/worker.ts @@ -1,28 +1,28 @@ /** * Universal Deep Review Worker (Remote) + * + * Handles worktree provisioning and parallel task execution. */ import { spawn, spawnSync } from 'child_process'; import path from 'path'; import fs from 'fs'; -import { fileURLToPath } from 'url'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const workDir = process.cwd(); const prNumber = process.argv[2]; const branchName = process.argv[3]; +const policyPath = process.argv[4]; async function main() { - if (!prNumber || !branchName) { - console.error('Usage: tsx worker.ts '); + if (!prNumber || !branchName || !policyPath) { + console.error('Usage: tsx worker.ts '); process.exit(1); } + const workDir = process.cwd(); // This is remoteWorkDir const targetDir = path.join(workDir, branchName); // 1. Provision PR Directory (Fast Blobless Clone) if (!fs.existsSync(targetDir)) { console.log(`🌿 Provisioning PR #${prNumber} into ${branchName}...`); - // Blobless clone: downloads history but no file contents until checked out. Extremely fast. const cloneCmd = `git clone --filter=blob:none https://github.com/google-gemini/gemini-cli.git ${targetDir}`; spawnSync(cloneCmd, { stdio: 'inherit', shell: true }); @@ -35,19 +35,14 @@ async function main() { const logDir = path.join(targetDir, `.gemini/logs/review-${prNumber}`); fs.mkdirSync(logDir, { recursive: true }); - const GEMINI_CMD = path.join(workDir, 'node_modules/.bin/gemini'); - - // Use mirrored policy if available - const policyFile = path.join(process.env.HOME || '', '.gemini/policies/policy.toml'); - const policyFlag = fs.existsSync(policyFile) ? `--policy ${policyFile}` : ''; + const geminiBin = path.join(workDir, 'node_modules/.bin/gemini'); // 2. Define Parallel Tasks const tasks = [ { id: 'build', name: 'Fast Build', cmd: `cd ${targetDir} && npm ci && npm run build` }, { id: 'ci', name: 'CI Checks', cmd: `gh pr checks ${prNumber}` }, - // Point the analysis at the PR directory specifically - { id: 'review', name: 'Gemini Analysis', cmd: `${GEMINI_CMD} ${policyFlag} --cwd ${targetDir} -p "/review-frontend ${prNumber}"` }, - { id: 'verify', name: 'Behavioral Proof', cmd: `${GEMINI_CMD} ${policyFlag} --cwd ${targetDir} -p "Analyze the code in ${targetDir} and exercise it."`, dep: 'build' } + { id: 'review', name: 'Gemini Analysis', cmd: `${geminiBin} --policy ${policyPath} --cwd ${targetDir} -p "/review-frontend ${prNumber}"` }, + { id: 'verify', name: 'Behavioral Proof', cmd: `${geminiBin} --policy ${policyPath} --cwd ${targetDir} -p "Analyze the code in ${targetDir} and exercise it to prove it works."`, dep: 'build' } ]; const state: Record = {}; @@ -68,7 +63,6 @@ async function main() { proc.on('close', (code) => { const exitCode = code ?? 0; state[task.id].status = exitCode === 0 ? 'SUCCESS' : 'FAILED'; - // Write exit code for remote polling fs.writeFileSync(path.join(logDir, `${task.id}.exit`), exitCode.toString()); render(); }); @@ -90,8 +84,6 @@ async function main() { const allDone = tasks.every(t => ['SUCCESS', 'FAILED'].includes(state[t.id].status)); if (allDone) { console.log(`\n✨ Verification complete. Launching interactive session...`); - // cd into the targetDir for the final interactive session - process.chdir(targetDir); process.exit(0); } }