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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This AGENTS.md is the top-level operating contract for this repository.
- Treat `main` and any currently checked-out base branch as read-only workspaces.
- Every new session must start by creating an isolated agent branch/worktree via `scripts/agent-branch-start.sh` before making edits.
- If edits are found on `main`/base by mistake, immediately move them to a dedicated agent branch/worktree before continuing.
- In-place agent branching is disallowed; keep the visible local/base checkout unchanged and do all edits in dedicated agent worktrees.
- Prefer deletion over addition.
- Reuse existing patterns before introducing new abstractions.
- No new dependencies without explicit request.
Expand Down Expand Up @@ -91,6 +92,7 @@ OMX runtime state typically lives under `.omx/`:
- 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>"`.
- In-place branch mode is disallowed: never switch the active local/base checkout to an agent branch.
- 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.
- Treat the base branch (`main` or the user's current local base branch) as read-only while the agent branch is active.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ gx status
# setup and repair
gx setup
gx doctor
# setup + repair another repo without switching your current repo checkout
gx setup --target /path/to/repo
gx doctor --target /path/to/repo

# protected branch management
gx protect list
Expand Down Expand Up @@ -147,6 +150,7 @@ Note: the monitor dispatches Codex through explicit `--task/--agent/--base` flag
- Optional repo override for manual VS Code protected-branch writes: `git config multiagent.allowVscodeProtectedBranchWrites true`.
- Codex/agent sessions stay blocked on protected branches and must use `agent/*` branch + PR workflow.
- On protected `main`, `gx doctor` auto-runs in a sandbox agent branch/worktree.
- In-place agent branching is disabled; `scripts/agent-branch-start.sh` always creates a separate worktree to keep your visible local/base branch unchanged.
- `scripts/agent-branch-start.sh` hydrates `scripts/codex-agent.sh` into new sandbox worktrees when missing, so auto-finish launcher flow stays available.

## Configure protected branches
Expand Down
45 changes: 6 additions & 39 deletions scripts/agent-branch-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ TASK_NAME="task"
AGENT_NAME="agent"
BASE_BRANCH=""
BASE_BRANCH_EXPLICIT=0
WORKTREE_MODE=1
ALLOW_IN_PLACE=0
WORKTREE_ROOT_REL=".omx/agent-worktrees"
POSITIONAL_ARGS=()

Expand All @@ -25,13 +23,10 @@ while [[ $# -gt 0 ]]; do
BASE_BRANCH_EXPLICIT=1
shift 2
;;
--in-place)
WORKTREE_MODE=0
shift
;;
--allow-in-place)
ALLOW_IN_PLACE=1
shift
--in-place|--allow-in-place)
echo "[agent-branch-start] In-place branch mode is disabled." >&2
echo "[agent-branch-start] This command always creates an isolated worktree to keep your active checkout unchanged." >&2
exit 1
;;
--worktree-root)
WORKTREE_ROOT_REL="${2:-.omx/agent-worktrees}"
Expand All @@ -47,7 +42,7 @@ while [[ $# -gt 0 ]]; do
;;
-*)
echo "[agent-branch-start] Unknown option: $1" >&2
echo "Usage: $0 [task] [agent] [base] [--in-place --allow-in-place] [--worktree-root <path>]" >&2
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
exit 1
;;
*)
Expand All @@ -59,7 +54,7 @@ done

if [[ "${#POSITIONAL_ARGS[@]}" -gt 3 ]]; then
echo "[agent-branch-start] Too many positional arguments." >&2
echo "Usage: $0 [task] [agent] [base] [--in-place --allow-in-place] [--worktree-root <path>]" >&2
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
exit 1
fi

Expand Down Expand Up @@ -237,34 +232,6 @@ while git show-ref --verify --quiet "refs/heads/${branch_name}"; do
branch_suffix=$((branch_suffix + 1))
done

if [[ "$WORKTREE_MODE" -eq 0 ]]; then
if [[ "$ALLOW_IN_PLACE" -ne 1 ]]; then
echo "[agent-branch-start] --in-place is blocked by default to prevent accidental edits on protected branches." >&2
echo "[agent-branch-start] If you really need it, pass both: --in-place --allow-in-place" >&2
exit 1
fi

if ! git diff --quiet || ! git diff --cached --quiet; then
echo "[agent-branch-start] Working tree is not clean. Commit/stash changes before starting an in-place branch." >&2
exit 1
fi

current_branch="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$current_branch" != "$BASE_BRANCH" ]]; then
git checkout "$BASE_BRANCH"
fi

