@imdeadpool/guardex 5.0.5 → 5.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imdeadpool/guardex",
3
- "version": "5.0.5",
3
+ "version": "5.0.8",
4
4
  "description": "GuardeX: the Guardian T-Rex for your repo, with hardened multi-agent git guardrails.",
5
5
  "license": "MIT",
6
6
  "preferGlobal": true,
@@ -28,7 +28,8 @@
28
28
  "agent:safety:setup": "gx setup",
29
29
  "agent:safety:scan": "gx scan",
30
30
  "agent:safety:fix": "gx fix",
31
- "agent:safety:doctor": "gx doctor"
31
+ "agent:safety:doctor": "gx doctor",
32
+ "agent:review:watch": "bash ./scripts/review-bot-watch.sh"
32
33
  },
33
34
  "engines": {
34
35
  "node": ">=18"
@@ -10,6 +10,7 @@
10
10
  - Before deleting/replacing code, each agent must read the latest session comments/handoffs first and confirm the target code is in their owned scope.
11
11
  - If ownership is unclear or overlaps, stop that edit, post a blocker comment, and let the leader/integrator reassign scope.
12
12
  - For git isolation, each agent must start on a dedicated branch via `scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"`.
13
+ - In-place branch mode is disallowed: never switch the active local/base checkout to an agent branch.
13
14
  - Treat the base branch (`main` or the user's current local base branch) as read-only while the agent branch is active.
14
15
  - 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).
15
16
  - Auto-finish now waits for required checks/merge and then cleans merged sandbox branch/worktree by default.
@@ -17,7 +18,8 @@
17
18
  - If codex-agent auto-finish cannot complete, immediately run `scripts/agent-branch-finish.sh --branch "<agent-branch>" --via-pr --wait-for-merge` and keep the branch open until checks/review pass.
18
19
  - 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.
19
20
  - Completion is not valid until these are true: commit exists on the agent branch, branch is pushed to `origin`, and PR/merge status is produced by `agent-branch-finish.sh` or `codex-agent`.
20
- - 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.
21
+ - For every new task, if an assigned agent sub-branch/worktree is already open, continue in that sub-branch; otherwise create a fresh one from the current local base snapshot with `scripts/agent-branch-start.sh`.
22
+ - Never implement directly on the local/base branch checkout; keep it unchanged and perform all edits in the agent sub-branch/worktree.
21
23
  - If the change publishes or bumps a version, the same change must also update release notes/changelog entries.
22
24
 
23
25
  1. Explicit ownership before edits
@@ -24,10 +24,23 @@ if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}"
24
24
  fi
25
25
 
26
26
  is_vscode_git_context=0
27
- if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" || "${TERM_PROGRAM:-}" == "vscode" ]]; then
27
+ if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" ]]; then
28
28
  is_vscode_git_context=1
29
29
  fi
30
30
 
31
+ allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
32
+ if [[ -z "$allow_vscode_protected_raw" ]]; then
33
+ allow_vscode_protected_raw="true"
34
+ fi
35
+ allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"
36
+
37
+ allow_vscode_protected_branch_writes=0
38
+ case "$allow_vscode_protected" in
39
+ 1|true|yes|on) allow_vscode_protected_branch_writes=1 ;;
40
+ 0|false|no|off) allow_vscode_protected_branch_writes=0 ;;
41
+ *) allow_vscode_protected_branch_writes=0 ;;
42
+ esac
43
+
31
44
  protected_branches_raw="${MUSAFETY_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
32
45
  if [[ -z "$protected_branches_raw" ]]; then
33
46
  protected_branches_raw="dev main master"
@@ -42,6 +55,15 @@ for protected_branch in $protected_branches_raw; do
42
55
  fi
43
56
  done
44
57
 
