@imdeadpool/guardex 5.0.0 → 5.0.2

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.
@@ -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,26 @@ 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
+
14
+ normalize_bool() {
15
+ local raw="${1:-}"
16
+ local fallback="${2:-0}"
17
+ local lowered
18
+ lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
19
+ case "$lowered" in
20
+ 1|true|yes|on) printf '1' ;;
21
+ 0|false|no|off) printf '0' ;;
22
+ '') printf '%s' "$fallback" ;;
23
+ *) printf '%s' "$fallback" ;;
24
+ esac
25
+ }
26
+
27
+ CLEANUP_AFTER_MERGE="$(normalize_bool "$CLEANUP_AFTER_MERGE_RAW" "0")"
11
28
 
12
29
  while [[ $# -gt 0 ]]; do
13
30
  case "$1" in
@@ -26,6 +43,20 @@ while [[ $# -gt 0 ]]; do
26
43
  ;;
27
44
  --keep-remote-branch)
28
45
  DELETE_REMOTE_BRANCH=0
46
+ DELETE_REMOTE_BRANCH_EXPLICIT=1
47
+ shift
48
+ ;;
49
+ --delete-remote-branch)
50
+ DELETE_REMOTE_BRANCH=1
51
+ DELETE_REMOTE_BRANCH_EXPLICIT=1
52
+ shift
53
+ ;;
54
+ --cleanup)
55
+ CLEANUP_AFTER_MERGE=1
56
+ shift
57
+ ;;
58
+ --no-cleanup)
59
+ CLEANUP_AFTER_MERGE=0
29
60
  shift
30
61
  ;;
31
62
  --mode)
@@ -42,12 +73,16 @@ while [[ $# -gt 0 ]]; do
42
73
  ;;
43
74
  *)
44
75
  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
76
+ echo "Usage: $0 [--base <branch>] [--branch <branch>] [--no-push] [--cleanup|--no-cleanup] [--keep-remote-branch|--delete-remote-branch] [--mode auto|direct|pr|--via-pr|--direct-only]" >&2
46
77
  exit 1
47
78
  ;;
48
79
  esac
49
80
  done
50
81
 
82
+ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 && "$DELETE_REMOTE_BRANCH_EXPLICIT" -eq 0 ]]; then
83
+ DELETE_REMOTE_BRANCH=1
84
+ fi
85
+
51
86
  case "$MERGE_MODE" in
52
87
  auto|direct|pr) ;;
53
88
  *)
@@ -347,43 +382,58 @@ if [[ -x "${repo_root}/scripts/agent-file-locks.py" ]]; then
347
382
  python3 "${repo_root}/scripts/agent-file-locks.py" release --branch "$SOURCE_BRANCH" >/dev/null 2>&1 || true
348
383
  fi
349
384
 
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
385
+ base_worktree="$(get_worktree_for_branch "$BASE_BRANCH")"
386
+ if [[ -n "$base_worktree" ]] && is_clean_worktree "$base_worktree" && [[ "$PUSH_ENABLED" -eq 1 ]]; then
387
+ git -C "$base_worktree" pull --ff-only origin "$BASE_BRANCH" >/dev/null 2>&1 || true
388
+ fi
389
+
390
+ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
391
+ if [[ "$source_worktree" == "$repo_root" ]]; then
392
+ if is_clean_worktree "$source_worktree"; then
393
+ git -C "$source_worktree" checkout "$BASE_BRANCH" >/dev/null 2>&1 || true
394
+ if [[ "$PUSH_ENABLED" -eq 1 ]] && git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
395
+ git -C "$source_worktree" pull --ff-only origin "$BASE_BRANCH" >/dev/null 2>&1 || true
396
+ fi
355
397
  fi
