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
18 changes: 18 additions & 0 deletions .claude/commands/musafety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# /musafety

Run a musafety check-and-repair workflow for the current repository.

## Steps

1. Run `musafety status`.
2. If status is degraded, run `musafety doctor`.
3. If still degraded, run `musafety scan` and summarize each finding with a fix.
4. Report final verdict as one of:
- `Repo is musafe`
- `Repo is not musafe` (include blockers)

## Style

- Keep output short and operational.
- Include exact commands you executed.
- Prefer concrete next actions over generic advice.
35 changes: 35 additions & 0 deletions .codex/skills/musafety/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
name: musafety
description: "Use when you need to check, repair, or bootstrap multi-agent safety guardrails in this repository."
---

# musafety (Codex skill)

Use this skill whenever branch safety, lock ownership, or guardrail setup may be broken.

## Fast path

1. Run `musafety status`.
2. If repo safety is degraded, run `musafety doctor`.
3. If issues remain, run `musafety scan` and address the findings.

## Setup path

If guardrails are missing entirely, run:

```sh
musafety setup
```

Then verify:

```sh
musafety status
musafety scan
```

## Operator notes

- Prefer `musafety doctor` for one-step repair + verification.
- Keep agent work isolated (`agent/*` branches + lock claims).
- Do not bypass protected branch safeguards unless explicitly required.
135 changes: 135 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env bash
set -euo pipefail

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

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

is_vscode_git_context=0
if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" ]]; then
is_vscode_git_context=1
fi

allow_vscode_protected_branch_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH:-$(git config --get multiagent.protectedBranches.allowVSCode || true)}"
allow_vscode_protected_branch_raw="$(printf '%s' "${allow_vscode_protected_branch_raw:-}" | tr '[:upper:]' '[:lower:]')"
allow_vscode_protected_branch=0
case "$allow_vscode_protected_branch_raw" in
1|true|yes|on) allow_vscode_protected_branch=1 ;;
*) allow_vscode_protected_branch=0 ;;
esac

protected_branches_raw="${MUSAFETY_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
if [[ -z "$protected_branches_raw" ]]; then
protected_branches_raw="dev main master"
fi
protected_branches_raw="${protected_branches_raw//,/ }"

is_protected_branch=0
for protected_branch in $protected_branches_raw; do
if [[ "$branch" == "$protected_branch" ]]; then
is_protected_branch=1
break
fi
done

if [[ "$is_protected_branch" == "1" ]]; then
if [[ "$is_vscode_git_context" == "1" && "$allow_vscode_protected_branch" == "1" ]]; then
exit 0
fi

git_dir="$(git rev-parse --git-dir)"
if [[ -f "$git_dir/MERGE_HEAD" ]]; then
exit 0
fi

cat >&2 <<'MSG'
[agent-branch-guard] Direct commits on protected branches are blocked.
Use an agent branch first:
bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
After finishing work:
bash scripts/agent-branch-finish.sh

Optional repo override to allow VS Code Source Control commits:
git config multiagent.protectedBranches.allowVSCode true

Temporary bypass (not recommended):
ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
MSG
exit 1
fi

