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>
123 lines
4.7 KiB
Bash
Executable File
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);
|
|
'
|