Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .githooks/post-merge
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail

if [[ "${MUSAFETY_DISABLE_POST_MERGE_CLEANUP:-0}" == "1" ]]; then
exit 0
fi

repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -z "$repo_root" ]]; then
exit 0
fi

branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
exit 0
fi

base_branch="${MUSAFETY_BASE_BRANCH:-$(git -C "$repo_root" config --get multiagent.baseBranch || true)}"
if [[ -z "$base_branch" ]]; then
base_branch="dev"
fi

if [[ "$branch" != "$base_branch" ]]; then
exit 0
fi

cli_path="$repo_root/bin/multiagent-safety.js"
if [[ ! -f "$cli_path" ]]; then
exit 0
fi

node_bin="${MUSAFETY_NODE_BIN:-node}"
if ! command -v "$node_bin" >/dev/null 2>&1; then
exit 0
fi

"$node_bin" "$cli_path" cleanup \
--target "$repo_root" \
--base "$base_branch" \
--keep-clean-worktrees >/dev/null 2>&1 || true

exit 0
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ scripts/install-agent-git-hooks.sh
scripts/openspec/init-plan-workspace.sh
.githooks/pre-commit
.githooks/pre-push
.githooks/post-merge
oh-my-codex/
.codex/skills/guardex/SKILL.md
.claude/commands/guardex.md
Expand Down
5 changes: 5 additions & 0 deletions bin/multiagent-safety.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const TEMPLATE_FILES = [
'scripts/openspec/init-plan-workspace.sh',
'githooks/pre-commit',
'githooks/pre-push',
'githooks/post-merge',
'codex/skills/guardex/SKILL.md',
'codex/skills/guardex-merge-skills-to-dev/SKILL.md',
'claude/commands/guardex.md',
Expand All @@ -63,6 +64,7 @@ const REQUIRED_WORKFLOW_FILES = [
'scripts/agent-file-locks.py',
'scripts/install-agent-git-hooks.sh',
'.githooks/pre-commit',
'.githooks/post-merge',
'.omx/state/agent-file-locks.json',
];

Expand All @@ -87,12 +89,14 @@ const EXECUTABLE_RELATIVE_PATHS = new Set([
'scripts/openspec/init-plan-workspace.sh',
'.githooks/pre-commit',
'.githooks/pre-push',
'.githooks/post-merge',
]);

const CRITICAL_GUARDRAIL_PATHS = new Set([
'AGENTS.md',
'.githooks/pre-commit',
'.githooks/pre-push',
'.githooks/post-merge',
'scripts/agent-branch-start.sh',
'scripts/agent-branch-finish.sh',
'scripts/agent-worktree-prune.sh',
Expand All @@ -118,6 +122,7 @@ const MANAGED_GITIGNORE_PATHS = [
'scripts/openspec/init-plan-workspace.sh',
'.githooks/pre-commit',
'.githooks/pre-push',
'.githooks/post-merge',
'oh-my-codex/',
'.codex/skills/guardex/SKILL.md',
'.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
Expand Down
1 change: 1 addition & 0 deletions scripts/agent-file-locks.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
'AGENTS.md',
'.githooks/pre-commit',
'.githooks/pre-push',
'.githooks/post-merge',
'scripts/agent-branch-start.sh',
'scripts/agent-branch-finish.sh',
'scripts/agent-file-locks.py',
Expand Down
42 changes: 42 additions & 0 deletions templates/githooks/post-merge
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail

if [[ "${MUSAFETY_DISABLE_POST_MERGE_CLEANUP:-0}" == "1" ]]; then
exit 0
fi

repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -z "$repo_root" ]]; then
exit 0
fi

branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
exit 0
fi

base_branch="${MUSAFETY_BASE_BRANCH:-$(git -C "$repo_root" config --get multiagent.baseBranch || true)}"
if [[ -z "$base_branch" ]]; then
base_branch="dev"
fi

if [[ "$branch" != "$base_branch" ]]; then
exit 0
fi

cli_path="$repo_root/bin/multiagent-safety.js"
if [[ ! -f "$cli_path" ]]; then
exit 0
fi

node_bin="${MUSAFETY_NODE_BIN:-node}"
if ! command -v "$node_bin" >/dev/null 2>&1; then
exit 0
fi

"$node_bin" "$cli_path" cleanup \
--target "$repo_root" \
--base "$base_branch" \
--keep-clean-worktrees >/dev/null 2>&1 || true

exit 0
51 changes: 51 additions & 0 deletions test/install.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ test('setup provisions workflow files and repo config', () => {
'scripts/openspec/init-plan-workspace.sh',
'.githooks/pre-commit',
'.githooks/pre-push',
'.githooks/post-merge',
'.codex/skills/guardex/SKILL.md',
'.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
'.claude/commands/guardex.md',
Expand Down Expand Up @@ -320,6 +321,7 @@ test('setup provisions workflow files and repo config', () => {
assert.match(gitignoreContent, /scripts\/agent-file-locks\.py/);
assert.match(gitignoreContent, /\.githooks\/pre-commit/);
assert.match(gitignoreContent, /\.githooks\/pre-push/);
assert.match(gitignoreContent, /\.githooks\/post-merge/);
assert.match(gitignoreContent, /\.omx\//);
assert.match(gitignoreContent, /oh-my-codex\//);
assert.match(gitignoreContent, /\.codex\/skills\/guardex\/SKILL\.md/);
Expand Down Expand Up @@ -1824,6 +1826,55 @@ test('pre-push blocks codex protected branch pushes even from VS Code Source Con
assert.match(hookResult.stderr, /\[guardex-preedit-guard\] Codex push detected toward protected branch\./);
});

test('post-merge auto-runs cleanup on base branch and skips non-base branches', () => {
const repoDir = initRepo();
seedCommit(repoDir);

const setupResult = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir);
assert.equal(setupResult.status, 0, setupResult.stderr || setupResult.stdout);

const markerPath = path.join(repoDir, '.post-merge-cleanup-args');
fs.writeFileSync(
path.join(repoDir, 'bin', 'multiagent-safety.js'),
'#!/usr/bin/env node\n' +
"const fs = require('node:fs');\n" +
"const marker = process.env.MUSAFETY_POST_MERGE_MARKER;\n" +
"if (marker) fs.appendFileSync(marker, process.argv.slice(2).join(' ') + '\\n', 'utf8');\n",
'utf8',
);

let result = runCmd('bash', ['.githooks/post-merge', '0'], repoDir, {
MUSAFETY_POST_MERGE_MARKER: markerPath,
});
assert.equal(result.status, 0, result.stderr || result.stdout);

let invocations = fs
.readFileSync(markerPath, 'utf8')
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
assert.equal(invocations.length, 1);
assert.match(invocations[0], /^cleanup /);
assert.match(invocations[0], new RegExp(`--target ${escapeRegexLiteral(repoDir)}`));
assert.match(invocations[0], /--base dev/);
assert.match(invocations[0], /--keep-clean-worktrees/);

result = runCmd('git', ['checkout', '-b', 'feature/post-merge-skip'], repoDir);
assert.equal(result.status, 0, result.stderr || result.stdout);

result = runCmd('bash', ['.githooks/post-merge', '0'], repoDir, {
MUSAFETY_POST_MERGE_MARKER: markerPath,
});
assert.equal(result.status, 0, result.stderr || result.stdout);

invocations = fs
.readFileSync(markerPath, 'utf8')
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
assert.equal(invocations.length, 1, 'post-merge should skip cleanup on non-base branch');
});

test('codex-agent launches codex inside a fresh sandbox worktree and keeps branch/worktree by default', () => {
const repoDir = initRepo();
seedCommit(repoDir);
Expand Down