if git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
git pull --ff-only origin "$BASE_BRANCH"
fi

git checkout -b "$branch_name"
git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
echo "[agent-branch-start] Created in-place branch: ${branch_name}"
echo "$branch_name"
exit 0
fi

worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
mkdir -p "$worktree_root"
worktree_path="${worktree_root}/${branch_name//\//__}"
Expand Down
1 change: 1 addition & 0 deletions templates/AGENTS.multiagent-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- 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>"`.
- In-place branch mode is disallowed: never switch the active local/base checkout to an agent branch.
- Treat the base branch (`main` or the user's current local base branch) as read-only while the agent branch is active.
- Agent completion defaults to `scripts/codex-agent.sh`, which auto-finishes the branch (auto-commit changed files, push/create PR, attempt merge, and pull the local base branch after merge).
- Auto-finish now waits for required checks/merge and then cleans merged sandbox branch/worktree by default.
Expand Down
45 changes: 6 additions & 39 deletions templates/scripts/agent-branch-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ TASK_NAME="task"
AGENT_NAME="agent"
BASE_BRANCH=""
BASE_BRANCH_EXPLICIT=0
WORKTREE_MODE=1
ALLOW_IN_PLACE=0
WORKTREE_ROOT_REL=".omx/agent-worktrees"
POSITIONAL_ARGS=()

Expand All @@ -25,13 +23,10 @@ while [[ $# -gt 0 ]]; do
BASE_BRANCH_EXPLICIT=1
shift 2
;;
--in-place)
WORKTREE_MODE=0
shift
;;
--allow-in-place)
ALLOW_IN_PLACE=1
shift
--in-place|--allow-in-place)
echo "[agent-branch-start] In-place branch mode is disabled." >&2
echo "[agent-branch-start] This command always creates an isolated worktree to keep your active checkout unchanged." >&2
exit 1
;;
--worktree-root)
WORKTREE_ROOT_REL="${2:-.omx/agent-worktrees}"
Expand All @@ -47,7 +42,7 @@ while [[ $# -gt 0 ]]; do
;;
-*)
echo "[agent-branch-start] Unknown option: $1" >&2
echo "Usage: $0 [task] [agent] [base] [--in-place --allow-in-place] [--worktree-root <path>]" >&2
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
exit 1
;;
*)
Expand All @@ -59,7 +54,7 @@ done

if [[ "${#POSITIONAL_ARGS[@]}" -gt 3 ]]; then
echo "[agent-branch-start] Too many positional arguments." >&2
echo "Usage: $0 [task] [agent] [base] [--in-place --allow-in-place] [--worktree-root <path>]" >&2
echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
exit 1
fi

Expand Down Expand Up @@ -237,34 +232,6 @@ while git show-ref --verify --quiet "refs/heads/${branch_name}"; do
branch_suffix=$((branch_suffix + 1))
done

if [[ "$WORKTREE_MODE" -eq 0 ]]; then
if [[ "$ALLOW_IN_PLACE" -ne 1 ]]; then
echo "[agent-branch-start] --in-place is blocked by default to prevent accidental edits on protected branches." >&2
echo "[agent-branch-start] If you really need it, pass both: --in-place --allow-in-place" >&2
exit 1
fi

if ! git diff --quiet || ! git diff --cached --quiet; then
echo "[agent-branch-start] Working tree is not clean. Commit/stash changes before starting an in-place branch." >&2
exit 1
fi

current_branch="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$current_branch" != "$BASE_BRANCH" ]]; then
git checkout "$BASE_BRANCH"
fi

if git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
git pull --ff-only origin "$BASE_BRANCH"
fi

git checkout -b "$branch_name"
git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
echo "[agent-branch-start] Created in-place branch: ${branch_name}"
echo "$branch_name"
exit 0
fi

worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
mkdir -p "$worktree_root"
worktree_path="${worktree_root}/${branch_name//\//__}"
Expand Down
147 changes: 144 additions & 3 deletions templates/scripts/codex-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,106 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
fi
repo_root="$(git rev-parse --show-toplevel)"

sanitize_slug() {
local raw="$1"
local fallback="${2:-task}"
local slug
slug="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
if [[ -z "$slug" ]]; then
slug="$fallback"
fi
printf '%s' "$slug"
}

resolve_start_base_branch() {
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
printf '%s' "$BASE_BRANCH"
return 0
fi

local configured_base
configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
if [[ -n "$configured_base" ]]; then
printf '%s' "$configured_base"
return 0
fi

local current_branch
current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
printf '%s' "$current_branch"
return 0
fi

printf 'dev'
}

