/*
RFC-0821: installWorkpieceCommitHook — writes a pre-commit guard to the
workpiece git repo that rejects raw `git commit` without MISSION_GIT_COMMIT=1 env var.
This enforces the commit discipline rule: all workpiece commits must go through
mission.git.commit, not raw git commit.
RFC-0878: also checks for .closed sentinel file — refuses all commits (including
MISSION_GIT_COMMIT=1 bypass) when the mission is closed.
- Does not install hooks in cache clones — the workspace-level hooks/pre-commit
handles platform-scope blocking via ECOSYSTEM_COMMIT env var.
- Does not block --no-verify commits — internal platform helpers
(commitWorkpieceIfDirty, commitCacheCloneIfDirty) use --no-verify intentionally.
- RFC-0821: initial installWorkpieceCommitHook implementation.
- RFC-0878: add .closed sentinel check — block commits in closed workpieces even with MISSION_GIT_COMMIT=1.
- RFC-1199: sentinel-aware refusal message for MATERIALIZATION-IN-PROGRESS; hook now installs during setup-staging (rides atomic-move) instead of post-commit.
*/
import path from "node:path";
import { writeFileIfChanged } from "@warpgogol/werkstatt-engine/kernel";
import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
export interface WorkpieceHookResult {
installed: boolean;
hookPath: string;
}
const HOOK_SCRIPT = `#!/bin/sh
# Warpgogol workpiece commit guard (RFC-0821 + RFC-0878 + RFC-1199)
# Rejects raw git commit — use mission.git.commit instead:
# pnpm exec werkstatt run mission.git.commit --mission= --message=""
if [ -z "\${MISSION_GIT_COMMIT:-}" ]; then
# RFC-1199: name the materialization window explicitly when its sentinel is
# present — during that window there is no baseline commit to build on.
if [ -f "$(pwd)/.git/MATERIALIZATION-IN-PROGRESS" ]; then
echo "ERROR: Commit blocked — workpiece materialization is still in progress (RFC-1199)." >&2
echo "" >&2
echo "The pipeline has not created the baseline commit yet." >&2
echo "Wait for mission.materialize to finish; if it failed, run:" >&2
echo " pnpm exec werkstatt run mission.resume --mission " >&2
exit 1
fi
echo "ERROR: Direct git commit blocked in workpiece (RFC-0821)." >&2
echo "" >&2
echo "Use mission.git.commit instead:" >&2
echo " pnpm exec werkstatt run mission.git.commit --mission= --message=\"\"" >&2
echo "" >&2
echo "Raw git commit bypasses pre-commit validators, bordbuch recording, and signed-commit support." >&2
exit 1
fi
# RFC-0878: Block all commits (including MISSION_GIT_COMMIT bypass) in closed workpieces.
# The .closed sentinel is written by mission.close as a final step.
if [ -f "$(pwd)/.closed" ]; then
echo "ERROR: Commit blocked — workpiece is closed (RFC-0878)." >&2
echo "" >&2
echo "The mission for this workpiece has been closed. No commits are allowed." >&2
echo "If you need to make changes, open a new mission or reopen this one." >&2
exit 1
fi
`;
export async function installWorkpieceCommitHook(
workpieceDir: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
const hooksDir = path.join(workpieceDir, ".git", "hooks");
const hookPath = path.join(hooksDir, "pre-commit");
if (!(await io.exists(path.join(workpieceDir, ".git")))) {
return { installed: false, hookPath };
}
if (!(await io.exists(hooksDir))) {
await io.mkdir(hooksDir);
}
await writeFileIfChanged(hookPath, HOOK_SCRIPT);
const chmodResult = await io.exec("chmod", ["755", hookPath]);
if (chmodResult.exitCode !== 0) {
throw new Error(
`chmod ${hookPath} exited ${chmodResult.exitCode}: ${chmodResult.stderr.slice(0, 200)}`,
);
}
return { installed: true, hookPath };
}