#!/usr/bin/env node /** * Rule #7 โ€” Branch-switch detection before commit / push. * * PreToolUse on Bash. Detects `git commit`, `git push`, `git cherry-pick`, * `git reset --hard`, `git rebase`, `git branch -f/-d`. Reads expected branch * from sentinel; if missing, defaults to "main". Compares to actual current * branch via `git branch --show-current`. Mismatch โ†’ block unless explicit * confirmation marker in last assistant text OR override phrase. * * Confirmation markers in assistant response (case-sensitive substring): * - BRANCH-SWITCH-CONFIRMED * - RECOVERY-INTENT: * Override phrases: "recovery" (suppresses branch-switch + git-recovery rule keys) * * Spec: docs/superpowers/specs/2026-05-25-enforce-hard-rules-design.md */ import { readStdin, parseEventJson, readTranscript, lastUserPromptText, lastAssistantText, findOverride, logOverride, exitDecision, detectGitCommandKind, readGitBranch, getExpectedBranch, } from './enforce-hook-helpers.mjs'; const RULE_KEY = 'branch-switch'; const CONFIRMATION_MARKERS = [ 'BRANCH-SWITCH-CONFIRMED', 'RECOVERY-INTENT:', ]; export function decide({ toolName, command, expectedBranch, actualBranch, assistantText, override, }) { if (toolName !== 'Bash' || typeof command !== 'string') return { block: false }; const kind = detectGitCommandKind(command); if (!kind) return { block: false }; if (override) return { block: false }; const exp = (expectedBranch || 'main').trim(); const act = (actualBranch || '').trim(); if (!act || act === exp) return { block: false }; for (const marker of CONFIRMATION_MARKERS) { if (assistantText && assistantText.includes(marker)) return { block: false }; } return { block: true, message: [ `[enforce-branch-switch] About to run \`git ${kind}\` on branch "${act}" but expected "${exp}".`, `Likely cause: parallel session switched HEAD silently (see Pravila ยง15.1).`, ``, `If intentional โ€” write one of these in your next response BEFORE running the command:`, ` BRANCH-SWITCH-CONFIRMED (you intend to commit on ${act})`, ` RECOVERY-INTENT: (recovery operation, e.g., cherry-pick to main)`, ``, `Or include the override phrase "recovery" in the user's next prompt.`, ].join('\n'), }; } async function main() { try { const raw = await readStdin(); const event = parseEventJson(raw); const toolName = event.tool_name || ''; const command = (event.tool_input && event.tool_input.command) || ''; const transcript = readTranscript(event.transcript_path); const userPrompt = lastUserPromptText(transcript); const override = findOverride(userPrompt, RULE_KEY); if (override) logOverride(RULE_KEY, override, event.session_id); const expected = getExpectedBranch(event.session_id) || 'main'; const actual = readGitBranch(); const assistantText = lastAssistantText(transcript); const result = decide({ toolName, command, expectedBranch: expected, actualBranch: actual, assistantText, override, }); exitDecision(result); } catch { exitDecision({ block: false }); } } const isCli = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/enforce-branch-switch.mjs'); if (isCli) main();