resolve_start_ref() {
local base_branch="$1"
git -C "$repo_root" fetch origin "$base_branch" --quiet >/dev/null 2>&1 || true
if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
printf 'origin/%s' "$base_branch"
return 0
fi
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${base_branch}"; then
printf '%s' "$base_branch"
return 0
fi
return 1
}

restore_repo_branch_if_changed() {
local expected_branch="$1"
if [[ -z "$expected_branch" || "$expected_branch" == "HEAD" ]]; then
return 0
fi
local current_branch
current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if [[ -z "$current_branch" || "$current_branch" == "$expected_branch" ]]; then
return 0
fi
git -C "$repo_root" checkout "$expected_branch" >/dev/null 2>&1
}

start_sandbox_fallback() {
local base_branch start_ref timestamp task_slug agent_slug branch_name_base branch_name suffix
local worktree_root worktree_path

base_branch="$(resolve_start_base_branch)"
if ! start_ref="$(resolve_start_ref "$base_branch")"; then
echo "[codex-agent] Unable to resolve base ref for fallback sandbox start: ${base_branch}" >&2
return 1
fi

timestamp="$(date +%Y%m%d-%H%M%S)"
task_slug="$(sanitize_slug "$TASK_NAME" "task")"
agent_slug="$(sanitize_slug "$AGENT_NAME" "agent")"
branch_name_base="agent/${agent_slug}/${timestamp}-${task_slug}"
branch_name="$branch_name_base"
suffix=2
while git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch_name}"; do
branch_name="${branch_name_base}-${suffix}"
suffix=$((suffix + 1))
done

worktree_root="${repo_root}/.omx/agent-worktrees"
mkdir -p "$worktree_root"
worktree_path="${worktree_root}/${branch_name//\//__}"
if [[ -e "$worktree_path" ]]; then
echo "[codex-agent] Fallback worktree path already exists: $worktree_path" >&2
return 1
fi

git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref" >/dev/null
git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$base_branch" >/dev/null 2>&1 || true
if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
git -C "$worktree_path" branch --set-upstream-to="origin/${base_branch}" "$branch_name" >/dev/null 2>&1 || true
fi

printf '[agent-branch-start] Created branch: %s\n' "$branch_name"
printf '[agent-branch-start] Worktree: %s\n' "$worktree_path"
}

if [[ ! -x "${repo_root}/scripts/agent-branch-start.sh" ]]; then
echo "[codex-agent] Missing scripts/agent-branch-start.sh. Run: gx setup" >&2
exit 1
Expand All @@ -135,12 +235,53 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
start_args+=("$BASE_BRANCH")
fi

start_output="$(bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}")"
printf '%s\n' "$start_output"
initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
start_output=""
start_status=0
set +e
start_output="$(bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
start_status=$?
set -e

worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
current_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
resolved_repo_root="$(cd "$repo_root" && pwd -P)"
resolved_worktree_path=""
if [[ -n "$worktree_path" && -d "$worktree_path" ]]; then
resolved_worktree_path="$(cd "$worktree_path" && pwd -P)"
fi

fallback_reason=""
if [[ "$start_status" -ne 0 ]]; then
fallback_reason="starter exited with status ${start_status}"
elif [[ -z "$worktree_path" ]]; then
fallback_reason="starter did not report worktree path"
elif [[ -n "$resolved_worktree_path" && "$resolved_worktree_path" == "$resolved_repo_root" ]]; then
fallback_reason="starter pointed to active checkout path"
elif [[ -n "$initial_repo_branch" && -n "$current_repo_branch" && "$current_repo_branch" != "$initial_repo_branch" ]]; then
fallback_reason="starter switched active checkout branch"
fi

if [[ -n "$fallback_reason" ]]; then
if ! restore_repo_branch_if_changed "$initial_repo_branch"; then
echo "[codex-agent] agent-branch-start changed the active checkout branch and restore failed." >&2
echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
exit 1
fi
if [[ -n "$start_output" ]]; then
printf '%s\n' "$start_output" >&2
fi
echo "[codex-agent] Unsafe starter output (${fallback_reason}); creating sandbox worktree directly." >&2
start_output="$(start_sandbox_fallback)"
printf '%s\n' "$start_output"
worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
else
printf '%s\n' "$start_output"
fi

if [[ -z "$worktree_path" ]]; then
echo "[codex-agent] Could not determine sandbox worktree path from agent-branch-start output." >&2
echo "[codex-agent] Could not determine sandbox worktree path from sandbox startup output." >&2
echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
exit 1
fi

Expand Down
Loading
Loading