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
72 lines
3.2 KiB
JavaScript
Executable File
72 lines
3.2 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// PreToolUse(Bash) — only the dispatched `merger` agent may run merge-class git
|
|
// commands. The main orchestrator session and every other subagent (developer,
|
|
// reviewers, …) are blocked; if any of them falls back to merging itself, the
|
|
// team workflow looks healthy but is silently broken (the dev↔merger path is
|
|
// dead). The behavioral rule lives in orchestrator.md ("NEVER act as merger
|
|
// yourself"); this is the runtime guard.
|
|
//
|
|
// OPT-IN: inert unless HARNESS_ENFORCE_MERGE_GUARD=1 (deprecated fallback:
|
|
// ATOMIC_CRM_ENFORCE_MERGE_GUARD, kept for one release). In a plain Claude Code
|
|
// checkout the main session is a general assistant, and blocking its git
|
|
// merge/pull/checkout would break normal use, so it stays off until the
|
|
// orchestrator explicitly enables it before a team wave.
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import { basename } from "node:path";
|
|
import { createHookContext } from "./lib/context.mjs";
|
|
import { isMerger } from "./lib/teams.mjs";
|
|
import { loadConfig, launcher } from "./lib/config.mjs";
|
|
|
|
const mergeGuardEnabled =
|
|
process.env.HARNESS_ENFORCE_MERGE_GUARD === "1" ||
|
|
process.env.ATOMIC_CRM_ENFORCE_MERGE_GUARD === "1";
|
|
if (!mergeGuardEnabled) process.exit(0);
|
|
|
|
const input = JSON.parse(readFileSync(0, "utf8"));
|
|
const ctx = createHookContext(input, "block-orchestrator-merge");
|
|
|
|
// Allow only the dispatched `merger` agent through. The runtime carries its
|
|
// (possibly suffixed) identity in agentName or agentType, so check both.
|
|
if ([ctx.agentName, ctx.agentType].some(isMerger)) process.exit(0);
|
|
|
|
const cmd = input.tool_input?.command || "";
|
|
if (!cmd) process.exit(0);
|
|
|
|
// The post-checkout script (e.g. CRM Builder's apply-app-variant.sh) is a
|
|
// launcher extension point: its path comes from config.launcher.postCheckoutScript
|
|
// so no chat-service path is hardcoded here. When unset, that guard clause is
|
|
// inert (a project with no post-checkout script has nothing to gate).
|
|
let postCheckoutScript = "";
|
|
try {
|
|
postCheckoutScript = launcher(loadConfig()).postCheckoutScript || "";
|
|
} catch {
|
|
postCheckoutScript = "";
|
|
}
|
|
|
|
let blocked = "";
|
|
if (/(^|[;&|\s])git\s+merge(\s|$)/.test(cmd)) blocked = "git merge";
|
|
else if (/(^|[;&|\s])git\s+checkout\s+(master|main)(\s|$)/.test(cmd))
|
|
blocked = "git checkout master/main";
|
|
else if (/(^|[;&|\s])git\s+pull(\s|$)/.test(cmd)) blocked = "git pull";
|
|
else if (/(^|[;&|\s])git\s+worktree\s+remove(\s|$)/.test(cmd))
|
|
blocked = "git worktree remove";
|
|
else if (postCheckoutScript && cmd.includes(basename(postCheckoutScript)))
|
|
blocked = `${basename(postCheckoutScript)} (merger-only command)`;
|
|
|
|
if (blocked) {
|
|
ctx.fail(
|
|
`Blocked: only the dispatched \`merger\` agent may run "${blocked}".\n\n` +
|
|
"Rule: the orchestrator must NEVER act as merger, and neither may any other\n" +
|
|
"subagent. Only the `merger` runs merge-class git commands. See orchestrator.md\n" +
|
|
'"NEVER act as merger yourself".\n\n' +
|
|
"If you got here because the merger didn't report back, the team communication broke —\n" +
|
|
"the dev's \"ready\" message never reached the merger. Don't salvage by merging yourself;\n" +
|
|
"report the failure to the user and stop. Salvaging hides the bug.\n\n" +
|
|
`Blocked command:\n ${cmd}`,
|
|
{ log: `BLOCK ${blocked}` },
|
|
);
|
|
}
|
|
|
|
process.exit(0);
|