Files
upstream-atomic-crm/.claude/hooks/block-merger-without-review.mjs
upstream-mirror 93ecc1a6f8
Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
snapshot of https://github.com/marmelab/atomic-crm.git (main, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:48:50 +05:00

39 lines
2.0 KiB
JavaScript
Executable File

#!/usr/bin/env node
// PreToolUse(Agent) — block dispatching a per-ticket merger until the
// quality-reviewer has recorded APPROVED for that ticket. With no SendMessage
// handshake, nothing else stops the orchestrator from going developer -> merger
// directly and skipping review; this gate enforces the
// dev -> reviewer -> (APPROVED) -> merger ordering structurally.
//
// Verdicts are recorded by record-review-verdict.mjs (SubagentStop) as flags
// under <sessionDir>/reviews/<TASK>-<role>. Skipped for the SIMPLE flow, the
// migration round, promotion-only, and rollback dispatches (no per-ticket
// review), and when the ticket can't be identified (fail open, never wedge the flow).
import { existsSync, readFileSync } from "node:fs";
import { createHookContext } from "./lib/context.mjs";
import { parseDispatch } from "./lib/dispatch-parse.mjs";
import { REVIEW_ROLES, reviewFlag } from "./lib/reviews.mjs";
const input = JSON.parse(readFileSync(0, "utf8"));
const ctx = createHookContext(input, "block-merger-without-review");
const d = parseDispatch(input);
if (d.subagentType !== "merger") process.exit(0); // only gate merger dispatches
if (d.mode === "promote") process.exit(0); // promotion-only carries no per-ticket review
if (["SIMPLE", "MIGRATION", "PROMOTE", "ROLLBACK"].includes(d.taskId))
process.exit(0);
if (!/^TASK-\d+$/.test(d.taskId)) process.exit(0); // can't identify a ticket → fail open
const missing = REVIEW_ROLES.filter(
(role) => !existsSync(reviewFlag(ctx, d.taskId, role)),
);
if (missing.length === 0) process.exit(0); // APPROVED → allow the merge
ctx.fail(
`Refusing to dispatch the merger for ${d.taskId}: no APPROVED verdict from ${missing.join(" and ")} yet.\n` +
"The flow is: developer -> quality-reviewer -> (APPROVED) -> merger.\n" +
`Dispatch quality-reviewer-${d.taskId} first (STATE B transition), then dispatch the merger only after it returns APPROVED.`,
{ log: `BLOCK ${d.taskId} missing=${missing.join(",")}` },
);