diff --git a/.gemini/skills/offload/scripts/attach.ts b/.gemini/skills/offload/scripts/attach.ts index 952bdb939e..6bb44e6f77 100644 --- a/.gemini/skills/offload/scripts/attach.ts +++ b/.gemini/skills/offload/scripts/attach.ts @@ -1,12 +1,13 @@ /** * Offload Attach Utility (Local) * - * Re-attaches to a running tmux session on the worker. + * Re-attaches to a running tmux session inside the container on the worker. */ -import { spawnSync } from 'child_process'; import path from 'path'; import fs from 'fs'; +import { spawnSync } from 'child_process'; import { fileURLToPath } from 'url'; +import { ProviderFactory } from './providers/ProviderFactory.ts'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, '../../../..'); @@ -23,23 +24,28 @@ export async function runAttach(args: string[], env: NodeJS.ProcessEnv = process return 1; } - // ... (load settings) - const settingsPath = path.join(REPO_ROOT, '.gemini/settings.json'); - const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); - const config = settings.maintainer?.deepReview; - if (!config) { + const settingsPath = path.join(REPO_ROOT, '.gemini/offload/settings.json'); + if (!fs.existsSync(settingsPath)) { console.error('โŒ Settings not found. Run "npm run offload:setup" first.'); return 1; } + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + const config = settings.deepReview; + if (!config) { + console.error('โŒ Deep Review configuration not found.'); + return 1; + } + + const { projectId, zone } = config; + const targetVM = `gcli-offload-${env.USER || 'mattkorwel'}`; + const provider = ProviderFactory.getProvider({ projectId, zone, instanceName: targetVM }); - const { remoteHost } = config; - const sshConfigPath = path.join(REPO_ROOT, '.gemini/offload_ssh_config'); const sessionName = `offload-${prNumber}-${action}`; - const finalSSH = `ssh -F ${sshConfigPath} -t ${remoteHost} "tmux attach-session -t ${sessionName}"`; + const containerAttach = `sudo docker exec -it maintainer-worker sh -c ${q(`tmux attach-session -t ${sessionName}`)}`; + const finalSSH = provider.getRunCommand(containerAttach, { interactive: true }); console.log(`๐Ÿ”— Attaching to session: ${sessionName}...`); - // 2. Open in iTerm2 if within Gemini AND NOT --local const isWithinGemini = !!env.GEMINI_CLI || !!env.GEMINI_SESSION_ID || !!env.GCLI_SESSION_ID; if (isWithinGemini && !isLocal) { const tempCmdPath = path.join(process.env.TMPDIR || '/tmp', `offload-attach-${prNumber}.sh`); @@ -48,16 +54,18 @@ export async function runAttach(args: string[], env: NodeJS.ProcessEnv = process const appleScript = ` on run argv tell application "iTerm" - set newWindow to (create window with default profile) - tell current session of newWindow - write text (item 1 of argv) & return + tell current window + set newTab to (create tab with default profile) + tell current session of newTab + write text (item 1 of argv) & return + end tell end tell activate end tell end run `; spawnSync('osascript', ['-', tempCmdPath], { input: appleScript }); - console.log(`โœ… iTerm2 window opened for ${sessionName}.`); + console.log(`โœ… iTerm2 tab opened for ${sessionName}.`); return 0; } diff --git a/.gemini/skills/offload/scripts/clean.ts b/.gemini/skills/offload/scripts/clean.ts index c3b2c576e2..a4ece9bdc8 100644 --- a/.gemini/skills/offload/scripts/clean.ts +++ b/.gemini/skills/offload/scripts/clean.ts @@ -2,12 +2,13 @@ * Universal Offload Cleanup (Local) * * Surgical or full cleanup of sessions and worktrees on the GCE worker. + * Refactored to use WorkerProvider for container compatibility. */ -import { spawnSync } from 'child_process'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; import readline from 'readline'; +import { ProviderFactory } from './providers/ProviderFactory.ts'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, '../../../..'); @@ -22,53 +23,52 @@ async function confirm(question: string): Promise { }); } -export async function runCleanup(args: string[]) { +export async function runCleanup(args: string[], env: NodeJS.ProcessEnv = process.env) { const prNumber = args[0]; const action = args[1]; - const settingsPath = path.join(REPO_ROOT, '.gemini/settings.json'); + const settingsPath = path.join(REPO_ROOT, '.gemini/offload/settings.json'); if (!fs.existsSync(settingsPath)) { console.error('โŒ Settings not found. Run "npm run offload:setup" first.'); return 1; } const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); - const config = settings.maintainer?.deepReview; - + const config = settings.deepReview; if (!config) { console.error('โŒ Offload configuration not found.'); return 1; } - const { remoteHost } = config; + const { projectId, zone } = config; + const targetVM = `gcli-offload-${env.USER || 'mattkorwel'}`; + const provider = ProviderFactory.getProvider({ projectId, zone, instanceName: targetVM }); if (prNumber && action) { const sessionName = `offload-${prNumber}-${action}`; - const worktreePath = `~/dev/worktrees/${sessionName}`; + const worktreePath = `/home/node/dev/worktrees/${sessionName}`; console.log(`๐Ÿงน Surgically removing session and worktree for ${prNumber}-${action}...`); - // Kill specific tmux session - spawnSync(`ssh ${remoteHost} "tmux kill-session -t ${sessionName} 2>/dev/null"`, { shell: true }); + // Kill specific tmux session inside container + await provider.exec(`tmux kill-session -t ${sessionName} 2>/dev/null`, { wrapContainer: 'maintainer-worker' }); - // Remove specific worktree - spawnSync(`ssh ${remoteHost} "cd ~/dev/main && git worktree remove -f ${worktreePath} 2>/dev/null"`, { shell: true }); - spawnSync(`ssh ${remoteHost} "cd ~/dev/main && git worktree prune"`, { shell: true }); + // Remove specific worktree inside container + await provider.exec(`cd /home/node/dev/main && git worktree remove -f ${worktreePath} 2>/dev/null && git worktree prune`, { wrapContainer: 'maintainer-worker' }); console.log(`โœ… Cleaned up ${prNumber}-${action}.`); return 0; } - // --- Bulk Cleanup (Old Behavior) --- - console.log(`๐Ÿงน Starting BULK cleanup on ${remoteHost}...`); + // --- Bulk Cleanup --- + console.log(`๐Ÿงน Starting BULK cleanup on ${targetVM}...`); // 1. Standard Cleanup console.log(' - Killing ALL remote tmux sessions...'); - spawnSync(`ssh ${remoteHost} "tmux kill-server"`, { shell: true }); + await provider.exec(`tmux kill-server`, { wrapContainer: 'maintainer-worker' }); console.log(' - Cleaning up ALL Git Worktrees...'); - spawnSync(`ssh ${remoteHost} "cd ~/dev/main && git worktree prune"`, { shell: true }); - spawnSync(`ssh ${remoteHost} "rm -rf ~/dev/worktrees/*"`, { shell: true }); + await provider.exec(`cd /home/node/dev/main && git worktree prune && rm -rf /home/node/dev/worktrees/*`, { wrapContainer: 'maintainer-worker' }); console.log('โœ… Remote environment cleared.'); @@ -76,8 +76,8 @@ export async function runCleanup(args: string[]) { const shouldWipe = await confirm('\nWould you like to COMPLETELY wipe the remote workspace (main clone)?'); if (shouldWipe) { - console.log(`๐Ÿ”ฅ Wiping ~/dev/main...`); - spawnSync(`ssh ${remoteHost} "rm -rf ~/dev/main && mkdir -p ~/dev/main"`, { stdio: 'inherit', shell: true }); + console.log(`๐Ÿ”ฅ Wiping /home/node/dev/main...`); + await provider.exec(`rm -rf /home/node/dev/main && mkdir -p /home/node/dev/main`, { wrapContainer: 'maintainer-worker' }); console.log('โœ… Remote hub wiped. You will need to run npm run offload:setup again.'); } return 0; diff --git a/.gemini/skills/offload/scripts/entrypoint.ts b/.gemini/skills/offload/scripts/entrypoint.ts index 65b5a95933..f3f1ba64b4 100644 --- a/.gemini/skills/offload/scripts/entrypoint.ts +++ b/.gemini/skills/offload/scripts/entrypoint.ts @@ -24,19 +24,29 @@ async function main() { const workDir = process.cwd(); // This is remoteWorkDir as set in review.ts const targetDir = path.join(workDir, branchName); - // Path to the locally installed binaries in the work directory - const tsxBin = path.join(workDir, 'node_modules/.bin/tsx'); - const geminiBin = path.join(workDir, 'node_modules/.bin/gemini'); + // Use global tsx or fallback to local + const tsxBin = 'tsx'; + // Gemini bin will be needed by the interactive session later + // We'll try to find it in the main repo or current worktree + const geminiBin = fs.existsSync(path.join(workDir, 'node_modules/.bin/gemini')) + ? path.join(workDir, 'node_modules/.bin/gemini') + : path.join(path.dirname(workDir), 'main/node_modules/.bin/gemini'); + + const action = process.argv[5] || 'review'; // 1. Run the Parallel Reviewer console.log('๐Ÿš€ Launching Parallel Review Worker...'); - const workerResult = spawnSync(tsxBin, [path.join(__dirname, 'worker.ts'), prNumber, branchName, policyPath], { + console.log(` - Script: ${path.join(__dirname, 'worker.ts')}`); + console.log(` - Action: ${action}`); + + const workerResult = spawnSync(tsxBin, [path.join(__dirname, 'worker.ts'), prNumber, branchName, policyPath, action], { stdio: 'inherit', env: { ...process.env, GEMINI_CLI_HOME: ISOLATED_CONFIG } }); if (workerResult.status !== 0) { - console.error('โŒ Worker failed. Check the logs above.'); + console.error(`โŒ Worker failed with exit code ${workerResult.status}.`); + if (workerResult.error) console.error(' Error:', workerResult.error.message); } // 2. Launch the Interactive Gemini Session (Local Nightly) diff --git a/.gemini/skills/offload/scripts/worker.ts b/.gemini/skills/offload/scripts/worker.ts index 3f297981ff..71181e7a64 100644 --- a/.gemini/skills/offload/scripts/worker.ts +++ b/.gemini/skills/offload/scripts/worker.ts @@ -39,7 +39,11 @@ export async function runWorker(args: string[]) { process.chdir(targetDir); } - const geminiBin = path.join(workDir, 'node_modules/.bin/gemini'); + // Find gemini binary (try worktree, then fallback to main repo) + let geminiBin = path.join(targetDir, 'node_modules/.bin/gemini'); + if (!fs.existsSync(geminiBin)) { + geminiBin = path.join(path.dirname(targetDir), 'main/node_modules/.bin/gemini'); + } // 2. Dispatch to Playbook switch (action) {