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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ 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 base detection)
bash scripts/agent-worktree-prune.sh --force-dirty # remove stale dirty worktrees too
bash scripts/openspec/init-plan-workspace.sh <plan-slug> # optional OpenSpec plan scaffold
```

Expand All @@ -284,6 +285,7 @@ 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.

## Advanced commands

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
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