Files
brain/tools/enforce-verify-before-push.mjs
T

126 lines
4.7 KiB
JavaScript

#!/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-<session>.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
// "<prefix> <reason>" 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} <one-line причина>" 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();