#!/usr/bin/env node /** * Rule #4 — Require fresh verification artifact before git commit / push. * * PreToolUse on Bash. If command is git commit / push, check the * verify-pass-.json sentinel: * - missing → block * - age > MAX_AGE_SEC → block ("stale") * - result !== 'pass' → block ("last run failed") * * Override phrases: `срочно` / `быстрый коммит` / `ремонт инфраструктуры`. * * Spec: docs/superpowers/specs/2026-05-25-enforce-hard-rules-design.md */ import { readStdin, parseEventJson, readTranscript, lastUserPromptText, findOverride, findOverrideAttempt, logOverride, exitDecision, detectGitCommandKind, readSentinel, sentinelAgeSec, isDocsOnlyChange, listChangedFiles, } from './enforce-hook-helpers.mjs'; const RULE_KEY_COMMIT = 'verify-before-commit'; const RULE_KEY_PUSH = 'verify-before-push'; const MAX_AGE_SEC = 30 * 60; // 30 min export function decide({ toolName, command, sentinel, sentinelAge, override, overrideAttempt, changedPaths }) { if (toolName !== 'Bash' || typeof command !== 'string') return { block: false }; const kind = detectGitCommandKind(command); if (kind !== 'commit' && kind !== 'push') return { block: false }; if (override) return { block: false }; // Docs-only short-circuit (2026-05-27): if EVERY staged/unpushed file is a // `.md` document, the regression gate adds no value — there's no executable // code in the change set, so a fresh test run can't tell us anything new. // Empty/missing changedPaths → unknown → fall through to normal checks. if (isDocsOnlyChange(changedPaths)) return { block: false }; // Silent-reject bug fix (2026-05-26): when user typed an override phrase that // requires justification (e.g. "ремонт инфраструктуры") but forgot the // " " line, emit an explicit diagnostic — not the generic // "no verification artifact" message that misled users into thinking the // override mechanism was broken. if (overrideAttempt && overrideAttempt.requires_justification) { return { block: true, message: [ `[enforce-verify-before-push] Override phrase "${overrideAttempt.phrase}" found, but missing justification line.`, `Add a line "${overrideAttempt.requires_justification} " in the SAME prompt.`, ``, `Example:`, ` ${overrideAttempt.phrase}`, ` ${overrideAttempt.requires_justification} observer refresh after brainstorm session`, ].join('\n'), }; } if (!sentinel) { return { block: true, message: [ `[enforce-verify-before-push] No verification artifact found.`, `Run a full test suite first (vitest run / composer test) before \`git ${kind}\`.`, ].join('\n'), }; } if (sentinel.result !== 'pass') { return { block: true, message: [ `[enforce-verify-before-push] Last verification FAILED (result=${sentinel.result}, exit=${sentinel.exit_code}).`, `Tests: ${sentinel.tests_passed}/${sentinel.tests_total} passed, ${sentinel.tests_failed} failed.`, `Re-run the suite and address failures before \`git ${kind}\`.`, ].join('\n'), }; } if (sentinelAge !== null && sentinelAge > MAX_AGE_SEC) { return { block: true, message: [ `[enforce-verify-before-push] Verification artifact is stale (age ${sentinelAge}s > ${MAX_AGE_SEC}s).`, `Re-run the full test suite before \`git ${kind}\`.`, ].join('\n'), }; } return { block: false }; } 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 kind = detectGitCommandKind(command); const ruleKey = kind === 'commit' ? RULE_KEY_COMMIT : RULE_KEY_PUSH; const override = findOverride(userPrompt, ruleKey); if (override) logOverride(ruleKey, override, event.session_id); const overrideAttempt = override ? null : findOverrideAttempt(userPrompt, ruleKey); const sentinel = readSentinel('verify-pass', event.session_id); const age = sentinelAgeSec('verify-pass', event.session_id); const changedPaths = (kind === 'commit' || kind === 'push') ? listChangedFiles(kind) : []; const result = decide({ toolName, command, sentinel, sentinelAge: age, override, overrideAttempt, changedPaths }); exitDecision(result); } catch { exitDecision({ block: false }); } } const isCli = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/enforce-verify-before-push.mjs'); if (isCli) main();