@imdeadpool/guardex 5.0.5 → 5.0.7

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/README.md CHANGED
@@ -110,7 +110,7 @@ gx sync
110
110
  # continuously monitor open PRs targeting current branch and dispatch codex-agent review/merge tasks
111
111
  bash scripts/review-bot-watch.sh --interval 30
112
112
 
113
- # cleanup merged agent branches/worktrees
113
+ # cleanup merged agent branches and hide clean stale agent worktrees
114
114
  gx cleanup
115
115
 
116
116
  # scan/report
@@ -143,7 +143,8 @@ Note: the monitor dispatches Codex through explicit `--task/--agent/--base` flag
143
143
  - `gx setup` checks GitHub CLI (`gh`) and prints install guidance if missing.
144
144
  - Interactive self-update prompt defaults to **No** (`[y/N]`).
145
145
  - In initialized repos, `setup`/`install`/`fix` block protected-base writes unless explicitly overridden.
146
- - In VS Code Source Control, manual (non-Codex) commits/pushes to protected branches are allowed by default.
146
+ - Direct commits/pushes to protected branches are blocked by default (including VS Code Source Control).
147
+ - Optional repo override for manual VS Code protected-branch writes: `git config multiagent.allowVscodeProtectedBranchWrites true`.
147
148
  - Codex/agent sessions stay blocked on protected branches and must use `agent/*` branch + PR workflow.
148
149
  - On protected `main`, `gx doctor` auto-runs in a sandbox agent branch/worktree.
149
150
  - `scripts/agent-branch-start.sh` hydrates `scripts/codex-agent.sh` into new sandbox worktrees when missing, so auto-finish launcher flow stays available.
@@ -238,6 +239,16 @@ npm pack --dry-run
238
239
 
239
240
  ## Release notes
240
241
 
242
+ ### v5.0.7
243
+
244
+ - Bumped package version from `5.0.6` to `5.0.7` to stay one patch ahead for the next npm publish.
245
+
246
+ ### v5.0.6
247
+
248
+ - `gx cleanup` and auto-finish cleanup now prune clean agent worktrees by default, so VS Code Source Control focuses on your local branch plus worktrees with active changes.
249
+ - Added `gx cleanup --keep-clean-worktrees` to opt out and keep clean worktrees visible.
250
+ - Bumped package version from `5.0.5` to `5.0.6` for the next npm publish.
251
+
241
252
  ### v5.0.5
242
253
 
243
254
  - Bumped package version from `5.0.4` to `5.0.5` so npm publish can proceed with the next patch release.
