UNPKG

automagik-genie

Version:

Self-evolving AI agent orchestration framework with Model Context Protocol support

228 lines (195 loc) 7.53 kB
#!/usr/bin/env node const { spawnSync, execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); function getWorktreeRoot() { try { // Get the top-level directory of the current worktree const result = execSync('git rev-parse --show-toplevel', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); return result; } catch (e) { // Fallback to current directory if git command fails return process.cwd(); } } function runNodeScript(script, args = []) { const worktreeRoot = getWorktreeRoot(); const p = path.join(worktreeRoot, '.genie', 'scripts', script); const res = spawnSync('node', [p, ...args], { stdio: 'inherit', cwd: worktreeRoot // Run from worktree root, not main repo }); return res.status || 0; } function getCurrentBranch() { try { // Try to get branch from current working directory context // This works correctly in worktrees when git push is executed const result = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); return result; } catch (e) { // Fallback: try reading HEAD file directly try { const gitDir = process.env.GIT_DIR || path.join(__dirname, '..'); const headFile = path.join(gitDir, 'HEAD'); if (fs.existsSync(headFile)) { const head = fs.readFileSync(headFile, 'utf8').trim(); if (head.startsWith('ref: refs/heads/')) { return head.replace('ref: refs/heads/', ''); } } } catch {} return ''; } } function autoSyncWithRemote(branch) { // Auto-sync with remote to prevent rejection due to automated commits (like RC version bumps) if (process.env.GENIE_SKIP_AUTO_SYNC) { console.log('⏭️ Auto-sync skipped (GENIE_SKIP_AUTO_SYNC set)'); return false; // No rebase happened } console.log('🔄 Auto-syncing with remote...'); try { // Fetch latest from remote execSync(`git fetch origin ${branch}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); // Check if remote is ahead const localCommit = execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); const remoteCommit = execSync(`git rev-parse origin/${branch}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); if (localCommit === remoteCommit) { console.log('✅ Already in sync with remote'); return false; // No rebase needed } // Check if we're behind const behindCount = execSync(`git rev-list --count HEAD..origin/${branch}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); if (parseInt(behindCount) > 0) { console.log(`📥 Remote is ${behindCount} commit(s) ahead, rebasing...`); // Rebase on remote const rebaseResult = spawnSync('git', ['rebase', `origin/${branch}`], { stdio: 'inherit' }); if (rebaseResult.status !== 0) { console.error('❌ Auto-rebase failed - please resolve conflicts manually'); console.error(' Run: git rebase --abort && git pull --rebase'); process.exit(1); } console.log('✅ Successfully rebased on remote changes'); return true; // Rebase happened, need to retry push } else { console.log('✅ Local is ahead of remote'); return false; // No rebase needed } } catch (e) { console.warn(`⚠️ Auto-sync failed: ${e.message}`); console.warn(' Continuing with push (may be rejected if remote changed)'); return false; // Error, let original push handle it } } function main() { console.log('🧞 Genie pre-push hook'); const currentBranch = getCurrentBranch(); const isForgeBranch = currentBranch.startsWith('forge/'); const isFeatBranch = currentBranch.startsWith('feat/'); const isWorkInProgress = isForgeBranch || isFeatBranch; console.log(`📍 Detected branch: ${currentBranch}`); // Early exit for forge worktrees (total isolation, full performance) try { const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim(); const isWorktree = gitDir.includes('/worktrees/'); if (isForgeBranch || isWorktree) { console.log('🔧 Forge worktree detected - skipping ALL hooks (full performance mode)'); console.log('- Result: ✅ Pre-push validations skipped (forge isolation)'); process.exit(0); } } catch (e) { // Continue with normal hooks on error } // Step 0: Auto-sync with remote (prevents rejection from automated commits) const didRebase = autoSyncWithRemote(currentBranch); // Step 1: tests (skip if GENIE_SKIP_TESTS is set) if (process.env.GENIE_SKIP_TESTS) { console.warn('⚠️ Tests skipped (GENIE_SKIP_TESTS set)'); } else { const testsCode = runNodeScript('run-tests.cjs'); if (testsCode !== 0) { console.error('❌ Pre-push blocked - tests failed'); process.exit(1); } } // Step 2: commit advisory (validates traceability) let advisoryCode = 0; if (process.env.GENIE_ALLOW_MAIN_PUSH) { console.warn('⚠️ Commit advisory skipped (GENIE_ALLOW_MAIN_PUSH set)'); } else { advisoryCode = runNodeScript('commit-advisory.cjs'); // Block on main branch, warn on dev/feature branches if (advisoryCode === 2) { if (currentBranch === 'main' || currentBranch === 'master') { console.error('❌ Pre-push blocked - fix commit validation errors before pushing to main'); console.error(' See output above for details'); process.exit(1); } else { console.warn('⚠️ Commit validation issues detected (blocking at PR approval)'); console.warn(' See output above for details'); } } if (advisoryCode === 1 && !process.env.GENIE_SKIP_WISH_CHECK) { console.warn('⚠️ Commit advisory warnings (will be checked at PR approval)'); } } // Step 3: update changelog (non-blocking) const clCode = runNodeScript('update-changelog.cjs'); if (clCode !== 0) { console.warn('⚠️ CHANGELOG update failed (non-blocking)'); } // Step 4: change-reviewer agent (non-blocking, advisory only) // Quick sanity check: security issues, large changes, missing tests // Disabled for now - enable when genie run is stable in hooks // try { // console.log('🔍 Running quick change review...'); // const reviewResult = execSync('genie run change-reviewer --quiet', { // encoding: 'utf8', // cwd: getWorktreeRoot(), // stdio: 'inherit' // }); // // Advisory only - never blocks (always exit 0) // } catch (e) { // // Silently skip if genie run fails (non-blocking) // } console.log('✅ Pre-push hooks passed'); // If we rebased, automatically retry the push if (didRebase && !process.env.GENIE_AUTO_PUSH_RETRY) { console.log('🔄 Auto-retrying push after rebase...'); // Set env var to prevent infinite recursion process.env.GENIE_AUTO_PUSH_RETRY = '1'; try { // Retry the push (this will trigger hooks again, but didRebase will be false) execSync(`git push origin ${currentBranch}`, { stdio: 'inherit' }); console.log('\x1b[32m✅ Push succeeded after auto-rebase\x1b[0m'); process.exit(0); // Success, tell original command to abort } catch (e) { console.error('❌ Auto-retry push failed'); process.exit(1); } } } main();