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
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,15 @@ agent_bot_<timestamp>-<snapshot>-<task>
That gives you one stable main repo view plus parallel agent worktrees in the
same VS Code window, so branch ownership and progress stay visible at once.

## Companion tool: `codex-auth` account switcher
## GuardeX dependency: `codex-auth` account switcher

If you run multiple Codex identities, this workflow pairs well with
[`codex-auth`](https://github.com/recodeecom/codex-account-switcher-cli/tree/main),
[`codex-auth`](https://github.com/recodeecom/codex-account-switcher-cli),
a CLI that snapshots `~/.codex/auth.json` per account and lets you switch fast
without repeated login/logout loops.

For multi-identity workflows, treat `codex-auth` as a GuardeX dependency.

> [!WARNING]
> Not affiliated with OpenAI or Codex. Not an official tool.

Expand Down Expand Up @@ -270,10 +272,13 @@ gx protect reset [--target <path>]
gx sync --check [--target <path>] [--base <branch>] [--json]
gx sync [--target <path>] [--base <branch>] [--strategy rebase|merge] [--ff-only]
gx report scorecard [--target <path>] [--repo github.com/<owner>/<repo>] [--scorecard-json <file>] [--output-dir <path>] [--date YYYY-MM-DD]
bash scripts/agent-worktree-prune.sh --base dev # manual stale worktree cleanup
bash scripts/agent-worktree-prune.sh # manual stale worktree cleanup (auto-detects base)
bash scripts/openspec/init-plan-workspace.sh <plan-slug> # optional OpenSpec plan scaffold
```

`scripts/codex-agent.sh` auto-runs worktree cleanup after each Codex session exit.
If a branch still appears in VS Code, it is usually still dirty/unmerged (kept intentionally).

No command defaults to `gx status` (non-mutating health/status view).
`gx status` reports CLI/runtime info, global OMX/OpenSpec/codex-auth service status, and repo safety service state.
`gx init` is an alias of `gx setup`.
Expand Down
2 changes: 1 addition & 1 deletion bin/multiagent-safety.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
'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',
'agent:cleanup': 'bash ./scripts/agent-worktree-prune.sh',
'agent:hooks:install': 'bash ./scripts/install-agent-git-hooks.sh',
'agent:locks:claim': 'python3 ./scripts/agent-file-locks.py claim',
'agent:locks:allow-delete': 'python3 ./scripts/agent-file-locks.py allow-delete',
Expand Down
76 changes: 72 additions & 4 deletions scripts/agent-worktree-prune.sh
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail

BASE_BRANCH="dev"
BASE_BRANCH="${MUSAFETY_BASE_BRANCH:-}"
BASE_BRANCH_EXPLICIT=0
DRY_RUN=0
FORCE_DIRTY=0

if [[ -n "$BASE_BRANCH" ]]; then
BASE_BRANCH_EXPLICIT=1
fi

while [[ $# -gt 0 ]]; do
case "$1" in
--base)
BASE_BRANCH="${2:-dev}"
BASE_BRANCH="${2:-}"
BASE_BRANCH_EXPLICIT=1
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--force-dirty)
FORCE_DIRTY=1
shift
;;
*)
echo "[agent-worktree-prune] Unknown argument: $1" >&2
echo "Usage: $0 [--base <branch>] [--dry-run]" >&2
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty]" >&2
exit 1
;;
esac
Expand All @@ -31,6 +42,46 @@ repo_root="$(git rev-parse --show-toplevel)"
current_pwd="$(pwd -P)"
worktree_root="${repo_root}/.omx/agent-worktrees"

resolve_base_branch() {
local configured=""
local current=""

configured="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
if [[ -n "$configured" ]] && git -C "$repo_root" show-ref --verify --quiet "refs/heads/${configured}"; then
printf '%s' "$configured"
return 0
fi

current="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if [[ -n "$current" && "$current" != "HEAD" ]] && git -C "$repo_root" show-ref --verify --quiet "refs/heads/${current}"; then
printf '%s' "$current"
return 0
fi

for fallback in main dev; do
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${fallback}"; then
printf '%s' "$fallback"
return 0
fi
done

printf '%s' ""
}

if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
echo "[agent-worktree-prune] --base requires a non-empty branch name." >&2
exit 1
fi

if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
BASE_BRANCH="$(resolve_base_branch)"
fi

if [[ -z "$BASE_BRANCH" ]]; then
echo "[agent-worktree-prune] Unable to infer base branch. Pass --base <branch>." >&2
exit 1
fi

if ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${BASE_BRANCH}"; then
echo "[agent-worktree-prune] Base branch not found: ${BASE_BRANCH}" >&2
exit 1
Expand All @@ -49,9 +100,17 @@ branch_has_worktree() {
git -C "$repo_root" worktree list --porcelain | grep -q "^branch refs/heads/${branch}$"
}

is_clean_worktree() {
local wt="$1"
git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
&& git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
&& [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
}

removed_worktrees=0
removed_branches=0
skipped_active=0
skipped_dirty=0

process_entry() {
local wt="$1"
Expand Down Expand Up @@ -89,6 +148,12 @@ process_entry() {
return
fi

if [[ "$FORCE_DIRTY" -ne 1 ]] && ! is_clean_worktree "$wt"; then
skipped_dirty=$((skipped_dirty + 1))
echo "[agent-worktree-prune] Skipping dirty worktree (${remove_reason}): ${wt}"
return
fi

echo "[agent-worktree-prune] Removing worktree (${remove_reason}): ${wt}"
run_cmd git -C "$repo_root" worktree remove "$wt" --force
removed_worktrees=$((removed_worktrees + 1))
Expand Down Expand Up @@ -149,7 +214,10 @@ done < <(git -C "$repo_root" for-each-ref --format='%(refname:short)' refs/heads

run_cmd git -C "$repo_root" worktree prune

echo "[agent-worktree-prune] Summary: removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}"
echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}"
if [[ "$skipped_active" -gt 0 ]]; then
echo "[agent-worktree-prune] Tip: leave active agent worktree directories, then run this command again for full cleanup." >&2
fi
if [[ "$skipped_dirty" -gt 0 ]]; then
echo "[agent-worktree-prune] Tip: dirty worktrees were preserved. Clean/finish them first, or pass --force-dirty to remove anyway." >&2
fi
76 changes: 72 additions & 4 deletions templates/scripts/agent-worktree-prune.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail

BASE_BRANCH="dev"
BASE_BRANCH="${MUSAFETY_BASE_BRANCH:-}"
BASE_BRANCH_EXPLICIT=0
DRY_RUN=0
FORCE_DIRTY=0

if [[ -n "$BASE_BRANCH" ]]; then
BASE_BRANCH_EXPLICIT=1
fi

while [[ $# -gt 0 ]]; do
case "$1" in
--base)
BASE_BRANCH="${2:-dev}"
BASE_BRANCH="${2:-}"
BASE_BRANCH_EXPLICIT=1
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--force-dirty)
FORCE_DIRTY=1
shift
;;
*)
echo "[agent-worktree-prune] Unknown argument: $1" >&2
echo "Usage: $0 [--base <branch>] [--dry-run]" >&2
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty]" >&2
exit 1
;;
esac
Expand All @@ -31,6 +42,46 @@ repo_root="$(git rev-parse --show-toplevel)"
current_pwd="$(pwd -P)"
worktree_root="${repo_root}/.omx/agent-worktrees"

resolve_base_branch() {
local configured=""
local current=""

configured="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
if [[ -n "$configured" ]] && git -C "$repo_root" show-ref --verify --quiet "refs/heads/${configured}"; then
printf '%s' "$configured"
return 0
fi

current="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if [[ -n "$current" && "$current" != "HEAD" ]] && git -C "$repo_root" show-ref --verify --quiet "refs/heads/${current}"; then
printf '%s' "$current"
return 0
fi

for fallback in main dev; do
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${fallback}"; then
printf '%s' "$fallback"
return 0
fi
done

printf '%s' ""
}

if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
echo "[agent-worktree-prune] --base requires a non-empty branch name." >&2
exit 1
fi

if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
BASE_BRANCH="$(resolve_base_branch)"
fi

if [[ -z "$BASE_BRANCH" ]]; then
echo "[agent-worktree-prune] Unable to infer base branch. Pass --base <branch>." >&2
exit 1
fi

if ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${BASE_BRANCH}"; then
echo "[agent-worktree-prune] Base branch not found: ${BASE_BRANCH}" >&2
exit 1
Expand All @@ -49,9 +100,17 @@ branch_has_worktree() {
git -C "$repo_root" worktree list --porcelain | grep -q "^branch refs/heads/${branch}$"
}

is_clean_worktree() {
local wt="$1"
git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
&& git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
&& [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
}

removed_worktrees=0
removed_branches=0
skipped_active=0
skipped_dirty=0

process_entry() {
local wt="$1"
Expand Down Expand Up @@ -89,6 +148,12 @@ process_entry() {
return
fi

if [[ "$FORCE_DIRTY" -ne 1 ]] && ! is_clean_worktree "$wt"; then
skipped_dirty=$((skipped_dirty + 1))
echo "[agent-worktree-prune] Skipping dirty worktree (${remove_reason}): ${wt}"
return
fi

echo "[agent-worktree-prune] Removing worktree (${remove_reason}): ${wt}"
run_cmd git -C "$repo_root" worktree remove "$wt" --force
removed_worktrees=$((removed_worktrees + 1))
Expand Down Expand Up @@ -149,7 +214,10 @@ done < <(git -C "$repo_root" for-each-ref --format='%(refname:short)' refs/heads

run_cmd git -C "$repo_root" worktree prune

echo "[agent-worktree-prune] Summary: removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}"
echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}"
if [[ "$skipped_active" -gt 0 ]]; then
echo "[agent-worktree-prune] Tip: leave active agent worktree directories, then run this command again for full cleanup." >&2
fi
if [[ "$skipped_dirty" -gt 0 ]]; then
echo "[agent-worktree-prune] Tip: dirty worktrees were preserved. Clean/finish them first, or pass --force-dirty to remove anyway." >&2
fi
40 changes: 37 additions & 3 deletions templates/scripts/codex-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ if ! command -v "$CODEX_BIN" >/dev/null 2>&1; then
exit 127
fi

if [[ ! -x "scripts/agent-branch-start.sh" ]]; then
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "[codex-agent] Not inside a git repository." >&2
exit 1
fi
repo_root="$(git rev-parse --show-toplevel)"

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
fi
Expand All @@ -75,7 +81,7 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
start_args+=("$BASE_BRANCH")
fi

start_output="$(bash scripts/agent-branch-start.sh "${start_args[@]}")"
start_output="$(bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}")"
printf '%s\n' "$start_output"

worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
Expand All @@ -91,4 +97,32 @@ fi

echo "[codex-agent] Launching ${CODEX_BIN} in sandbox: $worktree_path"
cd "$worktree_path"
exec "$CODEX_BIN" "$@"
set +e
"$CODEX_BIN" "$@"
codex_exit="$?"
set -e

cd "$repo_root"

if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
echo "[codex-agent] Session ended (exit=${codex_exit}). Running worktree cleanup..."
prune_args=()
if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
prune_args+=(--base "$BASE_BRANCH")
fi
if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
echo "[codex-agent] Warning: automatic worktree cleanup failed." >&2
fi
fi

if [[ ! -d "$worktree_path" ]]; then
echo "[codex-agent] Auto-cleaned sandbox worktree: $worktree_path"
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}\""
fi
fi

exit "$codex_exit"
Loading
Loading