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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ OMX runtime state typically lives under `.omx/`:
- For git isolation, each agent must start on a dedicated branch via `scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"`.
- Do not implement changes directly on `main` or other base branches; all edits must happen on dedicated agent branches/worktrees.
- If the current local branch already contains accidental edits, move them to an agent branch/worktree first, then continue implementation.
- Agent completion must use `scripts/agent-branch-finish.sh` (merge into `dev`, push, delete agent branch).
- Agent completion defaults to `scripts/codex-agent.sh`, which auto-finishes the branch (auto-commit changed files, push/create PR, attempt merge, clean branch/worktree, and pull the local base branch after merge).
- If codex-agent auto-finish cannot complete, run `scripts/agent-branch-finish.sh --branch "<agent-branch>" --via-pr` and keep the branch open until checks/review pass.
- If merge/rebase conflicts block auto-finish, run a conflict-resolution review pass in that sandbox branch, then rerun `agent-branch-finish.sh --via-pr` until merged.

1. Explicit ownership before edits

Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ Use this exact checklist to setup multi-agent safety in this repository for Code
- For every new user message/task, repeat the same cycle:
start isolated agent branch/worktree -> claim file locks -> implement/verify ->
finish via PR/merge cleanup with scripts/agent-branch-finish.sh.
- `scripts/codex-agent.sh` now auto-runs this finish flow after Codex exits:
auto-commit changed files -> push/create PR -> merge attempt -> branch/worktree cleanup ->
pull local base branch.

5) Optional: create OpenSpec planning workspace:
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
Expand Down Expand Up @@ -285,7 +288,9 @@ and asks `[y/N]` whether to update immediately (default is `N`).
- Interactive prompt is strict (`[y/n]`) and waits for explicit answer.
- Non-interactive setup: skips global installs by default; use `--yes-global-install` to force.
- In already-initialized repos, `setup` / `install` / `fix` / `doctor` block writes on protected `main` by default; start an agent branch first. Use `--allow-protected-base-write` only for emergency in-place maintenance.
- `scripts/codex-agent.sh` now auto-runs worktree prune after a Codex session; clean sandbox branches are removed automatically, dirty ones are kept.
- `scripts/codex-agent.sh` now auto-runs finish automation after a Codex session when `origin` exists:
auto-commit changed files, run PR/merge cleanup, and prune merged worktrees.
If conflicts remain, it keeps the sandbox and prompts for a conflict-resolution review pass.

## Advanced commands

Expand Down
4 changes: 3 additions & 1 deletion templates/AGENTS.multiagent-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
- 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>"`.
- Treat the base branch (`main` or the user's current local base branch) as read-only while the agent branch is active.
- Agent completion must use `scripts/agent-branch-finish.sh` (direct merge to base when allowed; auto PR fallback for protected bases, then cleanup after merge).
- Agent completion defaults to `scripts/codex-agent.sh`, which now auto-finishes the branch (auto-commit changed files, push/create PR, attempt merge, clean branch/worktree, and pull the local base branch after merge).
- If codex-agent auto-finish cannot complete, immediately run `scripts/agent-branch-finish.sh --branch "<agent-branch>" --via-pr` and keep the branch open until checks/review pass.
- If merge/rebase conflicts block auto-finish, run a conflict-resolution review pass in that sandbox branch, then rerun `agent-branch-finish.sh --via-pr` until merged.
- Per-message loop is mandatory: for every new user message/task, start a fresh agent branch/worktree, claim ownership locks, implement and verify, finish via PR/merge cleanup, then repeat for the next message/task.

1. Explicit ownership before edits
Expand Down
207 changes: 204 additions & 3 deletions templates/scripts/codex-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ AGENT_NAME="${MUSAFETY_AGENT_NAME:-agent}"
BASE_BRANCH="${MUSAFETY_BASE_BRANCH:-}"
BASE_BRANCH_EXPLICIT=0
CODEX_BIN="${MUSAFETY_CODEX_BIN:-codex}"
AUTO_FINISH_RAW="${MUSAFETY_CODEX_AUTO_FINISH:-true}"
AUTO_REVIEW_ON_CONFLICT_RAW="${MUSAFETY_CODEX_AUTO_REVIEW_ON_CONFLICT:-true}"

normalize_bool() {
local raw="${1:-}"
local fallback="${2:-0}"
local lowered
lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
case "$lowered" in
1|true|yes|on) printf '1' ;;
0|false|no|off) printf '0' ;;
'') printf '%s' "$fallback" ;;
*) printf '%s' "$fallback" ;;
esac
}