if [[ "$branch" == agent/* ]]; then
if ! python3 scripts/agent-file-locks.py validate --branch "$branch" --staged; then
cat >&2 <<'MSG'
[agent-branch-guard] Agent branch commits require file ownership locks.
Claim files first:
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
MSG
exit 1
fi

require_sync_before_commit_raw="$(git config --get multiagent.sync.requireBeforeCommit || true)"
if [[ -z "$require_sync_before_commit_raw" ]]; then
require_sync_before_commit_raw="false"
fi
require_sync_before_commit="$(printf '%s' "$require_sync_before_commit_raw" | tr '[:upper:]' '[:lower:]')"

should_require_sync=0
case "$require_sync_before_commit" in
1|true|yes|on) should_require_sync=1 ;;
0|false|no|off) should_require_sync=0 ;;
*) should_require_sync=0 ;;
esac

if [[ "$should_require_sync" == "1" ]]; then
base_branch="$(git config --get multiagent.baseBranch || true)"
if [[ -z "$base_branch" ]]; then
base_branch="dev"
fi

max_behind_raw="$(git config --get multiagent.sync.maxBehindCommits || true)"
if [[ -z "$max_behind_raw" ]]; then
max_behind_raw="0"
fi
if [[ ! "$max_behind_raw" =~ ^[0-9]+$ ]]; then
echo "[agent-sync-guard] Invalid multiagent.sync.maxBehindCommits value: ${max_behind_raw}" >&2
echo "[agent-sync-guard] Expected non-negative integer. Example: git config multiagent.sync.maxBehindCommits 0" >&2
exit 1
fi

if ! git fetch origin "$base_branch" --quiet >/dev/null 2>&1; then
echo "[agent-sync-guard] Unable to fetch origin/${base_branch} while commit sync gate is enabled." >&2
echo "[agent-sync-guard] Disable gate temporarily with: git config multiagent.sync.requireBeforeCommit false" >&2
exit 1
fi

if ! git show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
echo "[agent-sync-guard] Remote base branch not found: origin/${base_branch}" >&2
exit 1
fi

behind_count="$(git rev-list --left-right --count "${branch}...origin/${base_branch}" 2>/dev/null | awk '{print $2}')"
behind_count="${behind_count:-0}"
max_behind="${max_behind_raw}"

if [[ "$behind_count" -gt "$max_behind" ]]; then
cat >&2 <<MSG
[agent-sync-guard] Commit blocked: '${branch}' is behind origin/${base_branch} by ${behind_count} commit(s) (max allowed: ${max_behind}).
Run:
musafety sync --base ${base_branch}
Or relax threshold:
git config multiagent.sync.maxBehindCommits <n>
MSG
exit 1
fi
fi
fi

if command -v pre-commit >/dev/null 2>&1 && [[ -f .pre-commit-config.yaml ]]; then
pre-commit run --hook-stage pre-commit
fi
67 changes: 67 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail

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

is_vscode_git_context=0
if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" ]]; then
is_vscode_git_context=1
fi

allow_vscode_protected_branch_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH:-$(git config --get multiagent.protectedBranches.allowVSCode || true)}"
allow_vscode_protected_branch_raw="$(printf '%s' "${allow_vscode_protected_branch_raw:-}" | tr '[:upper:]' '[:lower:]')"
allow_vscode_protected_branch=0
case "$allow_vscode_protected_branch_raw" in
1|true|yes|on) allow_vscode_protected_branch=1 ;;
*) allow_vscode_protected_branch=0 ;;
esac

if [[ "$is_vscode_git_context" == "1" && "$allow_vscode_protected_branch" == "1" ]]; then
exit 0
fi

protected_branches_raw="${MUSAFETY_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
if [[ -z "$protected_branches_raw" ]]; then
protected_branches_raw="dev main master"
fi
protected_branches_raw="${protected_branches_raw//,/ }"

is_protected_branch() {
local branch="$1"
for protected_branch in $protected_branches_raw; do
if [[ "$branch" == "$protected_branch" ]]; then
return 0
fi
done
return 1
}

blocked_refs=()
while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
if [[ -z "${remote_ref:-}" || "$remote_ref" != refs/heads/* ]]; then
continue
fi

remote_branch="${remote_ref#refs/heads/}"
if is_protected_branch "$remote_branch"; then
blocked_refs+=("$remote_branch")
fi
done

if [[ "${#blocked_refs[@]}" -gt 0 ]]; then
{
echo "[agent-branch-guard] Push to protected branch blocked."
echo "[agent-branch-guard] Protected target(s): ${blocked_refs[*]}"
echo "[agent-branch-guard] Push from an agent branch and merge via PR."
echo "[agent-branch-guard] Optional repo override for VS Code Source Control:"
echo " git config multiagent.protectedBranches.allowVSCode true"
echo
echo "Temporary bypass (not recommended):"
echo " ALLOW_PUSH_ON_PROTECTED_BRANCH=1 git push ..."
} >&2
exit 1
fi

exit 0
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.omx/
node_modules
node_modules
oh-my-codex/
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ Example output:
npm i -g musafety
musafety setup
musafety doctor
bash scripts/codex-agent.sh "task" "agent-name"
bash scripts/agent-branch-start.sh "task" "agent-name"
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)"
Expand Down Expand Up @@ -157,6 +158,7 @@ Use this exact checklist to setup multi-agent safety in this repository for Code
musafety doctor

4) Confirm next safe agent workflow commands:
bash scripts/codex-agent.sh "task" "agent-name"
bash scripts/agent-branch-start.sh "task" "agent-name"
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)"
Expand Down Expand Up @@ -275,13 +277,15 @@ multiagent.protectedBranches
- risky stale/missing lock state
- accidental loss of critical guardrail files
- setup also writes a managed `.gitignore` block so generated musafety scripts/hooks stay out of normal git status noise by default
- includes `oh-my-codex/` by default to keep local OMX source clones out of repo status
- pass `--no-gitignore` if you want to keep tracking these files in git

## Files it installs

```text
scripts/agent-branch-start.sh
scripts/agent-branch-finish.sh
scripts/codex-agent.sh
scripts/agent-worktree-prune.sh
scripts/agent-file-locks.py
scripts/install-agent-git-hooks.sh
Expand Down
7 changes: 7 additions & 0 deletions bin/multiagent-safety.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const TEMPLATE_ROOT = path.resolve(__dirname, '..', 'templates');
const TEMPLATE_FILES = [
'scripts/agent-branch-start.sh',
'scripts/agent-branch-finish.sh',
'scripts/codex-agent.sh',
'scripts/agent-worktree-prune.sh',
'scripts/agent-file-locks.py',
'scripts/install-agent-git-hooks.sh',
Expand All @@ -39,6 +40,7 @@ const TEMPLATE_FILES = [
const EXECUTABLE_RELATIVE_PATHS = new Set([
'scripts/agent-branch-start.sh',
'scripts/agent-branch-finish.sh',
'scripts/codex-agent.sh',
'scripts/agent-worktree-prune.sh',
'scripts/agent-file-locks.py',
'scripts/install-agent-git-hooks.sh',
Expand All @@ -61,11 +63,13 @@ const GITIGNORE_MARKER_END = '# multiagent-safety:END';
const MANAGED_GITIGNORE_PATHS = [
'scripts/agent-branch-start.sh',
'scripts/agent-branch-finish.sh',
'scripts/codex-agent.sh',
'scripts/agent-worktree-prune.sh',
'scripts/agent-file-locks.py',
'scripts/install-agent-git-hooks.sh',
'scripts/openspec/init-plan-workspace.sh',
'.githooks/pre-commit',
'oh-my-codex/',
'.codex/skills/musafety/SKILL.md',
'.claude/commands/musafety.md',
LOCK_FILE_RELATIVE,
Expand Down Expand Up @@ -132,6 +136,7 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup multi-agent safety in
musafety doctor

4) Confirm next safe agent workflow commands:
bash scripts/codex-agent.sh "task" "agent-name"
bash scripts/agent-branch-start.sh "task" "agent-name"
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)"
Expand All @@ -150,6 +155,7 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup multi-agent safety in
const AI_SETUP_COMMANDS = `npm i -g musafety
musafety setup
musafety doctor
bash scripts/codex-agent.sh "task" "agent-name"
bash scripts/agent-branch-start.sh "task" "agent-name"
python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)"
Expand Down Expand Up @@ -462,6 +468,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
}

const wantedScripts = {
'agent:codex': 'bash ./scripts/codex-agent.sh',
'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
'agent:cleanup': 'bash ./scripts/agent-worktree-prune.sh --base dev',
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "musafety",
"version": "0.4.7",
"version": "0.4.9",
"description": "Simple setup command for hardened multi-agent collaboration safety in git repos.",
"license": "MIT",
"preferGlobal": true,
Expand Down
Loading
Loading