@imdeadpool/guardex 5.0.1 → 5.0.3

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.1",
3
+ "version": "5.0.3",
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,
@@ -15,7 +15,7 @@
15
15
  "agent:codex": "bash ./scripts/codex-agent.sh",
16
16
  "agent:branch:start": "bash ./scripts/agent-branch-start.sh",
17
17
  "agent:branch:finish": "bash ./scripts/agent-branch-finish.sh",
18
- "agent:cleanup": "bash ./scripts/agent-worktree-prune.sh --base dev",
18
+ "agent:cleanup": "gx cleanup",
19
19
  "agent:hooks:install": "bash ./scripts/install-agent-git-hooks.sh",
20
20
  "agent:locks:claim": "python3 ./scripts/agent-file-locks.py claim",
21
21
  "agent:locks:allow-delete": "python3 ./scripts/agent-file-locks.py allow-delete",
@@ -11,8 +11,14 @@
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
13
  - Treat the base branch (`main` or the user's current local base branch) as read-only while the agent branch is active.
14
- - Agent completion must use `scripts/agent-branch-finish.sh` (direct merge to base when allowed; auto PR fallback for protected bases, then cleanup after merge).
14
+ - 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
+ - Auto-finish now waits for required checks/merge and then cleans merged sandbox branch/worktree by default.
16
+ - Use `--no-cleanup` only when you explicitly need to keep a merged sandbox for audit/debug follow-up.
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
+ - 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
+ - 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`.
15
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
+ - If the change publishes or bumps a version, the same change must also update release notes/changelog entries.
16
22
 
17
23
  1. Explicit ownership before edits
18
24
 
@@ -36,4 +36,6 @@ gx scan
36
36
  - Keep agent work isolated (`agent/*` branches + lock claims).
37
37
  - For every new user message/task, restart the full loop on a fresh agent branch/worktree.
38
38
  - For one-command Codex sandbox startup, use `bash scripts/codex-agent.sh "<task>" "<agent-name>"`.
39
+ - `scripts/codex-agent.sh` auto-syncs the sandbox branch against base before each task and auto-finishes merge/PR flow after Codex exits.
40
+ - Auto-finish keeps the branch/worktree by default; remove merged branches explicitly with `gx cleanup` (or `gx cleanup --branch "<agent-branch>"`).
39
41
  - Do not bypass protected branch safeguards unless explicitly required.
@@ -50,9 +50,31 @@ case "$codex_require_agent_branch" in
50
50
  *) should_require_codex_agent_branch=1 ;;
51
51
  esac
52
52
 
53
+ is_codex_managed_only_commit_on_protected=0
54
+ if [[ "$is_codex_session" == "1" && "$is_protected_branch" == "1" ]]; then
55
+ deleted_paths="$(git diff --cached --name-only --diff-filter=D)"
56
+ staged_paths="$(git diff --cached --name-only --diff-filter=ACMRTUXB)"
57
+ if [[ -z "$deleted_paths" && -n "$staged_paths" ]]; then
58
+ managed_only=1
59
+ while IFS= read -r staged_path; do
60
+ case "$staged_path" in
61
+ AGENTS.md|.gitignore) ;;
62
+ *) managed_only=0; break ;;
63
+ esac
64
+ done <<< "$staged_paths"
65
+ if [[ "$managed_only" == "1" ]]; then
66
+ is_codex_managed_only_commit_on_protected=1
67
+ fi
68
+ fi
69
+ fi
70
+
53
71
  if [[ "$should_require_codex_agent_branch" == "1" && "${MUSAFETY_ALLOW_CODEX_ON_NON_AGENT:-0}" != "1" ]]; then
54
72
  if [[ "$is_codex_session" == "1" && "$branch" != agent/* ]]; then
55
73
  if [[ "$is_protected_branch" == "1" ]]; then
74
+ if [[ "$is_codex_managed_only_commit_on_protected" == "1" ]]; then
75
+ exit 0
76
+ fi
77
+
56
78
  cat >&2 <<'MSG'
57
79
  [guardex-preedit-guard] Codex edit/commit detected on a protected branch.
58
80
  GuardeX requires Codex work to run from an isolated agent/* branch.
@@ -5,9 +5,49 @@ BASE_BRANCH=""
5
5
  BASE_BRANCH_EXPLICIT=0
6
6
  SOURCE_BRANCH=""
7
7
  PUSH_ENABLED=1
8
- DELETE_REMOTE_BRANCH=1
8
+ DELETE_REMOTE_BRANCH=0
9
+ DELETE_REMOTE_BRANCH_EXPLICIT=0
9
10
  MERGE_MODE="auto"
10
11
  GH_BIN="${MUSAFETY_GH_BIN:-gh}"
12
+ CLEANUP_AFTER_MERGE_RAW="${MUSAFETY_FINISH_CLEANUP:-false}"
13
+ WAIT_FOR_MERGE_RAW="${MUSAFETY_FINISH_WAIT_FOR_MERGE:-false}"
14
+ WAIT_TIMEOUT_SECONDS_RAW="${MUSAFETY_FINISH_WAIT_TIMEOUT_SECONDS:-1800}"
15
+ WAIT_POLL_SECONDS_RAW="${MUSAFETY_FINISH_WAIT_POLL_SECONDS:-10}"
16
+
17
+ normalize_bool() {
18
+ local raw="${1:-}"
19
+ local fallback="${2:-0}"
20
+ local lowered
21
+ lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
22
+ case "$lowered" in
23
+ 1|true|yes|on) printf '1' ;;
24
+ 0|false|no|off) printf '0' ;;
25
+ '') printf '%s' "$fallback" ;;
26
+ *) printf '%s' "$fallback" ;;
27
+ esac
28
+ }
29
+
30
+ normalize_int() {
31
+ local raw="${1:-}"
32
+ local fallback="${2:-0}"
33
+ local min_value="${3:-0}"
34
+ local value="$raw"
35
+
36
+ if [[ -z "$value" || ! "$value" =~ ^[0-9]+$ ]]; then
37
+ value="$fallback"
38
+ fi
39
+
40
+ if (( value < min_value )); then
41
+ value="$min_value"
42
+ fi
43
+
44
+ printf '%s' "$value"
45
+ }
46
+
47
+ CLEANUP_AFTER_MERGE="$(normalize_bool "$CLEANUP_AFTER_MERGE_RAW" "0")"
48
+ WAIT_FOR_MERGE="$(normalize_bool "$WAIT_FOR_MERGE_RAW" "0")"
49
+ WAIT_TIMEOUT_SECONDS="$(normalize_int "$WAIT_TIMEOUT_SECONDS_RAW" "1800" "30")"
50
+ WAIT_POLL_SECONDS="$(normalize_int "$WAIT_POLL_SECONDS_RAW" "10" "0")"
11
51
 
12
52
  while [[ $# -gt 0 ]]; do
13
53
  case "$1" in
@@ -26,8 +66,38 @@ while [[ $# -gt 0 ]]; do
26
66
  ;;
27
67
  --keep-remote-branch)
28
68
  DELETE_REMOTE_BRANCH=0
69
+ DELETE_REMOTE_BRANCH_EXPLICIT=1
70
+ shift
71
+ ;;
72
+ --delete-remote-branch)
73
+ DELETE_REMOTE_BRANCH=1
74
+ DELETE_REMOTE_BRANCH_EXPLICIT=1
75
+ shift
76
+ ;;
77
+ --cleanup)
78
+ CLEANUP_AFTER_MERGE=1
79
+ shift
80
+ ;;
81
+ --no-cleanup)
82
+ CLEANUP_AFTER_MERGE=0
83
+ shift
84
+ ;;
85
+ --wait-for-merge)
86
+ WAIT_FOR_MERGE=1
87
+ shift
88
+ ;;
89
+ --no-wait-for-merge)
90
+ WAIT_FOR_MERGE=0
29
91
  shift
30
92
  ;;
93
+ --wait-timeout-seconds)
94
+ WAIT_TIMEOUT_SECONDS="$(normalize_int "${2:-}" "1800" "30")"
95
+ shift 2
96
+ ;;
97
+ --wait-poll-seconds)
98
+ WAIT_POLL_SECONDS="$(normalize_int "${2:-}" "10" "0")"
99
+ shift 2
100
+ ;;
31
101
  --mode)
32
102
  MERGE_MODE="${2:-auto}"
33
103
  shift 2
@@ -42,12 +112,16 @@ while [[ $# -gt 0 ]]; do
42
112
  ;;
43
113
  *)
44
114
  echo "[agent-branch-finish] Unknown argument: $1" >&2
45
- echo "Usage: $0 [--base <branch>] [--branch <branch>] [--no-push] [--keep-remote-branch] [--mode auto|direct|pr|--via-pr|--direct-only]" >&2
115
+ echo "Usage: $0 [--base <branch>] [--branch <branch>] [--no-push] [--cleanup|--no-cleanup] [--wait-for-merge|--no-wait-for-merge] [--wait-timeout-seconds <n>] [--wait-poll-seconds <n>] [--keep-remote-branch|--delete-remote-branch] [--mode auto|direct|pr|--via-pr|--direct-only]" >&2
46
116
  exit 1
47
117
  ;;
48
118
  esac
49
119
  done
50
120
 
121
+ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 && "$DELETE_REMOTE_BRANCH_EXPLICIT" -eq 0 ]]; then
122
+ DELETE_REMOTE_BRANCH=1
123
+ fi
124
+
51
125
  case "$MERGE_MODE" in
52
126
  auto|direct|pr) ;;
53
127
  *)
@@ -63,6 +137,14 @@ fi
63
137
 
64
138
  repo_root="$(git rev-parse --show-toplevel)"
65
139
  current_worktree="$(pwd -P)"
140
+ common_git_dir_raw="$(git -C "$repo_root" rev-parse --git-common-dir)"
141
+ if [[ "$common_git_dir_raw" == /* ]]; then
142
+ common_git_dir="$common_git_dir_raw"
143
+ else
144
+ common_git_dir="$(cd "$repo_root/$common_git_dir_raw" && pwd -P)"
145
+ fi
146
+ repo_common_root="$(cd "$common_git_dir/.." && pwd -P)"
147
+ agent_worktree_root="${repo_common_root}/.omx/agent-worktrees"
66
148
 
67
149
  if [[ -z "$SOURCE_BRANCH" ]]; then
68
150
  SOURCE_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
@@ -136,7 +218,7 @@ created_source_probe=0
136
218
  source_probe_path=""
137
219
 
138
220
  if [[ -z "$source_worktree" ]]; then
139
- source_probe_path="${repo_root}/.omx/agent-worktrees/__source-probe-${SOURCE_BRANCH//\//__}-$(date +%Y%m%d-%H%M%S)"
221
+ source_probe_path="${agent_worktree_root}/__source-probe-${SOURCE_BRANCH//\//__}-$(date +%Y%m%d-%H%M%S)"
140
222
  mkdir -p "$(dirname "$source_probe_path")"
141
223
  git -C "$repo_root" worktree add "$source_probe_path" "$SOURCE_BRANCH" >/dev/null
142
224
  source_worktree="$source_probe_path"
@@ -194,7 +276,7 @@ if [[ "$should_require_sync" -eq 1 ]] && git -C "$repo_root" show-ref --verify -
194
276
  fi
195
277
  fi
196
278
 
197
- integration_worktree="${repo_root}/.omx/agent-worktrees/__integrate-${BASE_BRANCH//\//__}-$(date +%Y%m%d-%H%M%S)"
279
+ integration_worktree="${agent_worktree_root}/__integrate-${BASE_BRANCH//\//__}-$(date +%Y%m%d-%H%M%S)"
198
280
  integration_branch="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
199
281
  mkdir -p "$(dirname "$integration_worktree")"
200
282
 
@@ -254,6 +336,78 @@ is_local_branch_delete_error() {
254
336
  return 1
255
337
  }
256
338
 
339
+ read_pr_state() {
340
+ local state_line
341
+ state_line="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json state,mergedAt,url --jq '[.state, (.mergedAt // ""), (.url // "")] | @tsv' 2>/dev/null || true)"
342
+ if [[ -z "$state_line" ]]; then
343
+ return 1
344
+ fi
345
+
346
+ local parsed_state=""
347
+ local parsed_merged_at=""
348
+ local parsed_url=""
349
+ IFS=$'\t' read -r parsed_state parsed_merged_at parsed_url <<< "$state_line"
350
+ PR_STATE="$parsed_state"
351
+ PR_MERGED_AT="$parsed_merged_at"
352
+ if [[ -n "$parsed_url" ]]; then
353
+ pr_url="$parsed_url"
354
+ fi
355
+ return 0
356
+ }
357
+
358
+ wait_for_pr_merge() {
359
+ local deadline
360
+ deadline=$(( $(date +%s) + WAIT_TIMEOUT_SECONDS ))
361
+ local wait_notice_printed=0
362
+ local merge_output=""
363
+
364
+ while true; do
365
+ if merge_output="$("$GH_BIN" pr merge "$SOURCE_BRANCH" --squash --delete-branch 2>&1)"; then
366
+ return 0
367
+ fi
368
+ if is_local_branch_delete_error "$merge_output"; then
369
+ echo "[agent-branch-finish] PR merged but gh could not delete the local branch (active worktree); continuing local cleanup." >&2
370
+ return 0
371
+ fi
372
+
373
+ PR_STATE=""
374
+ PR_MERGED_AT=""
375
+ if read_pr_state; then
376
+ if [[ "$PR_STATE" == "MERGED" || -n "$PR_MERGED_AT" ]]; then
377
+ return 0
378
+ fi
379
+ if [[ "$PR_STATE" == "CLOSED" ]]; then
380
+ echo "[agent-branch-finish] PR closed without merge; cannot continue auto-finish." >&2
381
+ if [[ -n "$pr_url" ]]; then
382
+ echo "[agent-branch-finish] PR: ${pr_url}" >&2
383
+ fi
384
+ if [[ -n "$merge_output" ]]; then
385
+ echo "$merge_output" >&2
386
+ fi
387
+ return 1
388
+ fi
389
+ fi
390
+
391
+ if [[ "$wait_notice_printed" -eq 0 ]]; then
392
+ echo "[agent-branch-finish] Waiting for required checks/reviews, then retrying merge automatically (timeout ${WAIT_TIMEOUT_SECONDS}s)." >&2
393
+ if [[ -n "$pr_url" ]]; then
394
+ echo "[agent-branch-finish] PR: ${pr_url}" >&2
395
+ fi
396
+ wait_notice_printed=1
397
+ fi
398
+
399
+ if (( $(date +%s) >= deadline )); then
400
+ echo "[agent-branch-finish] Timed out waiting for PR merge after ${WAIT_TIMEOUT_SECONDS}s." >&2
401
+ if [[ -n "$merge_output" ]]; then
402
+ echo "$merge_output" >&2
403
+ fi
404
+ return 2
405
+ fi
406
+
407
+ sleep "$WAIT_POLL_SECONDS"
408
+ done
409
+ }
410
+
257
411
  run_pr_flow() {
258
412
  if ! command -v "$GH_BIN" >/dev/null 2>&1; then
259
413
  echo "[agent-branch-finish] PR fallback requested but GitHub CLI not found: ${GH_BIN}" >&2
@@ -285,6 +439,11 @@ run_pr_flow() {
285
439
  return 0
286
440
  fi
287
441
 
442
+ if [[ "$WAIT_FOR_MERGE" -eq 1 ]]; then
443
+ wait_for_pr_merge
444
+ return $?
445
+ fi
446
+
288
447
  auto_output=""
289
448
  if auto_output="$("$GH_BIN" pr merge "$SOURCE_BRANCH" --squash --delete-branch --auto 2>&1)"; then
290
449
  echo "[agent-branch-finish] PR auto-merge enabled; waiting for required checks/reviews." >&2
@@ -330,6 +489,10 @@ if [[ "$PUSH_ENABLED" -eq 1 ]]; then
330
489
  if [[ -n "$pr_url" ]]; then
331
490
  echo "[agent-branch-finish] PR: ${pr_url}" >&2
332
491
  fi
492
+ if [[ "$WAIT_FOR_MERGE" -eq 1 ]]; then
493
+ echo "[agent-branch-finish] Merge did not complete within wait window; keeping branch open." >&2
494
+ exit 1
495
+ fi
333
496
  echo "[agent-branch-finish] Merge pending review/check policy. Branch cleanup skipped for now." >&2
334
497
  exit 0
335
498
  fi
@@ -347,43 +510,63 @@ if [[ -x "${repo_root}/scripts/agent-file-locks.py" ]]; then
347
510
  python3 "${repo_root}/scripts/agent-file-locks.py" release --branch "$SOURCE_BRANCH" >/dev/null 2>&1 || true
348
511
  fi
349
512
 
350
- if [[ "$source_worktree" == "$repo_root" ]]; then
351
- if is_clean_worktree "$source_worktree"; then
352
- git -C "$source_worktree" checkout "$BASE_BRANCH" >/dev/null 2>&1 || true
353
- if [[ "$PUSH_ENABLED" -eq 1 ]] && git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
354
- git -C "$source_worktree" pull --ff-only origin "$BASE_BRANCH" >/dev/null 2>&1 || true
513
+ base_worktree="$(get_worktree_for_branch "$BASE_BRANCH")"
514
+ if [[ -n "$base_worktree" ]] && is_clean_worktree "$base_worktree" && [[ "$PUSH_ENABLED" -eq 1 ]]; then
515
+ git -C "$base_worktree" pull --ff-only origin "$BASE_BRANCH" >/dev/null 2>&1 || true
516
+ fi
517
+
518
+ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
519
+ if [[ "$source_worktree" == "$repo_root" ]]; then
520
+ if is_clean_worktree "$source_worktree"; then
521
+ switched_to_base=0
522
+ if git -C "$source_worktree" checkout "$BASE_BRANCH" >/dev/null 2>&1; then
523
+ switched_to_base=1
524
+ else
525
+ git -C "$source_worktree" checkout --detach >/dev/null 2>&1 || true
526
+ fi
527
+ if [[ "$switched_to_base" -eq 1 && "$PUSH_ENABLED" -eq 1 ]] && git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
528
+ git -C "$source_worktree" pull --ff-only origin "$BASE_BRANCH" >/dev/null 2>&1 || true
529
+ fi
355
530
  fi
531
+ elif [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${agent_worktree_root}"/* ]]; then
532
+ git -C "$source_worktree" checkout --detach >/dev/null 2>&1 || true
356
533
  fi
357
- elif [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
358
- git -C "$source_worktree" checkout --detach >/dev/null 2>&1 || true
359
- fi
360
534
 
361
- if [[ "$source_worktree" != "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
362
- git -C "$repo_root" worktree remove "$source_worktree" --force >/dev/null 2>&1 || true
363
- fi
535
+ if [[ "$source_worktree" != "$current_worktree" && "$source_worktree" == "${agent_worktree_root}"/* ]]; then
536
+ git -C "$repo_root" worktree remove "$source_worktree" --force >/dev/null 2>&1 || true
537
+ fi
364
538
 
365
- git -C "$repo_root" branch -d "$SOURCE_BRANCH"
539
+ git -C "$repo_root" branch -d "$SOURCE_BRANCH"
366
540
 
367
- if [[ "$PUSH_ENABLED" -eq 1 && "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
368
- if git -C "$repo_root" ls-remote --exit-code --heads origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
369
- git -C "$repo_root" push origin --delete "$SOURCE_BRANCH"
541
+ if [[ "$PUSH_ENABLED" -eq 1 && "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
542
+ if git -C "$repo_root" ls-remote --exit-code --heads origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
543
+ git -C "$repo_root" push origin --delete "$SOURCE_BRANCH"
544
+ fi
370
545
  fi
371
- fi
372
546
 
373
- base_worktree="$(get_worktree_for_branch "$BASE_BRANCH")"
374
- if [[ -n "$base_worktree" ]] && is_clean_worktree "$base_worktree" && [[ "$PUSH_ENABLED" -eq 1 ]]; then
375
- git -C "$base_worktree" pull --ff-only origin "$BASE_BRANCH" >/dev/null 2>&1 || true
376
- fi
547
+ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
548
+ prune_args=(--base "$BASE_BRANCH" --delete-branches)
549
+ if [[ "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
550
+ prune_args+=(--delete-remote-branches)
551
+ fi
552
+ if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
553
+ echo "[agent-branch-finish] Warning: automatic worktree prune failed." >&2
554
+ echo "[agent-branch-finish] You can run manual cleanup: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches" >&2
555
+ fi
556
+ fi
377
557
 
378
- if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
379
- if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" --base "$BASE_BRANCH"; then
380
- echo "[agent-branch-finish] Warning: automatic worktree prune failed." >&2
381
- echo "[agent-branch-finish] You can run manual cleanup: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH}" >&2
558
+ echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and cleaned source branch/worktree."
559
+ if [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${agent_worktree_root}"/* ]]; then
560
+ echo "[agent-branch-finish] Current worktree '${source_worktree}' still exists because it is the active shell cwd." >&2
561
+ echo "[agent-branch-finish] Leave this directory, then run: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches" >&2
562
+ fi
563
+ else
564
+ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
565
+ if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" --base "$BASE_BRANCH"; then
566
+ echo "[agent-branch-finish] Warning: temporary worktree prune failed." >&2
567
+ fi
382
568
  fi
383
- fi
384
569
 
385
- echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and removed branch."
386
- if [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
387
- echo "[agent-branch-finish] Current worktree '${source_worktree}' still exists because it is the active shell cwd." >&2
388
- echo "[agent-branch-finish] Leave this directory, then run: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH}" >&2
570
+ echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and kept source branch/worktree."
571
+ echo "[agent-branch-finish] Cleanup later with: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches --delete-remote-branches"
389
572
  fi
@@ -152,6 +152,36 @@ is_protected_branch_name() {
152
152
  return 1
153
153
  }
154
154
 
155
+ hydrate_local_helper_in_worktree() {
156
+ local repo="$1"
157
+ local worktree="$2"
158
+ local relative_path="$3"
159
+ local worktree_target="${worktree}/${relative_path}"
160
+ local source_path=""
161
+
162
+ if [[ -e "$worktree_target" ]]; then
163
+ return 0
164
+ fi
165
+
166
+ if [[ -f "${repo}/${relative_path}" ]]; then
167
+ source_path="${repo}/${relative_path}"
168
+ elif [[ -f "${repo}/templates/${relative_path}" ]]; then
169
+ source_path="${repo}/templates/${relative_path}"
170
+ fi
171
+
172
+ if [[ -z "$source_path" ]]; then
173
+ return 0
174
+ fi
175
+
176
+ mkdir -p "$(dirname "$worktree_target")"
177
+ cp "$source_path" "$worktree_target"
178
+ if [[ -x "$source_path" ]]; then
179
+ chmod +x "$worktree_target"
180
+ fi
181
+
182
+ echo "[agent-branch-start] Hydrated local helper in worktree: ${relative_path}"
183
+ }
184
+
155
185
  if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
156
186
  echo "[agent-branch-start] Not inside a git repository." >&2
157
187
  exit 1
@@ -195,15 +225,17 @@ snapshot_name="$(resolve_active_codex_snapshot_name)"
195
225
  snapshot_slug="$(sanitize_slug "$snapshot_name" "")"
196
226
  timestamp="$(date +%Y%m%d-%H%M%S)"
197
227
  if [[ -n "$snapshot_slug" ]]; then
198
- branch_name="agent/${agent_slug}/${timestamp}-${snapshot_slug}-${task_slug}"
228
+ branch_name_base="agent/${agent_slug}/${snapshot_slug}-${task_slug}"
199
229
  else
200
- branch_name="agent/${agent_slug}/${timestamp}-${task_slug}"
230
+ branch_name_base="agent/${agent_slug}/${task_slug}"
201
231
  fi
202
232
 
203
- if git show-ref --verify --quiet "refs/heads/${branch_name}"; then
204
- echo "[agent-branch-start] Branch already exists: ${branch_name}" >&2
205
- exit 1
206
- fi
233
+ branch_name="$branch_name_base"
234
+ branch_suffix=2
235
+ while git show-ref --verify --quiet "refs/heads/${branch_name}"; do
236
+ branch_name="${branch_name_base}-${branch_suffix}"
237
+ branch_suffix=$((branch_suffix + 1))
238
+ done
207
239
 
208
240
  if [[ "$WORKTREE_MODE" -eq 0 ]]; then
209
241
  if [[ "$ALLOW_IN_PLACE" -ne 1 ]]; then
@@ -280,6 +312,8 @@ if [[ -n "$auto_transfer_stash_ref" ]]; then
280
312
  fi
281
313
  fi
282
314
 
315
+ hydrate_local_helper_in_worktree "$repo_root" "$worktree_path" "scripts/codex-agent.sh"
316
+
283
317
  echo "[agent-branch-start] Created branch: ${branch_name}"
284
318
  echo "[agent-branch-start] Worktree: ${worktree_path}"
285
319
  echo "[agent-branch-start] Next steps:"
@@ -5,6 +5,9 @@ BASE_BRANCH="${MUSAFETY_BASE_BRANCH:-}"
5
5
  BASE_BRANCH_EXPLICIT=0
6
6
  DRY_RUN=0
7
7
  FORCE_DIRTY=0
8
+ DELETE_BRANCHES=0
9
+ DELETE_REMOTE_BRANCHES=0
10
+ TARGET_BRANCH=""
8
11
 
9
12
  if [[ -n "$BASE_BRANCH" ]]; then
10
13
  BASE_BRANCH_EXPLICIT=1
@@ -25,9 +28,21 @@ while [[ $# -gt 0 ]]; do
25
28
  FORCE_DIRTY=1
26
29
  shift
27
30
  ;;
31
+ --delete-branches)
32
+ DELETE_BRANCHES=1
33
+ shift
34
+ ;;
35
+ --delete-remote-branches)
36
+ DELETE_REMOTE_BRANCHES=1
37
+ shift
38
+ ;;
39
+ --branch)
40
+ TARGET_BRANCH="${2:-}"
41
+ shift 2
42
+ ;;
28
43
  *)
29
44
  echo "[agent-worktree-prune] Unknown argument: $1" >&2
30
- echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty]" >&2
45
+ echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--branch <agent/...>]" >&2
31
46
  exit 1
32
47
  ;;
33
48
  esac
@@ -73,6 +88,11 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
73
88
  exit 1
74
89
  fi
75
90
 
91
+ if [[ -n "$TARGET_BRANCH" && "$TARGET_BRANCH" != agent/* ]]; then
92
+ echo "[agent-worktree-prune] --branch must reference an agent/* branch: ${TARGET_BRANCH}" >&2
93
+ exit 1
94
+ fi
95
+
76
96
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
77
97
  BASE_BRANCH="$(resolve_base_branch)"
78
98
  fi
@@ -124,6 +144,10 @@ process_entry() {
124
144
  branch="${branch_ref#refs/heads/}"
125
145
  fi
126
146
 
147
+ if [[ -n "$TARGET_BRANCH" && "$branch" != "$TARGET_BRANCH" ]]; then
148
+ return
149
+ fi
150
+
127
151
  if [[ "$wt" == "$current_pwd" ]]; then
128
152
  skipped_active=$((skipped_active + 1))
129
153
  echo "[agent-worktree-prune] Skipping active cwd worktree: ${wt}"
@@ -138,7 +162,9 @@ process_entry() {
138
162
  remove_reason="missing-branch"
139
163
  elif [[ "$branch" == agent/* ]]; then
140
164
  if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
141
- remove_reason="merged-agent-branch"
165
+ if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
166
+ remove_reason="merged-agent-branch"
167
+ fi
142
168
  fi
143
169
  elif [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]; then
144
170
  remove_reason="temporary-worktree"
@@ -163,10 +189,16 @@ process_entry() {
163
189
  fi
164
190
 
165
191
  if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}" && ! branch_has_worktree "$branch"; then
166
- if [[ "$branch" == agent/* ]]; then
192
+ if [[ "$branch" == agent/* && "$DELETE_BRANCHES" -eq 1 ]]; then
167
193
  if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
168
194
  removed_branches=$((removed_branches + 1))
169
195
  echo "[agent-worktree-prune] Deleted merged branch: ${branch}"
196
+ if [[ "$DELETE_REMOTE_BRANCHES" -eq 1 ]]; then
197
+ if git -C "$repo_root" ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
198
+ run_cmd git -C "$repo_root" push origin --delete "$branch" >/dev/null 2>&1 || true
199
+ echo "[agent-worktree-prune] Deleted merged remote branch: ${branch}"
200
+ fi
201
+ fi
170
202
  fi
171
203
  elif [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]; then
172
204
  run_cmd git -C "$repo_root" branch -D "$branch" >/dev/null 2>&1 || true
@@ -199,18 +231,29 @@ done < <(git -C "$repo_root" worktree list --porcelain)
199
231
 
200
232
  process_entry "$current_wt" "$current_branch_ref"
201
233
 
202
- while IFS= read -r branch; do
203
- [[ -z "$branch" ]] && continue
204
- if branch_has_worktree "$branch"; then
205
- continue
206
- fi
207
- if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
208
- if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
209
- removed_branches=$((removed_branches + 1))
210
- echo "[agent-worktree-prune] Deleted stale merged branch: ${branch}"
234
+ if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
235
+ while IFS= read -r branch; do
236
+ [[ -z "$branch" ]] && continue
237
+ if [[ -n "$TARGET_BRANCH" && "$branch" != "$TARGET_BRANCH" ]]; then
238
+ continue
211
239
  fi
212
- fi
213
- done < <(git -C "$repo_root" for-each-ref --format='%(refname:short)' refs/heads/agent)
240
+ if branch_has_worktree "$branch"; then
241
+ continue
242
+ fi
243
+ if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
244
+ if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
245
+ removed_branches=$((removed_branches + 1))
246
+ echo "[agent-worktree-prune] Deleted stale merged branch: ${branch}"
247
+ if [[ "$DELETE_REMOTE_BRANCHES" -eq 1 ]]; then
248
+ if git -C "$repo_root" ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
249
+ run_cmd git -C "$repo_root" push origin --delete "$branch" >/dev/null 2>&1 || true
250
+ echo "[agent-worktree-prune] Deleted stale merged remote branch: ${branch}"
251
+ fi
252
+ fi
253
+ fi
254
+ fi
255
+ done < <(git -C "$repo_root" for-each-ref --format='%(refname:short)' refs/heads/agent)
256
+ fi
214
257
 
215
258
  run_cmd git -C "$repo_root" worktree prune
216
259