AUTO_FINISH="$(normalize_bool "$AUTO_FINISH_RAW" "1")"
AUTO_REVIEW_ON_CONFLICT="$(normalize_bool "$AUTO_REVIEW_ON_CONFLICT_RAW" "1")"

if [[ -n "$BASE_BRANCH" ]]; then
BASE_BRANCH_EXPLICIT=1
Expand All @@ -30,6 +48,22 @@ while [[ $# -gt 0 ]]; do
CODEX_BIN="${2:-$CODEX_BIN}"
shift 2
;;
--auto-finish)
AUTO_FINISH=1
shift
;;
--no-auto-finish)
AUTO_FINISH=0
shift
;;
--auto-review-on-conflict)
AUTO_REVIEW_ON_CONFLICT=1
shift
;;
--no-auto-review-on-conflict)
AUTO_REVIEW_ON_CONFLICT=0
shift
;;
--)
shift
break
Expand Down Expand Up @@ -95,6 +129,145 @@ if [[ ! -d "$worktree_path" ]]; then
exit 1
fi

has_origin_remote() {
git -C "$repo_root" remote get-url origin >/dev/null 2>&1
}

worktree_has_changes() {
local wt="$1"
if ! git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
return 0
fi
if ! git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
return 0
fi
if [[ -n "$(git -C "$wt" ls-files --others --exclude-standard)" ]]; then
return 0
fi
return 1
}

claim_changed_files() {
local wt="$1"
local branch="$2"
local lock_script="${repo_root}/scripts/agent-file-locks.py"

if [[ ! -x "$lock_script" ]]; then
return 0
fi

local changed_raw deleted_raw
changed_raw="$({
git -C "$wt" diff --name-only -- . ":(exclude).omx/state/agent-file-locks.json";
git -C "$wt" diff --cached --name-only -- . ":(exclude).omx/state/agent-file-locks.json";
git -C "$wt" ls-files --others --exclude-standard;
} | sed '/^$/d' | sort -u)"

if [[ -n "$changed_raw" ]]; then
mapfile -t changed_files < <(printf '%s\n' "$changed_raw")
python3 "$lock_script" claim --branch "$branch" "${changed_files[@]}" >/dev/null 2>&1 || true
fi

deleted_raw="$({
git -C "$wt" diff --name-only --diff-filter=D -- . ":(exclude).omx/state/agent-file-locks.json";
git -C "$wt" diff --cached --name-only --diff-filter=D -- . ":(exclude).omx/state/agent-file-locks.json";
} | sed '/^$/d' | sort -u)"

if [[ -n "$deleted_raw" ]]; then
mapfile -t deleted_files < <(printf '%s\n' "$deleted_raw")
python3 "$lock_script" allow-delete --branch "$branch" "${deleted_files[@]}" >/dev/null 2>&1 || true
fi
}

auto_commit_worktree_changes() {
local wt="$1"
local branch="$2"

if ! worktree_has_changes "$wt"; then
return 0
fi

claim_changed_files "$wt" "$branch"
git -C "$wt" add -A

if git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"; then
return 0
fi

local default_message="Auto-finish: ${TASK_NAME}"
local commit_message="${MUSAFETY_CODEX_AUTO_COMMIT_MESSAGE:-$default_message}"

if ! git -C "$wt" commit -m "$commit_message" >/dev/null 2>&1; then
echo "[codex-agent] Auto-commit failed in sandbox. Keeping branch for manual review: $branch" >&2
return 1
fi

echo "[codex-agent] Auto-committed sandbox changes on '${branch}'."
return 0
}

looks_like_conflict_failure() {
local output="$1"
if grep -qiE 'preflight conflict detected|merge conflict detected|auto-sync failed while rebasing|rebase --continue|rebase --abort' <<< "$output"; then
return 0
fi
return 1
}

