Skip to content
Closed
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
15 changes: 14 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
.omx/
node_modules
node_modules

# multiagent-safety:START
scripts/agent-branch-start.sh
scripts/agent-branch-finish.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
.codex/skills/musafety/SKILL.md
.claude/commands/musafety.md
.omx/state/agent-file-locks.json
# multiagent-safety:END
61 changes: 61 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,64 @@ OMX runtime state typically lives under `.omx/`:
- `.omx/project-memory.json`
- `.omx/plans/`
- `.omx/logs/`

<!-- multiagent-safety:START -->
## Multi-Agent Execution Contract (multiagent-safety)

0. Session plan comment + read gate (required)

- Before editing, each agent must post a short session comment/handoff note that includes:
- plan/change name (or checkpoint id),
- owned files/scope,
- intended action.
- Before deleting/replacing code, each agent must read the latest session comments/handoffs first and confirm the target code is in their owned scope.
- If ownership is unclear or overlaps, stop that edit, post a blocker comment, and let the leader/integrator reassign scope.
- For git isolation, each agent must start on a dedicated branch via `scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"`.
- Agent completion must use `scripts/agent-branch-finish.sh` (merge into `dev`, push, delete agent branch).

1. Explicit ownership before edits

- Assign each agent clear file/module ownership.
- Do not edit files outside your assigned scope unless the leader reassigns ownership.

2. Preserve parallel safety

- Assume other agents are editing nearby code concurrently.
- Never revert unrelated changes authored by others.
- If another change conflicts with your approach, adapt and report the conflict in handoff.

3. Verify before completion

- Run required local checks for the area you changed.
- Do not mark work complete without command output evidence.

4. Required handoff format (every agent)

- Files changed
- Behavior touched
- Verification commands + results
- Risks / follow-ups

## OpenSpec Plan Workspace (recommended)

When work needs a durable planning phase, scaffold a plan workspace before implementation:

```bash
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
```

Expected shape:

```text
openspec/plan/<plan-slug>/
summary.md
checkpoints.md
planner/plan.md
planner/tasks.md
architect/tasks.md
critic/tasks.md
executor/tasks.md
writer/tasks.md
verifier/tasks.md
```
<!-- multiagent-safety:END -->
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Example output:
npm i -g musafety
musafety setup
musafety doctor
bash scripts/agent-branch-start.sh "task" "agent-name"
musafety sandbox "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)"
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
Expand Down Expand Up @@ -157,7 +157,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/agent-branch-start.sh "task" "agent-name"
musafety sandbox "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 @@ -176,6 +176,7 @@ Use this exact checklist to setup multi-agent safety in this repository for Code

```sh
musafety status [--target <path>] [--json]
musafety sandbox [task] [agent] [--target <path>] [--base <branch>] [--worktree-root <path>] [--allow-non-base] [--json]
musafety setup [--target <path>] [--dry-run] [--yes-global-install|--no-global-install] [--no-gitignore]
musafety doctor [--target <path>] [--dry-run] [--json] [--keep-stale-locks] [--no-gitignore]
musafety copy-prompt
Expand All @@ -192,6 +193,8 @@ bash scripts/agent-worktree-prune.sh --base dev # manual stale worktree cleanu
bash scripts/openspec/init-plan-workspace.sh <plan-slug> # optional OpenSpec plan scaffold
```

`musafety sandbox` keeps your visible checkout on the base branch (for example `main`) and creates an isolated agent worktree under `.omx/agent-worktrees/`, so sandbox terminals can use dedicated `agent/*` branches without flipping your primary Source Control branch.