58
+ is_local_only_branch=0
59
+ if [[ "$is_protected_branch" == "1" ]]; then
60
+ upstream_ref="$(git for-each-ref --format='%(upstream:short)' "refs/heads/${branch}" | head -n 1)"
61
+ remote_branch_ref="$(git for-each-ref --format='%(refname:short)' "refs/remotes/*/${branch}" | head -n 1)"
62
+ if [[ -z "$upstream_ref" && -z "$remote_branch_ref" ]]; then
63
+ is_local_only_branch=1
64
+ fi
65
+ fi
66
+
45
67
  codex_require_agent_branch_raw="${MUSAFETY_CODEX_REQUIRE_AGENT_BRANCH:-$(git config --get multiagent.codexRequireAgentBranch || true)}"
46
68
  if [[ -z "$codex_require_agent_branch_raw" ]]; then
47
69
  codex_require_agent_branch_raw="true"
@@ -112,7 +134,9 @@ fi
112
134
 
113
135
  if [[ "$is_protected_branch" == "1" ]]; then
114
136
  if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1" ]]; then
115
- exit 0
137
+ if [[ "$allow_vscode_protected_branch_writes" == "1" || "$is_local_only_branch" == "1" ]]; then
138
+ exit 0
139
+ fi
116
140
  fi
117
141
 
118
142
  if [[ "$is_unborn_branch" == "1" && "$is_codex_session" != "1" ]]; then
@@ -131,6 +155,12 @@ Use an agent branch first:
131
155
  After finishing work:
132
156
  bash scripts/agent-branch-finish.sh
133
157
 
158
+ Optional repo hard-block for VS Code protected-branch commits:
159
+ git config multiagent.allowVscodeProtectedBranchWrites false
160
+
161
+ VS Code Source Control commits on protected local-only branches
162
+ (no upstream and no remote branch) are allowed automatically.
163
+
134
164
  Temporary bypass (not recommended):
135
165
  ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
136
166
  MSG
@@ -10,6 +10,19 @@ if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n
10
10
  is_vscode_git_context=1
11
11
  fi
12
12
 
13
+ allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
14
+ if [[ -z "$allow_vscode_protected_raw" ]]; then
15
+ allow_vscode_protected_raw="true"
16
+ fi
17
+ allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"
18
+
19
+ allow_vscode_protected_branch_writes=0
20
+ case "$allow_vscode_protected" in
21
+ 1|true|yes|on) allow_vscode_protected_branch_writes=1 ;;
22
+ 0|false|no|off) allow_vscode_protected_branch_writes=0 ;;
23
+ *) allow_vscode_protected_branch_writes=0 ;;
24
+ esac
25
+
13
26
  is_codex_session=0
14
27
  if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}" == "1" ]]; then
15
28
  is_codex_session=1
@@ -56,14 +69,16 @@ if [[ "${#blocked_refs[@]}" -gt 0 ]]; then
56
69
  exit 1
57
70
  fi
58
71
 
59
- if [[ "$is_vscode_git_context" == "1" ]]; then
72
+ if [[ "$is_vscode_git_context" == "1" && "$allow_vscode_protected_branch_writes" == "1" ]]; then
60
73
  exit 0
61
74
  fi
62
75
 
63
76
  {
64
- echo "[agent-branch-guard] Push to protected branch blocked outside VS Code Git context."
77
+ echo "[agent-branch-guard] Push to protected branch blocked."
65
78
  echo "[agent-branch-guard] Protected target(s): ${blocked_refs[*]}"
66
- echo "[agent-branch-guard] Use VS Code Source Control for protected-branch push, or push from an agent branch and merge via PR."
79
+ echo "[agent-branch-guard] Use an agent branch and merge via PR."
80
+ echo "[agent-branch-guard] Optional repo hard-block for VS Code protected-branch push:"
81
+ echo " git config multiagent.allowVscodeProtectedBranchWrites false"
67
82
  echo
68
83
  echo "Temporary bypass (not recommended):"
69
84
  echo " ALLOW_PUSH_ON_PROTECTED_BRANCH=1 git push ..."
@@ -545,7 +545,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
545
545
  fi
546
546
 
547
547
  if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
548
- prune_args=(--base "$BASE_BRANCH" --delete-branches)
548
+ prune_args=(--base "$BASE_BRANCH" --only-dirty-worktrees --delete-branches)
549
549
  if [[ "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
550
550
  prune_args+=(--delete-remote-branches)
551
551
  fi
@@ -5,8 +5,6 @@ TASK_NAME="task"
5
5
  AGENT_NAME="agent"
6
6
  BASE_BRANCH=""
7
7
  BASE_BRANCH_EXPLICIT=0
8
- WORKTREE_MODE=1
9
- ALLOW_IN_PLACE=0
10
8
  WORKTREE_ROOT_REL=".omx/agent-worktrees"
11
9
  POSITIONAL_ARGS=()
12
10
 
@@ -25,13 +23,10 @@ while [[ $# -gt 0 ]]; do
25
23
  BASE_BRANCH_EXPLICIT=1
26
24
  shift 2
27
25
  ;;
28
- --in-place)
29
- WORKTREE_MODE=0
30
- shift
31
- ;;
32
- --allow-in-place)
33
- ALLOW_IN_PLACE=1
34
- shift
26
+ --in-place|--allow-in-place)
27
+ echo "[agent-branch-start] In-place branch mode is disabled." >&2
28
+ echo "[agent-branch-start] This command always creates an isolated worktree to keep your active checkout unchanged." >&2
29
+ exit 1
35
30
  ;;
36
31
  --worktree-root)
37
32
  WORKTREE_ROOT_REL="${2:-.omx/agent-worktrees}"
@@ -47,7 +42,7 @@ while [[ $# -gt 0 ]]; do
47
42
  ;;
48
43
  -*)
49
44
  echo "[agent-branch-start] Unknown option: $1" >&2
50
- echo "Usage: $0 [task] [agent] [base] [--in-place --allow-in-place] [--worktree-root <path>]" >&2
45
+ echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
51
46
  exit 1
52
47
  ;;
53
48
  *)
@@ -59,7 +54,7 @@ done
59
54
 
60
55
  if [[ "${#POSITIONAL_ARGS[@]}" -gt 3 ]]; then
61
56
  echo "[agent-branch-start] Too many positional arguments." >&2
62
- echo "Usage: $0 [task] [agent] [base] [--in-place --allow-in-place] [--worktree-root <path>]" >&2
57
+ echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
63
58
  exit 1
64
59
  fi
65
60
 
@@ -237,34 +232,6 @@ while git show-ref --verify --quiet "refs/heads/${branch_name}"; do
237
232
  branch_suffix=$((branch_suffix + 1))
238
233
  done
239
234
 
240
- if [[ "$WORKTREE_MODE" -eq 0 ]]; then
241
- if [[ "$ALLOW_IN_PLACE" -ne 1 ]]; then
242
- echo "[agent-branch-start] --in-place is blocked by default to prevent accidental edits on protected branches." >&2
243
- echo "[agent-branch-start] If you really need it, pass both: --in-place --allow-in-place" >&2
244
- exit 1
245
- fi
246
-
247
- if ! git diff --quiet || ! git diff --cached --quiet; then
248
- echo "[agent-branch-start] Working tree is not clean. Commit/stash changes before starting an in-place branch." >&2
249
- exit 1
250
- fi
251
-
252
- current_branch="$(git rev-parse --abbrev-ref HEAD)"
253
- if [[ "$current_branch" != "$BASE_BRANCH" ]]; then
254
- git checkout "$BASE_BRANCH"
255
- fi
256
-
257
- if git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
258
- git pull --ff-only origin "$BASE_BRANCH"
259
- fi
260
-
261
- git checkout -b "$branch_name"
262
- git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
263
- echo "[agent-branch-start] Created in-place branch: ${branch_name}"
264
- echo "$branch_name"
265
- exit 0
266
- fi
267
-
268
235
  worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
269
236
  mkdir -p "$worktree_root"
270
237
  worktree_path="${worktree_root}/${branch_name//\//__}"
@@ -7,6 +7,7 @@ DRY_RUN=0
7
7
  FORCE_DIRTY=0
8
8
  DELETE_BRANCHES=0
9
9
  DELETE_REMOTE_BRANCHES=0
10
+ ONLY_DIRTY_WORKTREES=0
10
11
  TARGET_BRANCH=""
11
12
 
12
13
  if [[ -n "$BASE_BRANCH" ]]; then
@@ -36,13 +37,17 @@ while [[ $# -gt 0 ]]; do
36
37
  DELETE_REMOTE_BRANCHES=1
37
38
  shift
38
39
  ;;
40
+ --only-dirty-worktrees)
41
+ ONLY_DIRTY_WORKTREES=1
42
+ shift
43
+ ;;
39
44
  --branch)
40
45
  TARGET_BRANCH="${2:-}"
41
46
  shift 2
42
47
  ;;
43
48
  *)
44
49
  echo "[agent-worktree-prune] Unknown argument: $1" >&2
45
- echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--branch <agent/...>]" >&2
50
+ echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...>]" >&2
46
51
  exit 1
47
52
  ;;
48
53
  esac
@@ -56,6 +61,11 @@ fi
56
61
  repo_root="$(git rev-parse --show-toplevel)"
57
62
  current_pwd="$(pwd -P)"
58
63
  worktree_root="${repo_root}/.omx/agent-worktrees"
64
+ repo_common_dir="$(
65
+ git -C "$repo_root" rev-parse --git-common-dir \
66
+ | awk -v root="$repo_root" '{ if ($0 ~ /^\//) { print $0 } else { print root "/" $0 } }'
67
+ )"
68
+ repo_common_dir="$(cd "$repo_common_dir" && pwd -P)"
59
69
 
60
70
  resolve_base_branch() {
61
71
  local configured=""
@@ -127,11 +137,98 @@ is_clean_worktree() {
127
137
  && [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
128
138
  }
129
139
 
140
+ resolve_worktree_common_dir() {
141
+ local wt="$1"
142
+ local common_dir=""
143
+ common_dir="$(git -C "$wt" rev-parse --git-common-dir 2>/dev/null || true)"
144
+ if [[ -z "$common_dir" ]]; then
145
+ return 1
146
+ fi
147
+ if [[ "$common_dir" == /* ]]; then
148
+ common_dir="$(cd "$common_dir" 2>/dev/null && pwd -P || true)"
149
+ else
150
+ common_dir="$(cd "$wt/$common_dir" 2>/dev/null && pwd -P || true)"
151
+ fi
152
+ if [[ -z "$common_dir" ]]; then
153
+ return 1
154
+ fi
155
+ printf '%s' "$common_dir"
156
+ }
157
+
158
+ select_unique_worktree_path() {
159
+ local root="$1"
160
+ local name="$2"
161
+ local candidate="${root}/${name}"
162
+ local suffix=2
163
+ while [[ -e "$candidate" ]]; do
164
+ candidate="${root}/${name}-${suffix}"
165
+ suffix=$((suffix + 1))
166
+ done
167
+ printf '%s' "$candidate"
168
+ }
169
+
170
+ relocated_foreign=0
171
+ skipped_foreign=0
172
+
173
+ relocate_foreign_worktree_entries() {
174
+ [[ -d "$worktree_root" ]] || return 0
175
+
176
+ local entry=""
177
+ for entry in "${worktree_root}"/*; do
178
+ [[ -d "$entry" ]] || continue
179
+ if ! git -C "$entry" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
180
+ continue
181
+ fi
182
+
183
+ local entry_common_dir=""
184
+ entry_common_dir="$(resolve_worktree_common_dir "$entry" || true)"
185
+ [[ -n "$entry_common_dir" ]] || continue
186
+
187
+ if [[ "$entry_common_dir" == "$repo_common_dir" ]]; then
188
+ continue
189
+ fi
190
+
191
+ if [[ "$(basename "$entry_common_dir")" != ".git" ]]; then
192
+ skipped_foreign=$((skipped_foreign + 1))
193
+ echo "[agent-worktree-prune] Skipping foreign worktree with unsupported git common dir: ${entry}"
194
+ continue
195
+ fi
196
+
197
+ local owner_repo_root
198
+ owner_repo_root="$(dirname "$entry_common_dir")"
199
+ local owner_worktree_root="${owner_repo_root}/.omx/agent-worktrees"
200
+ local target_path
201
+ target_path="$(select_unique_worktree_path "$owner_worktree_root" "$(basename "$entry")")"
202
+
203
+ if [[ "$entry" == "$current_pwd" || "$current_pwd" == "${entry}"/* ]]; then
204
+ skipped_foreign=$((skipped_foreign + 1))
205
+ echo "[agent-worktree-prune] Skipping active foreign worktree: ${entry}"
206
+ continue
207
+ fi
208
+
209
+ echo "[agent-worktree-prune] Relocating foreign worktree to owning repo: ${entry} -> ${target_path}"
210
+ if [[ "$DRY_RUN" -eq 1 ]]; then
211
+ relocated_foreign=$((relocated_foreign + 1))
212
+ continue
213
+ fi
214
+
215
+ mkdir -p "$owner_worktree_root"
216
+ if git -C "$owner_repo_root" worktree move "$entry" "$target_path" >/dev/null 2>&1; then
217
+ relocated_foreign=$((relocated_foreign + 1))
218
+ else
219
+ skipped_foreign=$((skipped_foreign + 1))
220
+ echo "[agent-worktree-prune] Failed to relocate foreign worktree: ${entry}" >&2
221
+ fi
222
+ done
223
+ }
224
+
130
225
  removed_worktrees=0
131
226
  removed_branches=0
132
227
  skipped_active=0
133
228
  skipped_dirty=0
134
229
 
230
+ relocate_foreign_worktree_entries
231
+
135
232
  process_entry() {
136
233
  local wt="$1"
137
234
  local branch_ref="$2"
@@ -165,6 +262,8 @@ process_entry() {
165
262
  if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
166
263
  remove_reason="merged-agent-branch"
167
264
  fi
265
+ elif [[ "$ONLY_DIRTY_WORKTREES" -eq 1 ]] && is_clean_worktree "$wt"; then
266
+ remove_reason="clean-agent-worktree"
168
267
  fi
169
268
  elif [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]; then
170
269
  remove_reason="temporary-worktree"
@@ -258,6 +357,9 @@ fi
258
357
  run_cmd git -C "$repo_root" worktree prune
259
358
 
260
359
  echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}"
360
+ if [[ "$relocated_foreign" -gt 0 || "$skipped_foreign" -gt 0 ]]; then
361
+ echo "[agent-worktree-prune] Foreign routing: relocated=${relocated_foreign}, skipped=${skipped_foreign}"
362
+ fi
261
363
  if [[ "$skipped_active" -gt 0 ]]; then
262
364
  echo "[agent-worktree-prune] Tip: leave active agent worktree directories, then run this command again for full cleanup." >&2
263
365
  fi
@@ -125,6 +125,106 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
125
125
  fi
126
126
  repo_root="$(git rev-parse --show-toplevel)"
127
127
 
128
+ sanitize_slug() {
129
+ local raw="$1"
130
+ local fallback="${2:-task}"
131
+ local slug
132
+ slug="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
133
+ if [[ -z "$slug" ]]; then
134
+ slug="$fallback"
135
+ fi
136
+ printf '%s' "$slug"
137
+ }
138
+
139
+ resolve_start_base_branch() {
140
+ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
141
+ printf '%s' "$BASE_BRANCH"
142
+ return 0
143
+ fi
144
+
145
+ local configured_base
146
+ configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
147
+ if [[ -n "$configured_base" ]]; then
148
+ printf '%s' "$configured_base"
149
+ return 0
150
+ fi
151
+
152
+ local current_branch
153
+ current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
154
+ if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
155
+ printf '%s' "$current_branch"
156
+ return 0
157
+ fi
158
+
159
+ printf 'dev'
160
+ }
161
+
162
+ resolve_start_ref() {
163
+ local base_branch="$1"
164
+ git -C "$repo_root" fetch origin "$base_branch" --quiet >/dev/null 2>&1 || true
165
+ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
166
+ printf 'origin/%s' "$base_branch"
167
+ return 0
168
+ fi
169
+ if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${base_branch}"; then
170
+ printf '%s' "$base_branch"
171
+ return 0
172
+ fi
173
+ return 1
174
+ }
175
+
176
+ restore_repo_branch_if_changed() {
177
+ local expected_branch="$1"
178
+ if [[ -z "$expected_branch" || "$expected_branch" == "HEAD" ]]; then
179
+ return 0
180
+ fi
181
+ local current_branch
182
+ current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
183
+ if [[ -z "$current_branch" || "$current_branch" == "$expected_branch" ]]; then
184
+ return 0
185
+ fi
186
+ git -C "$repo_root" checkout "$expected_branch" >/dev/null 2>&1
187
+ }
188
+
189
+ start_sandbox_fallback() {
190
+ local base_branch start_ref timestamp task_slug agent_slug branch_name_base branch_name suffix
191
+ local worktree_root worktree_path
192
+
193
+ base_branch="$(resolve_start_base_branch)"
194
+ if ! start_ref="$(resolve_start_ref "$base_branch")"; then
195
+ echo "[codex-agent] Unable to resolve base ref for fallback sandbox start: ${base_branch}" >&2
196
+ return 1
197
+ fi
198
+
199
+ timestamp="$(date +%Y%m%d-%H%M%S)"
200
+ task_slug="$(sanitize_slug "$TASK_NAME" "task")"
201
+ agent_slug="$(sanitize_slug "$AGENT_NAME" "agent")"
202
+ branch_name_base="agent/${agent_slug}/${timestamp}-${task_slug}"
203
+ branch_name="$branch_name_base"
204
+ suffix=2
205
+ while git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch_name}"; do
206
+ branch_name="${branch_name_base}-${suffix}"
207
+ suffix=$((suffix + 1))
208
+ done
209
+
210
+ worktree_root="${repo_root}/.omx/agent-worktrees"
211
+ mkdir -p "$worktree_root"
212
+ worktree_path="${worktree_root}/${branch_name//\//__}"
213
+ if [[ -e "$worktree_path" ]]; then
214
+ echo "[codex-agent] Fallback worktree path already exists: $worktree_path" >&2
215
+ return 1
216
+ fi
217
+
218
+ git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref" >/dev/null
219
+ git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$base_branch" >/dev/null 2>&1 || true
220
+ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
221
+ git -C "$worktree_path" branch --set-upstream-to="origin/${base_branch}" "$branch_name" >/dev/null 2>&1 || true
222
+ fi
223
+
224
+ printf '[agent-branch-start] Created branch: %s\n' "$branch_name"
225
+ printf '[agent-branch-start] Worktree: %s\n' "$worktree_path"
226
+ }
227
+
128
228
  if [[ ! -x "${repo_root}/scripts/agent-branch-start.sh" ]]; then
129
229
  echo "[codex-agent] Missing scripts/agent-branch-start.sh. Run: gx setup" >&2
130
230
  exit 1
@@ -135,12 +235,53 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
135
235
  start_args+=("$BASE_BRANCH")
136
236
  fi
137
237
 
138
- start_output="$(bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}")"
139
- printf '%s\n' "$start_output"
238
+ initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
239
+ start_output=""
240
+ start_status=0
241
+ set +e
242
+ start_output="$(bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
243
+ start_status=$?
244
+ set -e
140
245
 
141
246
  worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
247
+ current_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
248
+ resolved_repo_root="$(cd "$repo_root" && pwd -P)"
249
+ resolved_worktree_path=""
250
+ if [[ -n "$worktree_path" && -d "$worktree_path" ]]; then
251
+ resolved_worktree_path="$(cd "$worktree_path" && pwd -P)"
252
+ fi
253
+
254
+ fallback_reason=""
255
+ if [[ "$start_status" -ne 0 ]]; then
256
+ fallback_reason="starter exited with status ${start_status}"
257
+ elif [[ -z "$worktree_path" ]]; then
258
+ fallback_reason="starter did not report worktree path"
259
+ elif [[ -n "$resolved_worktree_path" && "$resolved_worktree_path" == "$resolved_repo_root" ]]; then
260
+ fallback_reason="starter pointed to active checkout path"
261
+ elif [[ -n "$initial_repo_branch" && -n "$current_repo_branch" && "$current_repo_branch" != "$initial_repo_branch" ]]; then
262
+ fallback_reason="starter switched active checkout branch"
263
+ fi
264
+
265
+ if [[ -n "$fallback_reason" ]]; then
266
+ if ! restore_repo_branch_if_changed "$initial_repo_branch"; then
267
+ echo "[codex-agent] agent-branch-start changed the active checkout branch and restore failed." >&2
268
+ echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
269
+ exit 1
270
+ fi
271
+ if [[ -n "$start_output" ]]; then
272
+ printf '%s\n' "$start_output" >&2
273
+ fi
274
+ echo "[codex-agent] Unsafe starter output (${fallback_reason}); creating sandbox worktree directly." >&2
275
+ start_output="$(start_sandbox_fallback)"
276
+ printf '%s\n' "$start_output"
277
+ worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
278
+ else
279
+ printf '%s\n' "$start_output"
280
+ fi
281
+
142
282
  if [[ -z "$worktree_path" ]]; then
143
- echo "[codex-agent] Could not determine sandbox worktree path from agent-branch-start output." >&2
283
+ echo "[codex-agent] Could not determine sandbox worktree path from sandbox startup output." >&2
284
+ echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
144
285
  exit 1
145
286
  fi
146
287
 
@@ -285,13 +426,84 @@ auto_commit_worktree_changes() {
285
426
 
286
427
  local default_message="Auto-finish: ${TASK_NAME}"
287
428
  local commit_message="${MUSAFETY_CODEX_AUTO_COMMIT_MESSAGE:-$default_message}"
429
+ local commit_output=""
430
+
431
+ if commit_output="$(git -C "$wt" commit -m "$commit_message" 2>&1)"; then
432
+ echo "[codex-agent] Auto-committed sandbox changes on '${branch}'."
433
+ return 0
434
+ fi
435
+
436
+ if auto_sync_for_commit_retry "$wt" "$branch"; then
437
+ claim_changed_files "$wt" "$branch"
438
+ git -C "$wt" add -A
439
+ if commit_output="$(git -C "$wt" commit -m "$commit_message" 2>&1)"; then
440
+ echo "[codex-agent] Auto-committed sandbox changes on '${branch}' after sync retry."
441
+ return 0
442
+ fi
443
+ fi
444
+
445
+ echo "[codex-agent] Auto-commit failed in sandbox. Keeping branch for manual review: $branch" >&2
446
+ if [[ -n "$commit_output" ]]; then
447
+ printf '%s\n' "$commit_output" >&2
448
+ fi
449
+ return 1
450
+ }
451
+
452
+ auto_sync_for_commit_retry() {
453
+ local wt="$1"
454
+ local branch="$2"
455
+
456
+ if ! has_origin_remote; then
457
+ return 1
458
+ fi
459
+
460
+ local base_branch
461
+ base_branch="$(resolve_worktree_base_branch "$wt")"
462
+ if [[ -z "$base_branch" ]]; then
463
+ return 1
464
+ fi
288
465
 
289
- if ! git -C "$wt" commit -m "$commit_message" >/dev/null 2>&1; then
290
- echo "[codex-agent] Auto-commit failed in sandbox. Keeping branch for manual review: $branch" >&2
466
+ if ! git -C "$wt" fetch origin "$base_branch" --quiet; then
467
+ return 1
468
+ fi
469
+
470
+ if ! git -C "$wt" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
291
471
  return 1
292
472
  fi
293
473
 
294
- echo "[codex-agent] Auto-committed sandbox changes on '${branch}'."
474
+ local behind_count
475
+ behind_count="$(git -C "$wt" rev-list --left-right --count "HEAD...origin/${base_branch}" 2>/dev/null | awk '{print $2}')"
476
+ behind_count="${behind_count:-0}"
477
+ if [[ "$behind_count" -le 0 ]]; then
478
+ return 1
479
+ fi
480
+
481
+ echo "[codex-agent] Auto-commit retry: '${branch}' is behind origin/${base_branch} by ${behind_count} commit(s). Syncing and retrying..."
482
+
483
+ local stash_ref=""
484
+ local stash_output=""
485
+ if worktree_has_changes "$wt"; then
486
+ if ! stash_output="$(git -C "$wt" stash push --include-untracked -m "codex-agent-autocommit-sync-${branch}-$(date +%s)" 2>&1)"; then
487
+ return 1
488
+ fi
489
+ stash_ref="$(printf '%s\n' "$stash_output" | grep -o 'stash@{[0-9]\+}' | head -n 1 || true)"
490
+ fi
491
+
492
+ if ! git -C "$wt" rebase "origin/${base_branch}" >/dev/null 2>&1; then
493
+ git -C "$wt" rebase --abort >/dev/null 2>&1 || true
494
+ if [[ -n "$stash_ref" ]]; then
495
+ git -C "$wt" stash pop "$stash_ref" >/dev/null 2>&1 || true
496
+ fi
497
+ return 1
498
+ fi
499
+
500
+ if [[ -n "$stash_ref" ]]; then
501
+ if ! git -C "$wt" stash pop "$stash_ref" >/dev/null 2>&1; then
502
+ echo "[codex-agent] Auto-commit retry could not re-apply local changes after sync. Manual resolution required in: $wt" >&2
503
+ return 1
504
+ fi
505
+ fi
506
+
295
507
  return 0
296
508
  }
297
509
 
@@ -419,7 +631,7 @@ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
419
631
  prune_args+=(--base "$BASE_BRANCH")
420
632
  fi
421
633
  if [[ "$AUTO_CLEANUP" -eq 1 && "$auto_finish_completed" -eq 1 ]]; then
422
- prune_args+=(--delete-branches --delete-remote-branches)
634
+ prune_args+=(--only-dirty-worktrees --delete-branches --delete-remote-branches)
423
635
  fi
424
636
  if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
425
637
  echo "[codex-agent] Warning: automatic worktree cleanup failed." >&2