run_finish_flow() {
local wt="$1"
local branch="$2"
local finish_output=""
local -a finish_args

finish_args=(--branch "$branch")
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
finish_args+=(--base "$BASE_BRANCH")
fi

if has_origin_remote; then
if command -v gh >/dev/null 2>&1 || command -v "${MUSAFETY_GH_BIN:-gh}" >/dev/null 2>&1; then
finish_args+=(--via-pr)
fi
else
echo "[codex-agent] No origin remote detected; skipping auto-finish merge/PR pipeline." >&2
return 2
fi

if finish_output="$(bash "${repo_root}/scripts/agent-branch-finish.sh" "${finish_args[@]}" 2>&1)"; then
printf '%s\n' "$finish_output"
return 0
fi

printf '%s\n' "$finish_output" >&2

if [[ "$AUTO_REVIEW_ON_CONFLICT" -eq 1 ]] && looks_like_conflict_failure "$finish_output"; then
echo "[codex-agent] Auto-finish hit conflicts. Launching Codex conflict-review pass in sandbox..." >&2
local review_prompt
review_prompt="Resolve git conflicts for branch ${branch} against ${BASE_BRANCH:-base branch}, then commit the resolution in this sandbox worktree and exit."

(
cd "$wt"
set +e
"$CODEX_BIN" "$review_prompt"
review_exit="$?"
set -e
if [[ "$review_exit" -ne 0 ]]; then
echo "[codex-agent] Conflict-review Codex pass exited with status ${review_exit}." >&2
fi
)

if finish_output="$(bash "${repo_root}/scripts/agent-branch-finish.sh" "${finish_args[@]}" 2>&1)"; then
printf '%s\n' "$finish_output"
return 0
fi

printf '%s\n' "$finish_output" >&2
fi

return 1
}

echo "[codex-agent] Launching ${CODEX_BIN} in sandbox: $worktree_path"
cd "$worktree_path"
set +e
Expand All @@ -103,6 +276,34 @@ codex_exit="$?"
set -e

cd "$repo_root"
final_exit="$codex_exit"
auto_finish_completed=0

worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"

