#!/usr/bin/env bash # BMAD Plan Review Gate — PreToolUse hook for ExitPlanMode. # # Enforces CLAUDE.md "Multi-Agent Plan Review Gate": blocks ExitPlanMode # until /tmp/bmad-panel-verdicts-.json contains verdicts from # all 7 BMAD panel agents (analyst, architect, pm, sm, ux-designer, dev, tea). # # Uses /usr/bin/node (always present; no jq dependency which is missing in # this box's non-interactive PATH). Fail-closed via permissionDecision=deny # in hookSpecificOutput JSON on stdout. # # The Claude session is responsible for writing verdicts via Agent calls — # this hook only validates the file exists and is well-formed. set -eu export HOOK_PAYLOAD HOOK_PAYLOAD="$(cat)" /usr/bin/node -e ' const raw = process.env.HOOK_PAYLOAD || ""; let payload; try { payload = JSON.parse(raw); } catch (e) { // Malformed payload — allow through; not our job to second-guess. process.exit(0); } if ((payload.tool_name || "") !== "ExitPlanMode") process.exit(0); const fs = require("fs"); const sessionId = payload.session_id || "unknown"; const verdictFile = "/tmp/bmad-panel-verdicts-" + sessionId + ".json"; function deny(reason) { process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } })); process.exit(0); } if (!fs.existsSync(verdictFile)) { deny("BMAD plan review gate: verdicts file missing at " + verdictFile + ". Invoke the /bmad-plan skill (or launch the 7 panel agents manually) " + "and write verdicts to this path before ExitPlanMode. See CLAUDE.md > Plan Review Gate."); } let doc; try { doc = JSON.parse(fs.readFileSync(verdictFile, "utf8")); } catch (e) { deny("BMAD plan review gate: " + verdictFile + " is not valid JSON (" + e.message + ")."); } const verdicts = Array.isArray(doc && doc.verdicts) ? doc.verdicts : null; if (!verdicts || verdicts.length < 7) { const n = verdicts ? verdicts.length : 0; deny("BMAD plan review gate: " + verdictFile + " has only " + n + "/7 panel verdicts. All 7 (analyst, architect, pm, sm, ux-designer, dev, tea) are required."); } const missing = verdicts.filter(function (v) { return !v || !v.agent || !v.verdict; }).length; if (missing > 0) { deny("BMAD plan review gate: " + missing + " verdict entries missing .agent or .verdict fields."); } const rejects = verdicts.filter(function (v) { return String(v.verdict).toUpperCase() === "REJECT"; }).length; if (rejects > 0) { deny("BMAD plan review gate: " + rejects + " panel agent(s) returned REJECT. Revise the plan or escalate before ExitPlanMode."); } // Foundational Principles gate (two-tier). // Tier-1 principles MUST be scored on every verdict. // Tier-2 principles are scored only when declared in doc.applicable_tier2. // See .clinerules/12-foundational-principles.md for doctrine and rubric. var TIER1 = ["SEBP", "SSoT", "FPT", "DiDSP", "PbD", "OF"]; var TIER2_ALLOWED = ["RBR", "A11y", "Testability"]; var MIN_SCORE = 3; var declaredTier2 = Array.isArray(doc.applicable_tier2) ? doc.applicable_tier2.slice() : []; var invalidTier2 = declaredTier2.filter(function (k) { return TIER2_ALLOWED.indexOf(k) === -1; }); if (invalidTier2.length > 0) { deny("BMAD plan review gate: applicable_tier2 contains unknown principle(s): " + invalidTier2.join(", ") + ". Allowed values: " + TIER2_ALLOWED.join(", ") + ". See .clinerules/12-foundational-principles.md > Tier-2 Principles."); } var requiredKeys = TIER1.concat(declaredTier2); var principleViolations = []; verdicts.forEach(function (v) { var p = v && v.principles; if (!p || typeof p !== "object") { principleViolations.push((v && v.agent ? v.agent : "?") + ": missing principles object"); return; } requiredKeys.forEach(function (key) { var entry = p[key]; if (!entry || typeof entry.score !== "number") { principleViolations.push(v.agent + "." + key + ": missing or non-numeric score"); } else if (entry.score < MIN_SCORE) { principleViolations.push(v.agent + "." + key + ": score " + entry.score + " < " + MIN_SCORE); } else if (typeof entry.rationale !== "string" || !entry.rationale.trim()) { principleViolations.push(v.agent + "." + key + ": missing rationale"); } }); }); if (principleViolations.length > 0) { var tier2Note = declaredTier2.length > 0 ? " plus declared tier-2 {" + declaredTier2.join(",") + "}" : " (no tier-2 declared)"; deny("BMAD plan review gate: foundational principle scoring failed:\n - " + principleViolations.join("\n - ") + "\n\nEvery verdict must include principles for tier-1 {" + TIER1.join(",") + "}" + tier2Note + " with .score >= " + MIN_SCORE + " and a non-empty .rationale. " + "See .clinerules/12-foundational-principles.md."); } process.exit(0); '