398
+ elif [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
399
+ git -C "$source_worktree" checkout --detach >/dev/null 2>&1 || true
356
400
  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
401
 
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
402
+ if [[ "$source_worktree" != "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
403
+ git -C "$repo_root" worktree remove "$source_worktree" --force >/dev/null 2>&1 || true
404
+ fi
364
405
 
365
- git -C "$repo_root" branch -d "$SOURCE_BRANCH"
406
+ git -C "$repo_root" branch -d "$SOURCE_BRANCH"
366
407
 
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"
408
+ if [[ "$PUSH_ENABLED" -eq 1 && "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
409
+ if git -C "$repo_root" ls-remote --exit-code --heads origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
410
+ git -C "$repo_root" push origin --delete "$SOURCE_BRANCH"
411
+ fi
370
412
  fi
371
- fi
372
413
 
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
414
+ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
415
+ prune_args=(--base "$BASE_BRANCH" --delete-branches)
416
+ if [[ "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
417
+ prune_args+=(--delete-remote-branches)
418
+ fi
419
+ if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
420
+ echo "[agent-branch-finish] Warning: automatic worktree prune failed." >&2
421
+ echo "[agent-branch-finish] You can run manual cleanup: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches" >&2
422
+ fi
423
+ fi
377
424
 
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
425
+ echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and cleaned source branch/worktree."
426
+ if [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${repo_root}/.omx/agent-worktrees"/* ]]; then
427
+ echo "[agent-branch-finish] Current worktree '${source_worktree}' still exists because it is the active shell cwd." >&2
428
+ echo "[agent-branch-finish] Leave this directory, then run: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches" >&2
429
+ fi
430
+ else
431
+ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
432
+ if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" --base "$BASE_BRANCH"; then
433
+ echo "[agent-branch-finish] Warning: temporary worktree prune failed." >&2
434
+ fi
382
435
  fi
383
- fi
384
436
 
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
437
+ echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and kept source branch/worktree."
438
+ echo "[agent-branch-finish] Cleanup later with: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches --delete-remote-branches"
389
439
  fi
@@ -1,22 +1,48 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- BASE_BRANCH="dev"
4
+ BASE_BRANCH="${MUSAFETY_BASE_BRANCH:-}"
5
+ BASE_BRANCH_EXPLICIT=0
5
6
  DRY_RUN=0
7
+ FORCE_DIRTY=0
8
+ DELETE_BRANCHES=0
9
+ DELETE_REMOTE_BRANCHES=0
10
+ TARGET_BRANCH=""
11
+
12
+ if [[ -n "$BASE_BRANCH" ]]; then
13
+ BASE_BRANCH_EXPLICIT=1
14
+ fi
6
15
 
7
16
  while [[ $# -gt 0 ]]; do
8
17
  case "$1" in
9
18
  --base)
10
- BASE_BRANCH="${2:-dev}"
19
+ BASE_BRANCH="${2:-}"
20
+ BASE_BRANCH_EXPLICIT=1
11
21
  shift 2
12
22
  ;;
13
23
  --dry-run)
14
24
  DRY_RUN=1
15
25
  shift
16
26
  ;;
27
+ --force-dirty)
28
+ FORCE_DIRTY=1
29
+ shift
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
+ ;;
17
43
  *)
18
44
  echo "[agent-worktree-prune] Unknown argument: $1" >&2
19
- echo "Usage: $0 [--base <branch>] [--dry-run]" >&2
45
+ echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--branch <agent/...>]" >&2
20
46
  exit 1
21
47
  ;;
22
48
  esac
@@ -31,6 +57,51 @@ repo_root="$(git rev-parse --show-toplevel)"
31
57
  current_pwd="$(pwd -P)"
32
58
  worktree_root="${repo_root}/.omx/agent-worktrees"
33
59
 
60
+ resolve_base_branch() {
61
+ local configured=""
62
+ local current=""
63
+
64
+ configured="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
65
+ if [[ -n "$configured" ]] && git -C "$repo_root" show-ref --verify --quiet "refs/heads/${configured}"; then
66
+ printf '%s' "$configured"
67
+ return 0
68
+ fi
69
+
70
+ current="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
71
+ if [[ -n "$current" && "$current" != "HEAD" ]] && git -C "$repo_root" show-ref --verify --quiet "refs/heads/${current}"; then
72
+ printf '%s' "$current"
73
+ return 0
74
+ fi
75
+
76
+ for fallback in main dev; do
77
+ if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${fallback}"; then
78
+ printf '%s' "$fallback"
79
+ return 0
80
+ fi
81
+ done
82
+
83
+ printf '%s' ""
84
+ }
85
+
86
+ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
87
+ echo "[agent-worktree-prune] --base requires a non-empty branch name." >&2
88
+ exit 1
89
+ fi
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
+
96
+ if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
97
+ BASE_BRANCH="$(resolve_base_branch)"
98
+ fi
99
+
100
+ if [[ -z "$BASE_BRANCH" ]]; then
101
+ echo "[agent-worktree-prune] Unable to infer base branch. Pass --base <branch>." >&2
102
+ exit 1
103
+ fi
104
+
34
105
  if ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${BASE_BRANCH}"; then
35
106
  echo "[agent-worktree-prune] Base branch not found: ${BASE_BRANCH}" >&2
36
107
  exit 1
@@ -49,9 +120,17 @@ branch_has_worktree() {
49
120
  git -C "$repo_root" worktree list --porcelain | grep -q "^branch refs/heads/${branch}$"
50
121
  }
51
122
 
123
+ is_clean_worktree() {
124
+ local wt="$1"
125
+ git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
126
+ && git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
127
+ && [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
128
+ }
129
+
52
130
  removed_worktrees=0
53
131
  removed_branches=0
54
132
  skipped_active=0
133
+ skipped_dirty=0
55
134
 
56
135
  process_entry() {
57
136
  local wt="$1"
@@ -65,6 +144,10 @@ process_entry() {
65
144
  branch="${branch_ref#refs/heads/}"
66
145
  fi
67
146
 
147
+ if [[ -n "$TARGET_BRANCH" && "$branch" != "$TARGET_BRANCH" ]]; then
148
+ return
149
+ fi
150
+
68
151
  if [[ "$wt" == "$current_pwd" ]]; then
69
152
  skipped_active=$((skipped_active + 1))
70
153
  echo "[agent-worktree-prune] Skipping active cwd worktree: ${wt}"
@@ -79,7 +162,9 @@ process_entry() {
79
162
  remove_reason="missing-branch"
80
163
  elif [[ "$branch" == agent/* ]]; then
81
164
  if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
82
- remove_reason="merged-agent-branch"
165
+ if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
166
+ remove_reason="merged-agent-branch"
167
+ fi
83
168
  fi
84
169
  elif [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]; then
85
170
  remove_reason="temporary-worktree"
@@ -89,6 +174,12 @@ process_entry() {
89
174
  return
90
175
  fi
91
176
 
177
+ if [[ "$FORCE_DIRTY" -ne 1 ]] && ! is_clean_worktree "$wt"; then
178
+ skipped_dirty=$((skipped_dirty + 1))
179
+ echo "[agent-worktree-prune] Skipping dirty worktree (${remove_reason}): ${wt}"
180
+ return
181
+ fi
182
+
92
183
  echo "[agent-worktree-prune] Removing worktree (${remove_reason}): ${wt}"
93
184
  run_cmd git -C "$repo_root" worktree remove "$wt" --force
94
185
  removed_worktrees=$((removed_worktrees + 1))
@@ -98,10 +189,16 @@ process_entry() {
98
189
  fi
99
190
 
100
191
  if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}" && ! branch_has_worktree "$branch"; then
101
- if [[ "$branch" == agent/* ]]; then
192
+ if [[ "$branch" == agent/* && "$DELETE_BRANCHES" -eq 1 ]]; then
102
193
  if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
103
194
  removed_branches=$((removed_branches + 1))
104
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
105
202
  fi
106
203
  elif [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]; then
107
204
  run_cmd git -C "$repo_root" branch -D "$branch" >/dev/null 2>&1 || true
@@ -134,22 +231,36 @@ done < <(git -C "$repo_root" worktree list --porcelain)
134
231
 
135
232
  process_entry "$current_wt" "$current_branch_ref"
136
233
 
137
- while IFS= read -r branch; do
138
- [[ -z "$branch" ]] && continue
139
- if branch_has_worktree "$branch"; then
140
- continue
141
- fi
142
- if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
143
- if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
144
- removed_branches=$((removed_branches + 1))
145
- 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
146
239
  fi
147
- fi
148
- 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
149
257
 
150
258
  run_cmd git -C "$repo_root" worktree prune
151
259
 
152
- echo "[agent-worktree-prune] Summary: removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}"
260
+ echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}"
153
261
  if [[ "$skipped_active" -gt 0 ]]; then
154
262
  echo "[agent-worktree-prune] Tip: leave active agent worktree directories, then run this command again for full cleanup." >&2
155
263
  fi
264
+ if [[ "$skipped_dirty" -gt 0 ]]; then
265
+ echo "[agent-worktree-prune] Tip: dirty worktrees were preserved. Clean/finish them first, or pass --force-dirty to remove anyway." >&2
266
+ fi