No command defaults to `musafety status` (non-mutating health/status view).
`musafety status` reports CLI/runtime info, global OMX/OpenSpec service status, and repo safety service state.
When run in an interactive terminal, default `musafety` checks npm for a newer version first
Expand Down
201 changes: 199 additions & 2 deletions bin/multiagent-safety.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const COMMAND_TYPO_ALIASES = new Map([
]);
const SUGGESTIBLE_COMMANDS = [
'status',
'sandbox',
'setup',
'doctor',
'report',
Expand All @@ -99,6 +100,7 @@ const SUGGESTIBLE_COMMANDS = [
];
const CLI_COMMAND_DESCRIPTIONS = [
['status', 'Show musafety CLI + service health without modifying files'],
['sandbox', 'Create an isolated agent worktree sandbox while keeping visible repo branch unchanged'],
['setup', 'Install + repair guardrails in a git repo (supports --no-gitignore)'],
['doctor', 'Repair safety setup drift, then verify repo safety'],
['report', 'Generate security/safety reports (for example: OpenSSF scorecard)'],
Expand Down Expand Up @@ -132,7 +134,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/agent-branch-start.sh "task" "agent-name"
musafety sandbox "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,7 +152,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/agent-branch-start.sh "task" "agent-name"
musafety sandbox "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)"
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
Expand Down Expand Up @@ -462,6 +464,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
}

const wantedScripts = {
'agent:sandbox': `${TOOL_NAME} sandbox`,
'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 Expand Up @@ -1067,6 +1070,131 @@ function parseSyncArgs(rawArgs) {
return options;
}

function parseSandboxArgs(rawArgs) {
const options = {
target: process.cwd(),
task: 'task',
agent: 'agent',
base: '',
worktreeRoot: '.omx/agent-worktrees',
allowNonBase: false,
json: false,
};

const positional = [];

for (let index = 0; index < rawArgs.length; index += 1) {
const arg = rawArgs[index];
if (arg === '--target') {
const next = rawArgs[index + 1];
if (!next) {
throw new Error('--target requires a path value');
}
options.target = next;
index += 1;
continue;
}
if (arg === '--task') {
const next = rawArgs[index + 1];
if (!next) {
throw new Error('--task requires a value');
}
options.task = next;
index += 1;
continue;
}
if (arg === '--agent') {
const next = rawArgs[index + 1];
if (!next) {
throw new Error('--agent requires a value');
}
options.agent = next;
index += 1;
continue;
}
if (arg === '--base') {
const next = rawArgs[index + 1];
if (!next) {
throw new Error('--base requires a branch value');
}
options.base = next;
index += 1;
continue;
}
if (arg === '--worktree-root') {
const next = rawArgs[index + 1];
if (!next) {
throw new Error('--worktree-root requires a path value');
}
options.worktreeRoot = next;
index += 1;
continue;
}
if (arg === '--allow-non-base') {
options.allowNonBase = true;
continue;
}
if (arg === '--json') {
options.json = true;
continue;
}
if (arg.startsWith('-')) {
throw new Error(`Unknown option: ${arg}`);
}
positional.push(arg);
}

if (positional.length > 2) {
throw new Error(`Unexpected argument: ${positional[2]}`);
}
if (positional[0] && options.task === 'task') {
options.task = positional[0];
}
if (positional[1] && options.agent === 'agent') {
options.agent = positional[1];
}
if (!options.target) {
throw new Error('--target requires a path value');
}

return options;
}

function resolveSandboxBaseBranch(repoRoot, explicitBase) {
if (explicitBase) {
return explicitBase.trim();
}

const configured = readGitConfig(repoRoot, GIT_BASE_BRANCH_KEY);
if (configured) {
return configured;
}

const current = currentBranchName(repoRoot);
if (current && current !== 'HEAD' && !current.startsWith('agent/')) {
return current;
}

if (gitRefExists(repoRoot, 'refs/heads/main') || gitRefExists(repoRoot, 'refs/remotes/origin/main')) {
return 'main';
}

return DEFAULT_BASE_BRANCH;
}

function parseSandboxStartOutput(stdout) {
const out = String(stdout || '');
const branchMatch = out.match(/^\[agent-branch-start\] Created branch:\s*(.+)$/m);
const worktreeMatch = out.match(/^\[agent-branch-start\] Worktree:\s*(.+)$/m);
if (!branchMatch || !worktreeMatch) {
throw new Error(`Unable to parse agent sandbox output:\n${out.trim()}`);
}
return {
branch: branchMatch[1].trim(),
worktreePath: worktreeMatch[1].trim(),
};
}

function syncOperation(repoRoot, strategy, baseRef, ffOnly) {
if (strategy === 'rebase') {
if (ffOnly) {
Expand Down Expand Up @@ -2081,6 +2209,70 @@ function copyCommands() {
process.exitCode = 0;
}

function sandbox(rawArgs) {
const options = parseSandboxArgs(rawArgs);
const repoRoot = resolveRepoRoot(options.target);
const startScript = path.join(repoRoot, 'scripts', 'agent-branch-start.sh');
if (!fs.existsSync(startScript)) {
throw new Error(`Missing scripts/agent-branch-start.sh in target repo. Run '${TOOL_NAME} setup' first.`);
}

const baseBranch = resolveSandboxBaseBranch(repoRoot, options.base);
const visibleBranchBefore = currentBranchName(repoRoot);

if (!options.allowNonBase && visibleBranchBefore !== baseBranch) {
throw new Error(
`Sandbox expects visible repo branch '${baseBranch}' but current branch is '${visibleBranchBefore}'. ` +
`Switch first, or pass --allow-non-base to override.`,
);
}

const startArgs = [
'scripts/agent-branch-start.sh',
'--task', options.task,
'--agent', options.agent,
'--base', baseBranch,
'--worktree-root', options.worktreeRoot,
];

const started = run('bash', startArgs, { cwd: repoRoot });
if (started.status !== 0) {
throw new Error((started.stderr || started.stdout || 'Sandbox start failed').trim());
}

const parsed = parseSandboxStartOutput(started.stdout || '');
const visibleBranchAfter = currentBranchName(repoRoot);
if (visibleBranchAfter !== visibleBranchBefore) {
throw new Error(
`Sandbox changed visible repo branch from '${visibleBranchBefore}' to '${visibleBranchAfter}', which is not allowed.`,
);
}

const payload = {
repoRoot,
baseBranch,
visibleBranch: visibleBranchAfter,
branch: parsed.branch,
worktreePath: parsed.worktreePath,
};

if (options.json) {
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
} else {
console.log(`[${TOOL_NAME}] Sandbox ready.`);
console.log(`[${TOOL_NAME}] Visible repo branch: ${visibleBranchAfter}`);
console.log(`[${TOOL_NAME}] Base branch: ${baseBranch}`);
console.log(`[${TOOL_NAME}] Agent branch: ${parsed.branch}`);
console.log(`[${TOOL_NAME}] Sandbox worktree: ${parsed.worktreePath}`);
console.log(`[${TOOL_NAME}] Open a sandbox terminal:`);
console.log(` cd "${parsed.worktreePath}"`);
console.log(` # commit + push from sandbox, then finish:`);
console.log(` bash scripts/agent-branch-finish.sh --branch "${parsed.branch}"`);
}

process.exitCode = 0;
}

function sync(rawArgs) {
const options = parseSyncArgs(rawArgs);
const repoRoot = resolveRepoRoot(options.target);
Expand Down Expand Up @@ -2394,6 +2586,11 @@ function main() {
return;
}

if (command === 'sandbox') {
sandbox(rest);
return;
}

if (command === 'setup') {
setup(rest);
return;
Expand Down
Loading
Loading