if [[ "$AUTO_FINISH" -eq 1 && -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> merge -> cleanup."
if auto_commit_worktree_changes "$worktree_path" "$worktree_branch"; then
if run_finish_flow "$worktree_path" "$worktree_branch"; then
auto_finish_completed=1
echo "[codex-agent] Auto-finish completed for '${worktree_branch}'."
else
finish_status="$?"
if [[ "$finish_status" -eq 2 ]]; then
echo "[codex-agent] Auto-finish skipped for '${worktree_branch}' (no mergeable remote context)." >&2
else
echo "[codex-agent] Auto-finish did not complete; keeping sandbox for manual review: $worktree_path" >&2
if [[ "$final_exit" -eq 0 ]]; then
final_exit=1
fi
fi
fi
else
if [[ "$final_exit" -eq 0 ]]; then
final_exit=1
fi
fi
fi

if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
echo "[codex-agent] Session ended (exit=${codex_exit}). Running worktree cleanup..."
Expand All @@ -120,9 +321,9 @@ if [[ ! -d "$worktree_path" ]]; then
else
worktree_branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
echo "[codex-agent] Sandbox worktree kept: $worktree_path"
if [[ -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
echo "[codex-agent] If finished, merge + clean with: bash scripts/agent-branch-finish.sh --branch \"${worktree_branch}\""
if [[ "$auto_finish_completed" -eq 0 && -n "$worktree_branch" && "$worktree_branch" != "HEAD" ]]; then
echo "[codex-agent] If finished, merge + clean with: bash scripts/agent-branch-finish.sh --branch \"${worktree_branch}\" --via-pr"
fi
fi

exit "$codex_exit"
exit "$final_exit"
82 changes: 81 additions & 1 deletion test/install.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -827,14 +827,94 @@ test('codex-agent keeps dirty sandbox worktrees after session exit', () => {
},
);
assert.equal(launch.status, 0, launch.stderr || launch.stdout);
assert.match(launch.stdout, /\[agent-worktree-prune\] Skipping dirty worktree/);
assert.match(
launch.stdout,
/\[agent-worktree-prune\] Skipping dirty worktree|\[codex-agent\] Auto-committed sandbox changes on/,
);
assert.match(launch.stdout, /\[codex-agent\] Sandbox worktree kept:/);

const launchedCwd = fs.readFileSync(cwdMarker, 'utf8').trim();
assert.equal(fs.existsSync(launchedCwd), true, 'dirty sandbox should be preserved');
assert.equal(fs.existsSync(path.join(launchedCwd, 'codex-dirty.txt')), true);
});

test('codex-agent auto-finishes dirty sandbox branches via PR flow when origin is configured', () => {
const repoDir = initRepo();
seedCommit(repoDir);
attachOriginRemote(repoDir);

let result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir);
assert.equal(result.status, 0, result.stderr || result.stdout);
result = runCmd('git', ['add', '.'], repoDir);
assert.equal(result.status, 0, result.stderr);
result = runCmd('git', ['commit', '-m', 'apply gx setup'], repoDir, {
ALLOW_COMMIT_ON_PROTECTED_BRANCH: '1',
});
assert.equal(result.status, 0, result.stderr || result.stdout);
result = runCmd('git', ['push', 'origin', 'dev'], repoDir);
assert.equal(result.status, 0, result.stderr || result.stdout);

const fakeCodexBin = fs.mkdtempSync(path.join(os.tmpdir(), 'musafety-fake-codex-autofinish-'));
const fakeCodexPath = path.join(fakeCodexBin, 'codex');
fs.writeFileSync(
fakeCodexPath,
`#!/usr/bin/env bash\n` +
`pwd > "${'${MUSAFETY_TEST_CODEX_CWD}'}"\n` +
`echo "$@" > "${'${MUSAFETY_TEST_CODEX_ARGS}'}"\n` +
`echo "auto-finish-change" > codex-autofinish.txt\n`,
'utf8',
);
fs.chmodSync(fakeCodexPath, 0o755);

const { fakePath: fakeGhPath } = createFakeGhScript(`
if [[ "$1" == "pr" && "$2" == "create" ]]; then
exit 0
fi
if [[ "$1" == "pr" && "$2" == "view" ]]; then
if [[ " $* " == *" --json url "* ]]; then
echo "https://example.test/pr/auto-finish"
exit 0
fi
echo "unexpected gh pr view args: $*" >&2
exit 1
fi
if [[ "$1" == "pr" && "$2" == "merge" ]]; then
exit 0
fi
echo "unexpected gh args: $*" >&2
exit 1
`);

const cwdMarker = path.join(repoDir, '.codex-agent-cwd-autofinish');
const argsMarker = path.join(repoDir, '.codex-agent-args-autofinish');
const launch = runCmd(
'bash',
['scripts/codex-agent.sh', 'autofinish-task', 'planner', 'dev', '--model', 'gpt-5.4-mini'],
repoDir,
{
PATH: `${fakeCodexBin}:${process.env.PATH}`,
MUSAFETY_TEST_CODEX_CWD: cwdMarker,
MUSAFETY_TEST_CODEX_ARGS: argsMarker,
MUSAFETY_GH_BIN: fakeGhPath,
},
);
assert.equal(launch.status, 0, launch.stderr || launch.stdout);
assert.match(launch.stdout, /\[codex-agent\] Auto-finish enabled: commit -> push\/PR -> merge -> cleanup\./);
assert.match(launch.stdout, /\[codex-agent\] Auto-finish completed for/);
assert.match(launch.stdout, /\[codex-agent\] Auto-cleaned sandbox worktree:/);

const launchedCwd = fs.readFileSync(cwdMarker, 'utf8').trim();
assert.equal(fs.existsSync(launchedCwd), false, 'auto-finished sandbox should be removed');
const launchedBranch = extractCreatedBranch(launch.stdout);
result = runCmd('git', ['show-ref', '--verify', '--quiet', `refs/heads/${launchedBranch}`], repoDir);
assert.notEqual(result.status, 0, 'auto-finished branch should be removed locally');
result = runCmd('git', ['ls-remote', '--heads', 'origin', launchedBranch], repoDir);
assert.equal(result.stdout.trim(), '', 'auto-finished branch should be removed on origin');

const launchedArgs = fs.readFileSync(argsMarker, 'utf8').trim();
assert.match(launchedArgs, /--model gpt-5\.4-mini/);
});

test('sync command rebases current agent branch onto latest origin/dev', () => {
const repoDir = initRepo();
seedCommit(repoDir);
Expand Down
Loading