feat(offload): implement unified control plane with log tailing and flexible attachment

This commit is contained in:
mkorwel
2026-03-14 11:02:27 -07:00
parent 7ee56e7cfd
commit 369241d271
8 changed files with 330 additions and 38 deletions

View File

@@ -0,0 +1,69 @@
/**
* Offload Attach Utility (Local)
*
* Re-attaches to a running tmux session on the worker.
*/
import { 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 REPO_ROOT = path.resolve(__dirname, '../../../..');
const q = (str: string) => `'${str.replace(/'/g, "'\\''")}'`;
export async function runAttach(args: string[], env: NodeJS.ProcessEnv = process.env) {
const prNumber = args[0];
const action = args[1] || 'review';
const isLocal = args.includes('--local');
if (!prNumber) {
console.error('Usage: npm run offload:attach <PR_NUMBER> [action] [--local]');
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) {
console.error('❌ Settings not found. Run "npm run offload:setup" first.');
return 1;
}
const { remoteHost } = config;
const sessionName = `offload-${prNumber}-${action}`;
const finalSSH = `ssh -t ${remoteHost} "tmux attach-session -t ${sessionName}"`;
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`);
fs.writeFileSync(tempCmdPath, `#!/bin/bash\n${finalSSH}\nrm "$0"`, { mode: 0o755 });
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
end tell
activate
end tell
end run
`;
spawnSync('osascript', ['-', tempCmdPath], { input: appleScript });
console.log(`✅ iTerm2 window opened for ${sessionName}.`);
return 0;
}
spawnSync(finalSSH, { stdio: 'inherit', shell: true });
return 0;
}
if (import.meta.url === `file://${process.argv[1]}`) {
runAttach(process.argv.slice(2)).catch(console.error);
}

View File

@@ -108,6 +108,14 @@ async function stopWorker() {
}
}
async function remoteStatus() {
const name = INSTANCE_PREFIX;
const zone = 'us-west1-a';
console.log(`📡 Fetching remote status from ${name}...`);
spawnSync('ssh', ['gcli-worker', 'tsx .offload/scripts/status.ts'], { stdio: 'inherit', shell: true });
}
async function main() {
const action = process.argv[2] || 'list';
@@ -121,6 +129,9 @@ async function main() {
case 'stop':
await stopWorker();
break;
case 'status':
await remoteStatus();
break;
case 'create-image':
await createImage();
break;

View File

@@ -0,0 +1,50 @@
/**
* Offload Log Tailer (Local)
*
* Tails the latest remote logs for a specific job.
*/
import { 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 REPO_ROOT = path.resolve(__dirname, '../../../..');
export async function runLogs(args: string[]) {
const prNumber = args[0];
const action = args[1] || 'review';
if (!prNumber) {
console.error('Usage: npm run offload:logs <PR_NUMBER> [action]');
return 1;
}
const settingsPath = path.join(REPO_ROOT, '.gemini/settings.json');
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const config = settings.maintainer?.deepReview;
const { remoteHost, remoteHome } = config;
const jobDir = `${remoteHome}/dev/worktrees/offload-${prNumber}-${action}`;
const logDir = `${jobDir}/.gemini/logs`;
console.log(`📋 Tailing latest logs for job ${prNumber}-${action}...`);
// Remote command to find the latest log file and tail it
const tailCmd = `
latest_log=$(ls -t ${logDir}/*.log 2>/dev/null | head -n 1)
if [ -z "$latest_log" ]; then
echo "❌ No logs found for this job yet."
exit 1
fi
echo "📄 Tailing: $latest_log"
tail -f "$latest_log"
`;
spawnSync(`ssh ${remoteHost} ${JSON.stringify(tailCmd)}`, { stdio: 'inherit', shell: true });
return 0;
}
if (import.meta.url === `file://${process.argv[1]}`) {
runLogs(process.argv.slice(2)).catch(console.error);
}

View File

@@ -54,7 +54,7 @@ export async function runOrchestrator(args: string[], env: NodeJS.ProcessEnv = p
const setupCmd = `
mkdir -p ${remoteHome}/dev/worktrees && \
cd ${remoteWorkDir} && \
git fetch origin pull/${prNumber}/head && \
git fetch upstream pull/${prNumber}/head && \
git worktree add -f ${remoteWorktreeDir} FETCH_HEAD
`;

View File

@@ -2,24 +2,73 @@ import { TaskRunner } from '../TaskRunner.js';
import path from 'path';
import { spawnSync } from 'child_process';
import { TaskRunner } from '../TaskRunner.js';
import path from 'path';
import { spawnSync } from 'child_process';
import fs from 'fs';
export async function runImplementPlaybook(issueNumber: string, workDir: string, policyPath: string, geminiBin: string) {
const runner = new TaskRunner(
path.join(workDir, `.gemini/logs/offload-issue-${issueNumber}`),
`🚀 Offload | IMPLEMENT | Issue #${issueNumber}`
);
console.log(`🚀 Offload | IMPLEMENT (Supervisor Loop) | Issue #${issueNumber}`);
const ghView = spawnSync('gh', ['issue', 'view', issueNumber, '--json', 'title,body', '-q', '{title:.title,body:.body}'], { shell: true });
const meta = JSON.parse(ghView.stdout.toString());
const branchName = `impl/${issueNumber}-${meta.title.toLowerCase().replace(/[^a-z0-9]/g, '-')}`.slice(0, 50);
console.log(`🔍 Fetching metadata for Issue #${issueNumber}...`);
const ghView = spawnSync('gh', ['issue', 'view', issueNumber, '--json', 'title', '-q', '.title'], { shell: true });
const title = ghView.stdout.toString().trim() || `issue-${issueNumber}`;
const branchName = `impl/${issueNumber}-${title.toLowerCase().replace(/[^a-z0-9]/g, '-')}`.slice(0, 50);
// 1. Initial Research & Test Creation
console.log('\n🧠 Phase 1: Research & Reproduction...');
spawnSync(geminiBin, [
'--policy', policyPath, '--cwd', workDir,
'-p', `Research Issue #${issueNumber}: "${meta.title}".
Description: ${meta.body}.
ACTION: Create a NEW Vitest test file in 'tests/repro_issue_${issueNumber}.test.ts' that demonstrates the issue or feature.
Ensure this test fails currently.`
], { stdio: 'inherit' });
runner.register([
{ id: 'branch', name: 'Create Branch', cmd: `git checkout -b ${branchName}` },
{ id: 'research', name: 'Codebase Research', cmd: `${geminiBin} --policy ${policyPath} -p "Research the requirements for issue #${issueNumber} using 'gh issue view ${issueNumber}'. Map out the files that need to be changed."`, dep: 'branch' },
{ id: 'implement', name: 'Implementation', cmd: `${geminiBin} --policy ${policyPath} -p "Implement the changes for issue #${issueNumber} based on your research. Ensure all code follows project standards."`, dep: 'research' },
{ id: 'verify', name: 'Verification', cmd: `npm run build && npm test`, dep: 'implement' },
{ id: 'pr', name: 'Create Pull Request', cmd: `git add . && git commit -m "feat: implement issue #${issueNumber}" && git push origin ${branchName} && gh pr create --title "${title}" --body "Closes #${issueNumber}"`, dep: 'verify' }
]);
// 2. The Self-Healing Loop
let attempts = 0;
const maxAttempts = 5;
let success = false;
return runner.run();
console.log('\n🛠 Phase 2: Implementation Loop...');
while (attempts < maxAttempts && !success) {
attempts++;
console.log(`\n👉 Attempt ${attempts}/${maxAttempts}...`);
// Run the specific repro test
const testRun = spawnSync('npx', ['vitest', 'run', `tests/repro_issue_${issueNumber}.test.ts`], { cwd: workDir });
if (testRun.status === 0) {
console.log('✅ Reproduction test PASSED!');
success = true;
break;
}
console.log('❌ Test failed. Asking Gemini to fix the implementation...');
const testError = testRun.stdout.toString() + testRun.stderr.toString();
spawnSync(geminiBin, [
'--policy', policyPath, '--cwd', workDir,
'-p', `The reproduction test for Issue #${issueNumber} is still failing.
ERROR OUTPUT:
${testError.slice(-2000)}
ACTION: Modify the source code to fix this error and make the test pass.
Do not modify the test itself unless it has a syntax error.`
], { stdio: 'inherit' });
}
// 3. Final Verification
if (success) {
console.log('\n🧪 Phase 3: Final Verification...');
const finalCheck = spawnSync('npm', ['test'], { cwd: workDir, stdio: 'inherit' });
if (finalCheck.status === 0) {
console.log('\n🎉 Implementation complete and verified!');
spawnSync('git', ['add', '.'], { cwd: workDir });
spawnSync('git', ['commit', '-m', `feat: implement issue #${issueNumber}`], { cwd: workDir });
return 0;
}
}
console.error('\n❌ Supervisor: Failed to reach a passing state within retry limit.');
return 1;
}

View File

@@ -85,6 +85,34 @@ Host ${sshAlias}
// 1. Configure Fast-Path SSH Alias
// ... (unchanged)
// 1b. Security Fork Management
console.log('\n🍴 Configuring Security Fork...');
const upstreamRepo = 'google-gemini/gemini-cli';
const forkCheck = spawnSync('gh', ['repo', 'view', '--json', 'parent,nameWithOwner'], { stdio: 'pipe' });
let currentRepo = '';
try {
const repoInfo = JSON.parse(forkCheck.stdout.toString());
currentRepo = repoInfo.nameWithOwner;
} catch (e) {}
let userFork = '';
if (currentRepo.includes(`${env.USER}/`) || currentRepo.includes('mattkorwel/')) {
userFork = currentRepo;
console.log(` ✅ Using existing fork: ${userFork}`);
} else {
console.log(` 🔍 No personal fork detected for ${upstreamRepo}.`);
if (await confirm(' Would you like to create a personal fork for autonomous work?')) {
const forkResult = spawnSync('gh', ['repo', 'fork', upstreamRepo, '--clone=false'], { stdio: 'inherit' });
if (forkResult.status === 0) {
// Get the fork name (usually <user>/gemini-cli)
const user = spawnSync('gh', ['api', 'user', '-q', '.login'], { stdio: 'pipe' }).stdout.toString().trim();
userFork = `${user}/gemini-cli`;
console.log(` ✅ Created fork: ${userFork}`);
}
}
}
// Use the alias for remaining setup steps
const remoteHost = sshAlias;
const remoteHome = spawnSync(`ssh ${remoteHost} "pwd"`, { shell: true }).stdout.toString().trim();
@@ -109,25 +137,36 @@ Host ${sshAlias}
}
}
// 4. Remote GitHub Login
if (await confirm('Authenticate GitHub CLI on the worker?')) {
console.log('\n🔐 Performing non-interactive GitHub CLI authentication on worker...');
const localToken = spawnSync('gh', ['auth', 'token'], { stdio: 'pipe' }).stdout.toString().trim();
if (!localToken) {
console.error('❌ Could not find local GitHub token. Please log in locally first with "gh auth login".');
return 1;
}
// Pipe the local token to the remote worker's gh auth login
const loginCmd = `gh auth login --with-token --insecure-storage`;
const result = spawnSync(`echo ${localToken} | ssh ${remoteHost} ${JSON.stringify(loginCmd)}`, { shell: true, stdio: 'inherit' });
// 4. Scoped Token Onboarding (Security Hardening)
if (await confirm('Generate a scoped, secure token for the autonomous agent? (Recommended)')) {
const user = spawnSync('gh', ['api', 'user', '-q', '.login'], { stdio: 'pipe' }).stdout.toString().trim();
if (result.status === 0) {
console.log('✅ GitHub CLI authenticated successfully on worker.');
// Set git protocol to https by default for simplicity
spawnSync(`ssh ${remoteHost} "gh config set git_protocol https"`, { shell: true });
} else {
console.error('❌ GitHub CLI authentication failed on worker.');
// Construct the Pre-Filled Magic Link for Fine-Grained PAT
const scopes = 'contents:write,pull_requests:write,metadata:read';
const description = `Gemini CLI Offload - ${env.USER || 'maintainer'}`;
const magicLink = `https://github.com/settings/tokens/beta/new?description=${encodeURIComponent(description)}&repositories[]=${encodeURIComponent(upstreamRepo)}&repositories[]=${encodeURIComponent(userFork)}&permissions[contents]=write&permissions[pull_requests]=write&permissions[metadata]=read`;
console.log('\n🔐 SECURITY HARDENING:');
console.log('1. Open this Magic Link in your browser to create a scoped token:');
console.log(` \x1b[34m${magicLink}\x1b[0m`);
console.log('2. Click "Generate token" at the bottom of the page.');
console.log('3. Copy the token and paste it here.');
const scopedToken = await prompt('\nPaste Scoped Token', '');
if (scopedToken) {
console.log(` - Mirroring scoped token to worker...`);
// Save it to a persistent file on the worker that entrypoint.ts will prioritize
spawnSync(`ssh ${remoteHost} "mkdir -p ~/.offload && echo ${scopedToken} > ~/.offload/.gh_token && chmod 600 ~/.offload/.gh_token"`, { shell: true });
console.log(' ✅ Scoped token saved on worker.');
}
} else {
// Fallback: Standard gh auth login if they skip scoped token
if (await confirm('Fallback: Authenticate via standard GitHub CLI login?')) {
console.log('\n🔐 Starting GitHub CLI authentication on worker...');
const localToken = spawnSync('gh', ['auth', 'token'], { stdio: 'pipe' }).stdout.toString().trim();
const loginCmd = `gh auth login --with-token --insecure-storage`;
spawnSync(`echo ${localToken} | ssh ${remoteHost} ${JSON.stringify(loginCmd)}`, { shell: true });
}
}
@@ -136,9 +175,9 @@ Host ${sshAlias}
console.log('🚀 Installing global developer tools...');
spawnSync(`ssh ${remoteHost} "sudo npm install -g tsx vitest"`, { shell: true, stdio: 'inherit' });
console.log('🚀 Cloning repository (blob-less) on worker...');
const repoUrl = 'https://github.com/google-gemini/gemini-cli.git';
const cloneCmd = `[ -d ${remoteWorkDir}/.git ] || git clone --filter=blob:none ${repoUrl} ${remoteWorkDir}`;
console.log(`🚀 Cloning fork ${userFork} on worker...`);
const repoUrl = `https://github.com/${userFork}.git`;
const cloneCmd = `[ -d ${remoteWorkDir}/.git ] || (git clone --filter=blob:none ${repoUrl} ${remoteWorkDir} && cd ${remoteWorkDir} && git remote add upstream https://github.com/${upstreamRepo}.git && git fetch upstream)`;
spawnSync(`ssh ${remoteHost} ${JSON.stringify(cloneCmd)}`, { shell: true, stdio: 'inherit' });
// We skip the full npm install here as requested; per-worktree builds will handle it if needed.
@@ -157,6 +196,8 @@ Host ${sshAlias}
remoteHost,
remoteHome,
remoteWorkDir,
userFork,
upstreamRepo,
terminalType: 'iterm2'
};
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));

View File

@@ -0,0 +1,69 @@
/**
* Offload Status Inspector (Remote)
*
* Scans tmux sessions and logs to provide a real-time status of offload jobs.
*/
import { spawnSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import os from 'os';
const WORKTREE_BASE = path.join(os.homedir(), 'dev/worktrees');
function getStatus() {
console.log('\n🛰 Offload Mission Control Status:');
console.log(''.padEnd(80, '-'));
console.log(`${'JOB ID'.padEnd(10)} | ${'ACTION'.padEnd(10)} | ${'STATE'.padEnd(12)} | ${'SESSION'.padEnd(25)}`);
console.log(''.padEnd(80, '-'));
// 1. Get active tmux sessions
const tmux = spawnSync('tmux', ['ls', '-F', '#{session_name}']);
const activeSessions = tmux.stdout.toString().split('\n').filter(s => s.startsWith('offload-'));
// 2. Scan worktrees for job history
if (!fs.existsSync(WORKTREE_BASE)) {
console.log(' No jobs found.');
return;
}
const jobs = fs.readdirSync(WORKTREE_BASE).filter(d => d.startsWith('offload-'));
if (jobs.length === 0 && activeSessions.length === 0) {
console.log(' No jobs found.');
return;
}
const allJobIds = new Set([...jobs, ...activeSessions.map(s => s)]);
allJobIds.forEach(id => {
const parts = id.split('-'); // offload-123-review
const pr = parts[1] || '???';
const action = parts[2] || '???';
let state = '💤 IDLE';
if (activeSessions.includes(id)) {
state = '🏃 RUNNING';
} else {
// Check logs for final state
const logDir = path.join(WORKTREE_BASE, id, '.gemini/logs');
if (fs.existsSync(logDir)) {
const logFiles = fs.readdirSync(logDir).sort();
if (logFiles.length > 0) {
const lastLog = fs.readFileSync(path.join(logDir, logFiles[logFiles.length - 1]), 'utf8');
if (lastLog.includes('SUCCESS')) state = '✅ SUCCESS';
else if (lastLog.includes('FAILED')) state = '❌ FAILED';
else state = '🏁 FINISHED';
}
}
}
console.log(`${pr.padEnd(10)} | ${action.padEnd(10)} | ${state.padEnd(12)} | ${id.padEnd(25)}`);
if (state === '🏃 RUNNING') {
console.log(` └─ Attach: npm run offload:attach ${pr} ${action} [--local]`);
console.log(` └─ Logs: npm run offload:logs ${pr} ${action}`);
}
});
console.log(''.padEnd(80, '-'));
}
getStatus();

View File

@@ -68,6 +68,9 @@
"offload:check": "tsx .gemini/skills/offload/scripts/check.ts",
"offload:clean": "tsx .gemini/skills/offload/scripts/clean.ts",
"offload:fleet": "tsx .gemini/skills/offload/scripts/fleet.ts",
"offload:status": "npm run offload:fleet status",
"offload:attach": "tsx .gemini/skills/offload/scripts/attach.ts",
"offload:logs": "tsx .gemini/skills/offload/scripts/logs.ts",
"pre-commit": "node scripts/pre-commit.js"
},
"overrides": {