@@ -79,6 +79,7 @@ const CRITICAL_GUARDRAIL_PATHS = new Set([
79
79
 
80
80
  const LOCK_FILE_RELATIVE = '.omx/state/agent-file-locks.json';
81
81
  const AGENTS_MARKER_START = '<!-- multiagent-safety:START -->';
82
+ const AGENTS_MARKER_END = '<!-- multiagent-safety:END -->';
82
83
  const GITIGNORE_MARKER_START = '# multiagent-safety:START';
83
84
  const GITIGNORE_MARKER_END = '# multiagent-safety:END';
84
85
  const MANAGED_GITIGNORE_PATHS = [
@@ -608,6 +609,10 @@ function ensurePackageScripts(repoRoot, dryRun) {
608
609
  function ensureAgentsSnippet(repoRoot, dryRun) {
609
610
  const agentsPath = path.join(repoRoot, 'AGENTS.md');
610
611
  const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md'), 'utf8').trimEnd();
612
+ const managedRegex = new RegExp(
613
+ `${AGENTS_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${AGENTS_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
614
+ 'm',
615
+ );
611
616
 
612
617
  if (!fs.existsSync(agentsPath)) {
613
618
  if (!dryRun) {
@@ -617,8 +622,19 @@ function ensureAgentsSnippet(repoRoot, dryRun) {
617
622
  }
618
623
 
619
624
  const existing = fs.readFileSync(agentsPath, 'utf8');
625
+ if (managedRegex.test(existing)) {
626
+ const next = existing.replace(managedRegex, snippet);
627
+ if (next === existing) {
628
+ return { status: 'unchanged', file: 'AGENTS.md' };
629
+ }
630
+ if (!dryRun) {
631
+ fs.writeFileSync(agentsPath, next, 'utf8');
632
+ }
633
+ return { status: 'updated', file: 'AGENTS.md', note: 'refreshed guardex-managed block' };
634
+ }
635
+
620
636
  if (existing.includes(AGENTS_MARKER_START)) {
621
- return { status: 'unchanged', file: 'AGENTS.md' };
637
+ return { status: 'unchanged', file: 'AGENTS.md', note: 'existing marker found without managed end marker' };
622
638
  }
623
639
 
624
640
  const separator = existing.endsWith('\n') ? '\n' : '\n\n';
@@ -1069,6 +1085,19 @@ function isCommandAvailable(commandName) {
1069
1085
  return run('which', [commandName]).status === 0;
1070
1086
  }
1071
1087
 
1088
+ function extractAgentBranchFinishPrUrl(output) {
1089
+ const match = String(output || '').match(/\[agent-branch-finish\] PR:\s*(\S+)/);
1090
+ return match ? match[1] : '';
1091
+ }
1092
+
1093
+ function doctorFinishFlowIsPending(output) {
1094
+ return (
1095
+ /\[agent-branch-finish\] PR merge not completed yet; leaving PR open\./.test(output) ||
1096
+ /\[agent-branch-finish\] Merge pending review\/check policy\. Branch cleanup skipped for now\./.test(output) ||
1097
+ /\[agent-branch-finish\] PR auto-merge enabled; waiting for required checks\/reviews\./.test(output)
1098
+ );
1099
+ }
1100
+
1072
1101
  function finishDoctorSandboxBranch(blocked, metadata) {
1073
1102
  const finishScript = path.join(metadata.worktreePath, 'scripts', 'agent-branch-finish.sh');
1074
1103
  if (!fs.existsSync(finishScript)) {
@@ -1122,6 +1151,17 @@ function finishDoctorSandboxBranch(blocked, metadata) {
1122
1151
  };
1123
1152
  }
1124
1153
 
1154
+ const combinedOutput = `${finishResult.stdout || ''}\n${finishResult.stderr || ''}`;
1155
+ if (doctorFinishFlowIsPending(combinedOutput)) {
1156
+ return {
1157
+ status: 'pending',
1158
+ note: 'PR created and waiting for merge policy/checks',
1159
+ prUrl: extractAgentBranchFinishPrUrl(combinedOutput),
1160
+ stdout: finishResult.stdout || '',
1161
+ stderr: finishResult.stderr || '',
1162
+ };
1163
+ }
1164
+
1125
1165
  return {
1126
1166
  status: 'completed',
1127
1167
  note: 'doctor sandbox finish flow completed',
@@ -1266,6 +1306,15 @@ function runDoctorInSandbox(options, blocked) {
1266
1306
  console.log(`[${TOOL_NAME}] Auto-finish flow completed for sandbox branch '${metadata.branch}'.`);
1267
1307
  if (finishResult.stdout) process.stdout.write(finishResult.stdout);
1268
1308
  if (finishResult.stderr) process.stderr.write(finishResult.stderr);
1309
+ } else if (finishResult.status === 'pending') {
1310
+ console.log(
1311
+ `[${TOOL_NAME}] Auto-finish pending for sandbox branch '${metadata.branch}': ${finishResult.note}.`,
1312
+ );
1313
+ if (finishResult.prUrl) {
1314
+ console.log(`[${TOOL_NAME}] PR: ${finishResult.prUrl}`);
1315
+ }
1316
+ if (finishResult.stdout) process.stdout.write(finishResult.stdout);
1317
+ if (finishResult.stderr) process.stderr.write(finishResult.stderr);
1269
1318
  } else if (finishResult.status === 'failed') {
1270
1319
  console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
1271
1320
  if (finishResult.stdout) process.stdout.write(finishResult.stdout);
@@ -1818,6 +1867,7 @@ function parseCleanupArgs(rawArgs) {
1818
1867
  dryRun: false,
1819
1868
  forceDirty: false,
1820
1869
  keepRemote: false,
1870
+ keepCleanWorktrees: false,
1821
1871
  };
1822
1872
 
1823
1873
  for (let index = 0; index < rawArgs.length; index += 1) {
@@ -1861,6 +1911,10 @@ function parseCleanupArgs(rawArgs) {
1861
1911
  options.keepRemote = true;
1862
1912
  continue;
1863
1913
  }
1914
+ if (arg === '--keep-clean-worktrees') {
1915
+ options.keepCleanWorktrees = true;
1916
+ continue;
1917
+ }
1864
1918
  throw new Error(`Unknown option: ${arg}`);
1865
1919
  }
1866
1920
 
@@ -2996,6 +3050,9 @@ function cleanup(rawArgs) {
2996
3050
  if (options.dryRun) {
2997
3051
  args.push('--dry-run');
2998
3052
  }
3053
+ if (!options.keepCleanWorktrees) {
3054
+ args.push('--only-dirty-worktrees');
3055
+ }
2999
3056
  args.push('--delete-branches');
3000
3057
  if (!options.keepRemote) {
3001
3058
  args.push('--delete-remote-branches');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imdeadpool/guardex",
3
- "version": "5.0.5",
3
+ "version": "5.0.7",
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"
@@ -17,7 +17,8 @@
17
17
  - 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
18
  - 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
19
  - 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.
20
+ - 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`.
21
+ - Never implement directly on the local/base branch checkout; keep it unchanged and perform all edits in the agent sub-branch/worktree.
21
22
  - If the change publishes or bumps a version, the same change must also update release notes/changelog entries.
22
23
 
23
24
  1. Explicit ownership before edits
@@ -28,6 +28,19 @@ if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n
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="false"
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"
@@ -111,7 +124,7 @@ MSG
111
124
  fi
112
125
 
113
126
  if [[ "$is_protected_branch" == "1" ]]; then
114
- if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1" ]]; then
127
+ if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1" && "$allow_vscode_protected_branch_writes" == "1" ]]; then
115
128
  exit 0
116
129
  fi
117
130
 
@@ -131,6 +144,9 @@ Use an agent branch first:
131
144
  After finishing work:
132
145
  bash scripts/agent-branch-finish.sh
133
146
 
147
+ Optional repo override for manual VS Code protected-branch commits:
148
+ git config multiagent.allowVscodeProtectedBranchWrites true
149
+
134
150
  Temporary bypass (not recommended):
135
151
  ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
136
152
  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="false"
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 VS Code override:"
81
+ echo " git config multiagent.allowVscodeProtectedBranchWrites true"
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
@@ -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
@@ -285,13 +285,84 @@ auto_commit_worktree_changes() {
285
285
 
286
286
  local default_message="Auto-finish: ${TASK_NAME}"
287
287
  local commit_message="${MUSAFETY_CODEX_AUTO_COMMIT_MESSAGE:-$default_message}"
288
+ local commit_output=""
288
289
 
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
290
+ if commit_output="$(git -C "$wt" commit -m "$commit_message" 2>&1)"; then
291
+ echo "[codex-agent] Auto-committed sandbox changes on '${branch}'."
292
+ return 0
293
+ fi
294
+
295
+ if auto_sync_for_commit_retry "$wt" "$branch"; then
296
+ claim_changed_files "$wt" "$branch"
297
+ git -C "$wt" add -A
298
+ if commit_output="$(git -C "$wt" commit -m "$commit_message" 2>&1)"; then
299
+ echo "[codex-agent] Auto-committed sandbox changes on '${branch}' after sync retry."
300
+ return 0
301
+ fi
302
+ fi
303
+
304
+ echo "[codex-agent] Auto-commit failed in sandbox. Keeping branch for manual review: $branch" >&2
305
+ if [[ -n "$commit_output" ]]; then
306
+ printf '%s\n' "$commit_output" >&2
307
+ fi
308
+ return 1
309
+ }
310
+
311
+ auto_sync_for_commit_retry() {
312
+ local wt="$1"
313
+ local branch="$2"
314
+
315
+ if ! has_origin_remote; then
316
+ return 1
317
+ fi
318
+
319
+ local base_branch
320
+ base_branch="$(resolve_worktree_base_branch "$wt")"
321
+ if [[ -z "$base_branch" ]]; then
291
322
  return 1
292
323
  fi
293
324
 
294
- echo "[codex-agent] Auto-committed sandbox changes on '${branch}'."
325
+ if ! git -C "$wt" fetch origin "$base_branch" --quiet; then
326
+ return 1
327
+ fi
328
+
329
+ if ! git -C "$wt" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
330
+ return 1
331
+ fi
332
+
333
+ local behind_count
334
+ behind_count="$(git -C "$wt" rev-list --left-right --count "HEAD...origin/${base_branch}" 2>/dev/null | awk '{print $2}')"
335
+ behind_count="${behind_count:-0}"
336
+ if [[ "$behind_count" -le 0 ]]; then
337
+ return 1
338
+ fi
339
+
340
+ echo "[codex-agent] Auto-commit retry: '${branch}' is behind origin/${base_branch} by ${behind_count} commit(s). Syncing and retrying..."
341
+
342
+ local stash_ref=""
343
+ local stash_output=""
344
+ if worktree_has_changes "$wt"; then
345
+ if ! stash_output="$(git -C "$wt" stash push --include-untracked -m "codex-agent-autocommit-sync-${branch}-$(date +%s)" 2>&1)"; then
346
+ return 1
347
+ fi
348
+ stash_ref="$(printf '%s\n' "$stash_output" | grep -o 'stash@{[0-9]\+}' | head -n 1 || true)"
349
+ fi
350
+
351
+ if ! git -C "$wt" rebase "origin/${base_branch}" >/dev/null 2>&1; then
352
+ git -C "$wt" rebase --abort >/dev/null 2>&1 || true
353
+ if [[ -n "$stash_ref" ]]; then
354
+ git -C "$wt" stash pop "$stash_ref" >/dev/null 2>&1 || true
355
+ fi
356
+ return 1
357
+ fi
358
+
359
+ if [[ -n "$stash_ref" ]]; then
360
+ if ! git -C "$wt" stash pop "$stash_ref" >/dev/null 2>&1; then
361
+ echo "[codex-agent] Auto-commit retry could not re-apply local changes after sync. Manual resolution required in: $wt" >&2
362
+ return 1
363
+ fi
364
+ fi
365
+
295
366
  return 0
296
367
  }
297
368
 
@@ -419,7 +490,7 @@ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
419
490
  prune_args+=(--base "$BASE_BRANCH")
420
491
  fi
421
492
  if [[ "$AUTO_CLEANUP" -eq 1 && "$auto_finish_completed" -eq 1 ]]; then
422
- prune_args+=(--delete-branches --delete-remote-branches)
493
+ prune_args+=(--only-dirty-worktrees --delete-branches --delete-remote-branches)
423
494
  fi
424
495
  if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
425
496
  echo "[codex-agent] Warning: automatic worktree cleanup failed." >&2