From c29cfd13e0592c0225441d9406cfd0ce4a967780 Mon Sep 17 00:00:00 2001 From: Gabriel Malinosqui Date: Sat, 14 Feb 2026 16:40:29 -0300 Subject: [PATCH 1/4] feat: switch git hook from pre-commit to pre-push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Less intrusive review trigger — runs before pushing to remote instead of blocking every commit. The hook reads the pre-push stdin protocol and uses --branch to review only changes not yet on the remote. Co-Authored-By: Claude Opus 4.6 --- src/commands/__tests__/hook.test.ts | 2 +- src/commands/hook/index.ts | 10 ++++---- src/commands/hook/install.ts | 36 ++++++++++++++++++++++------- src/commands/hook/status.ts | 8 +++---- src/commands/hook/uninstall.ts | 8 +++---- 5 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/commands/__tests__/hook.test.ts b/src/commands/__tests__/hook.test.ts index d2abdd9..0b4e2c7 100644 --- a/src/commands/__tests__/hook.test.ts +++ b/src/commands/__tests__/hook.test.ts @@ -42,7 +42,7 @@ afterEach(async () => { }); function hookPath(): string { - return path.join(tmpDir, '.git', 'hooks', 'pre-commit'); + return path.join(tmpDir, '.git', 'hooks', 'pre-push'); } describe('hook install', () => { diff --git a/src/commands/hook/index.ts b/src/commands/hook/index.ts index 5d2dc3f..50856a1 100644 --- a/src/commands/hook/index.ts +++ b/src/commands/hook/index.ts @@ -4,12 +4,12 @@ import { uninstallAction } from './uninstall.js'; import { statusAction } from './status.js'; export const hookCommand = new Command('hook') - .description('Manage pre-commit hook for automatic code review'); + .description('Manage pre-push hook for automatic code review'); hookCommand .command('install') - .description('Install pre-commit hook for automatic code review') - .option('--fail-on ', 'Minimum severity to block commit (info, warning, error, critical)', 'critical') + .description('Install pre-push hook for automatic code review') + .option('--fail-on ', 'Minimum severity to block push (info, warning, error, critical)', 'critical') .option('--fast', 'Use fast mode for review (default: true)', true) .option('--no-fast', 'Disable fast mode for review') .option('--force', 'Overwrite existing hook without prompting') @@ -17,10 +17,10 @@ hookCommand hookCommand .command('uninstall') - .description('Remove pre-commit hook installed by kodus') + .description('Remove pre-push hook installed by kodus') .action(uninstallAction); hookCommand .command('status') - .description('Show pre-commit hook status') + .description('Show pre-push hook status') .action(statusAction); diff --git a/src/commands/hook/install.ts b/src/commands/hook/install.ts index ff3969f..1b8ef65 100644 --- a/src/commands/hook/install.ts +++ b/src/commands/hook/install.ts @@ -7,12 +7,14 @@ import { gitService } from '../../services/git.service.js'; const KODUS_MARKER = '# kodus-hook'; function generateHookScript(failOn: string, fast: boolean): string { - const flags = ['--staged']; + const flags: string[] = []; if (fast) flags.push('--fast'); flags.push('--fail-on', failOn); flags.push('--format', 'terminal'); flags.push('--quiet'); + const reviewFlags = flags.join(' '); + return `#!/bin/sh ${KODUS_MARKER} — installed by kodus CLI # To uninstall: kodus hook uninstall @@ -24,13 +26,31 @@ fi # Check if kodus is available if ! command -v kodus >/dev/null 2>&1; then - echo "Warning: kodus CLI not found. Skipping pre-commit review." + echo "Warning: kodus CLI not found. Skipping pre-push review." echo "Install: npm install -g @kodus/cli" exit 0 fi -# Run review on staged files -kodus review ${flags.join(' ')} +remote="$1" + +while read local_ref local_sha remote_ref remote_sha; do + # Skip branch deletions + if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then + continue + fi + + # New branch — no remote state to compare, skip review + if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then + continue + fi + + # Extract branch name from ref (refs/heads/my-branch → my-branch) + branch_name="\${remote_ref#refs/heads/}" + + # Review changes not yet on the remote + kodus review --branch "\${remote}/\${branch_name}" ${reviewFlags} + exit $? +done `; } @@ -50,7 +70,7 @@ export async function installAction(options: { const gitRoot = await gitService.getGitRoot(); const hooksDir = path.join(gitRoot.trim(), '.git', 'hooks'); - const hookPath = path.join(hooksDir, 'pre-commit'); + const hookPath = path.join(hooksDir, 'pre-push'); // Check if hook already exists let existingContent: string | null = null; @@ -68,7 +88,7 @@ export async function installAction(options: { { type: 'confirm', name: 'overwrite', - message: 'A pre-commit hook already exists. Overwrite it?', + message: 'A pre-push hook already exists. Overwrite it?', default: false, }, ]); @@ -87,12 +107,12 @@ export async function installAction(options: { const script = generateHookScript(failOn, fast); await fs.writeFile(hookPath, script, { mode: 0o755 }); - console.log(chalk.green('✓ Pre-commit hook installed successfully!')); + console.log(chalk.green('✓ Pre-push hook installed successfully!')); console.log(chalk.dim(` Path: ${hookPath}`)); console.log(chalk.dim(` Fail on: ${failOn}`)); console.log(chalk.dim(` Fast mode: ${fast ? 'yes' : 'no'}`)); console.log(''); - console.log(chalk.dim('Skip with: KODUS_SKIP_HOOK=1 git commit')); + console.log(chalk.dim('Skip with: KODUS_SKIP_HOOK=1 git push')); console.log(chalk.dim('Remove with: kodus hook uninstall')); } diff --git a/src/commands/hook/status.ts b/src/commands/hook/status.ts index 8141131..13203c1 100644 --- a/src/commands/hook/status.ts +++ b/src/commands/hook/status.ts @@ -12,22 +12,22 @@ export async function statusAction(): Promise { } const gitRoot = await gitService.getGitRoot(); - const hookPath = path.join(gitRoot.trim(), '.git', 'hooks', 'pre-commit'); + const hookPath = path.join(gitRoot.trim(), '.git', 'hooks', 'pre-push'); let content: string; try { content = await fs.readFile(hookPath, 'utf-8'); } catch { - console.log(chalk.yellow('Pre-commit hook: not installed')); + console.log(chalk.yellow('Pre-push hook: not installed')); return; } if (!content.includes(KODUS_MARKER)) { - console.log(chalk.yellow('Pre-commit hook: installed (not by kodus)')); + console.log(chalk.yellow('Pre-push hook: installed (not by kodus)')); return; } - console.log(chalk.green('Pre-commit hook: installed')); + console.log(chalk.green('Pre-push hook: installed')); // Parse config from hook script const failOnMatch = content.match(/--fail-on\s+(\S+)/); diff --git a/src/commands/hook/uninstall.ts b/src/commands/hook/uninstall.ts index aca5236..8652af5 100644 --- a/src/commands/hook/uninstall.ts +++ b/src/commands/hook/uninstall.ts @@ -12,21 +12,21 @@ export async function uninstallAction(): Promise { } const gitRoot = await gitService.getGitRoot(); - const hookPath = path.join(gitRoot.trim(), '.git', 'hooks', 'pre-commit'); + const hookPath = path.join(gitRoot.trim(), '.git', 'hooks', 'pre-push'); let content: string; try { content = await fs.readFile(hookPath, 'utf-8'); } catch { - console.log(chalk.yellow('No pre-commit hook found.')); + console.log(chalk.yellow('No pre-push hook found.')); return; } if (!content.includes(KODUS_MARKER)) { - console.log(chalk.yellow('The pre-commit hook was not installed by kodus. Skipping.')); + console.log(chalk.yellow('The pre-push hook was not installed by kodus. Skipping.')); return; } await fs.unlink(hookPath); - console.log(chalk.green('✓ Pre-commit hook removed successfully.')); + console.log(chalk.green('✓ Pre-push hook removed successfully.')); } From 1b1674b772277ee5104c42c8445df287ccd928bd Mon Sep 17 00:00:00 2001 From: Gabriel Malinosqui Date: Sat, 14 Feb 2026 16:50:41 -0300 Subject: [PATCH 2/4] fix: skip review when pushing a branch that is not checked out --branch compares against HEAD, so reviewing a ref that doesn't match the current branch would produce wrong diffs. Now the hook extracts the branch name from local_ref and only runs the review when it matches the currently checked-out branch. Co-Authored-By: Claude Opus 4.6 --- src/commands/hook/install.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/commands/hook/install.ts b/src/commands/hook/install.ts index 1b8ef65..0f18ba9 100644 --- a/src/commands/hook/install.ts +++ b/src/commands/hook/install.ts @@ -32,6 +32,7 @@ if ! command -v kodus >/dev/null 2>&1; then fi remote="$1" +current_branch="$(git symbolic-ref --short HEAD 2>/dev/null)" while read local_ref local_sha remote_ref remote_sha; do # Skip branch deletions @@ -45,7 +46,13 @@ while read local_ref local_sha remote_ref remote_sha; do fi # Extract branch name from ref (refs/heads/my-branch → my-branch) - branch_name="\${remote_ref#refs/heads/}" + branch_name="\${local_ref#refs/heads/}" + + # Only review if pushing the currently checked-out branch + # (--branch compares against HEAD, so reviewing other refs would produce wrong diffs) + if [ "\$branch_name" != "\$current_branch" ]; then + continue + fi # Review changes not yet on the remote kodus review --branch "\${remote}/\${branch_name}" ${reviewFlags} From 174a6deac3f7080cbbbbe4402833c931fc202e39 Mon Sep 17 00:00:00 2001 From: Gabriel Malinosqui Date: Sat, 14 Feb 2026 16:52:13 -0300 Subject: [PATCH 3/4] fix: review all branches when pushing multiple refs Exit only on failure instead of unconditionally after the first review, so all pushed branches get reviewed. Co-Authored-By: Claude Opus 4.6 --- src/commands/hook/install.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands/hook/install.ts b/src/commands/hook/install.ts index 0f18ba9..882cf27 100644 --- a/src/commands/hook/install.ts +++ b/src/commands/hook/install.ts @@ -55,8 +55,9 @@ while read local_ref local_sha remote_ref remote_sha; do fi # Review changes not yet on the remote - kodus review --branch "\${remote}/\${branch_name}" ${reviewFlags} - exit $? + if ! kodus review --branch "\${remote}/\${branch_name}" ${reviewFlags}; then + exit 1 + fi done `; } From 1c785f97444c3687e2bdcba3d92872f5e1219525 Mon Sep 17 00:00:00 2001 From: Gabriel Malinosqui Date: Sat, 14 Feb 2026 16:57:44 -0300 Subject: [PATCH 4/4] fix: update integration tests to expect pre-push hook Co-Authored-By: Claude Opus 4.6 --- tests/integration/cli.integration.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/cli.integration.test.ts b/tests/integration/cli.integration.test.ts index 53633a9..8d5b801 100644 --- a/tests/integration/cli.integration.test.ts +++ b/tests/integration/cli.integration.test.ts @@ -231,13 +231,13 @@ describe('auth status integration', () => { // Hook commands — install, status, uninstall // --------------------------------------------------------------------------- describe('hook integration', () => { - it('kodus hook install creates pre-commit hook', async () => { + it('kodus hook install creates pre-push hook', async () => { const { stdout, stderr, exitCode } = await runCli(['hook', 'install', '--force']); expect(exitCode).toBe(0); const output = stdout + stderr; expect(output).toContain('installed'); - const hookPath = path.join(gitRepoDir, '.git', 'hooks', 'pre-commit'); + const hookPath = path.join(gitRepoDir, '.git', 'hooks', 'pre-push'); const content = await fs.readFile(hookPath, 'utf-8'); expect(content).toContain('# kodus-hook'); expect(content).toContain('--fail-on critical'); @@ -263,7 +263,7 @@ describe('hook integration', () => { const output = stdout + stderr; expect(output).toContain('removed'); - const hookPath = path.join(gitRepoDir, '.git', 'hooks', 'pre-commit'); + const hookPath = path.join(gitRepoDir, '.git', 'hooks', 'pre-push'); await expect(fs.access(hookPath)).rejects.toThrow(); }); });