Files
stargue-publishing-engine/.claude/hooks/gate-plan-exit.sh
Angelo B. J. Luidens 1dc1a1a07a Stage 0: governance scaffolding + monorepo bootstrap
Phase 1 foundation for the Stargue Publishing Engine (plan v2, BMAD
panel-reviewed 2026-04-19 — 1 APPROVE, 6 REVISE, 0 REJECT; all principles >=3).

- Governance doctrine adopted from DQMS
  (.clinerules/12-foundational-principles.md,
  .claude/hooks/gate-plan-exit.sh, .claude/skills/bmad-plan/SKILL.md)
- Bun workspaces + Turbo; apps/{mcp-linkedin,scheduler,admin};
  packages/{schema,sanitize,linkedin-client,observability}
- Drizzle schema (content, publications, approvals, metrics,
  linkedin_tokens, audit, outlet_feature_flags) with idempotency_key
  UNIQUE and kill-switch table per TEA/dev panel revisions
- LinkedIn API canon: Posts API /rest/posts (not legacy UGC); OAuth
  auth-code without PKCE; secretbox (not sealed-box); Community
  Management API as separate approval gate from MDP
- Frontmatter Zod schema (status, language, outlets[], sanitize,
  scheduled, version)
- Pino observability with PII redaction
- Expand-then-contract migration runbook
- Plan + panel verdicts mirrored to docs/plans/
- Deferred gates logged (Dokploy PaaS verification, LinkedIn Dev
  Portal app registration)

bun install + bun run typecheck both exit 0 across 11 workspaces.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 07:22:07 -04:00

123 lines
4.7 KiB
Bash
Executable File

#!/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-<session_id>.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);
'