@imdeadpool/guardex 7.0.39 → 7.0.43

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.
Files changed (85) hide show
  1. package/README.md +85 -16
  2. package/package.json +2 -1
  3. package/skills/gitguardex/SKILL.md +13 -0
  4. package/skills/guardex-merge-skills-to-dev/SKILL.md +59 -0
  5. package/src/agents/cleanup-sessions.js +126 -0
  6. package/src/agents/detect.js +160 -0
  7. package/src/agents/finish.js +172 -0
  8. package/src/agents/inspect.js +189 -0
  9. package/src/agents/launch.js +240 -0
  10. package/src/agents/registry.js +133 -0
  11. package/src/agents/selection-panel.js +571 -0
  12. package/src/agents/sessions.js +151 -0
  13. package/src/agents/start.js +591 -0
  14. package/src/agents/status.js +143 -0
  15. package/src/agents/terminal.js +152 -0
  16. package/src/budget/index.js +343 -0
  17. package/src/ci-init/index.js +265 -0
  18. package/src/cli/args.js +305 -1
  19. package/src/cli/main.js +262 -132
  20. package/src/cockpit/action-runner.js +3 -0
  21. package/src/cockpit/actions.js +80 -0
  22. package/src/cockpit/control.js +1121 -0
  23. package/src/cockpit/index.js +426 -0
  24. package/src/cockpit/keybindings.js +224 -0
  25. package/src/cockpit/kitty-layout.js +549 -0
  26. package/src/cockpit/kitty-tree.js +144 -0
  27. package/src/cockpit/layout.js +224 -0
  28. package/src/cockpit/logs-reader.js +182 -0
  29. package/src/cockpit/menu.js +204 -0
  30. package/src/cockpit/pane-actions.js +597 -0
  31. package/src/cockpit/pane-menu.js +387 -0
  32. package/src/cockpit/projects-finder.js +178 -0
  33. package/src/cockpit/render.js +215 -0
  34. package/src/cockpit/settings-render.js +128 -0
  35. package/src/cockpit/settings.js +124 -0
  36. package/src/cockpit/shortcuts.js +24 -0
  37. package/src/cockpit/sidebar.js +311 -0
  38. package/src/cockpit/state.js +72 -0
  39. package/src/cockpit/theme.js +128 -0
  40. package/src/cockpit/welcome.js +266 -0
  41. package/src/context.js +78 -35
  42. package/src/doctor/index.js +4 -3
  43. package/src/finish/index.js +39 -2
  44. package/src/git/index.js +65 -0
  45. package/src/kitty/command.js +101 -0
  46. package/src/kitty/runtime.js +250 -0
  47. package/src/output/index.js +1 -1
  48. package/src/pr-review.js +241 -0
  49. package/src/scaffold/index.js +19 -0
  50. package/src/submodule/index.js +288 -0
  51. package/src/terminal/index.js +120 -0
  52. package/src/terminal/kitty.js +622 -0
  53. package/src/terminal/tmux.js +126 -0
  54. package/src/tmux/command.js +27 -0
  55. package/src/tmux/session.js +89 -0
  56. package/templates/AGENTS.multiagent-safety.md +421 -37
  57. package/templates/codex/skills/gitguardex/SKILL.md +2 -0
  58. package/templates/githooks/pre-commit +22 -1
  59. package/templates/github/workflows/README.md +87 -0
  60. package/templates/github/workflows/ci-full.yml +55 -0
  61. package/templates/github/workflows/ci.yml +56 -0
  62. package/templates/github/workflows/cr.yml +20 -1
  63. package/templates/scripts/agent-branch-finish.sh +545 -27
  64. package/templates/scripts/agent-branch-start.sh +193 -21
  65. package/templates/scripts/agent-preflight.sh +89 -0
  66. package/templates/scripts/agent-worktree-prune.sh +96 -5
  67. package/templates/scripts/codex-agent.sh +41 -6
  68. package/templates/scripts/openspec/init-plan-workspace.sh +43 -0
  69. package/templates/scripts/review-bot-watch.sh +31 -2
  70. package/templates/scripts/agent-session-state.js +0 -171
  71. package/templates/scripts/install-vscode-active-agents-extension.js +0 -135
  72. package/templates/vscode/guardex-active-agents/README.md +0 -34
  73. package/templates/vscode/guardex-active-agents/extension.js +0 -3782
  74. package/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +0 -54
  75. package/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +0 -5
  76. package/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +0 -7
  77. package/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +0 -4
  78. package/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +0 -4
  79. package/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +0 -5
  80. package/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +0 -4
  81. package/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +0 -5
  82. package/templates/vscode/guardex-active-agents/icon.png +0 -0
  83. package/templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg +0 -14
  84. package/templates/vscode/guardex-active-agents/package.json +0 -169
  85. package/templates/vscode/guardex-active-agents/session-schema.js +0 -1348
@@ -11,11 +11,22 @@ MERGE_MODE="auto"
11
11
  GH_BIN="${GUARDEX_GH_BIN:-gh}"
12
12
  NODE_BIN="${GUARDEX_NODE_BIN:-node}"
13
13
  CLI_ENTRY="${GUARDEX_CLI_ENTRY:-}"
14
- CLEANUP_AFTER_MERGE_RAW="${GUARDEX_FINISH_CLEANUP:-false}"
15
- WAIT_FOR_MERGE_RAW="${GUARDEX_FINISH_WAIT_FOR_MERGE:-false}"
14
+ CLEANUP_AFTER_MERGE_RAW="${GUARDEX_FINISH_CLEANUP:-true}"
15
+ WAIT_FOR_MERGE_RAW="${GUARDEX_FINISH_WAIT_FOR_MERGE:-true}"
16
16
  WAIT_TIMEOUT_SECONDS_RAW="${GUARDEX_FINISH_WAIT_TIMEOUT_SECONDS:-1800}"
17
17
  WAIT_POLL_SECONDS_RAW="${GUARDEX_FINISH_WAIT_POLL_SECONDS:-10}"
18
18
  PARENT_GITLINK_AUTO_COMMIT_RAW="${GUARDEX_FINISH_PARENT_GITLINK_AUTO_COMMIT:-true}"
19
+ AUTO_RESOLVE_MODE_RAW="${GUARDEX_FINISH_AUTO_RESOLVE:-none}"
20
+ AUTO_RESOLVE_SAFE_GLOBS_DEFAULT='.omc/**:.omx/state/**:.dev-ports.json:apps/logs/**:.agents/settings.local.json:.codex/state/**:.claude/state/**'
21
+ AUTO_RESOLVE_SAFE_GLOBS_RAW="${GUARDEX_FINISH_AUTO_RESOLVE_SAFE_GLOBS-$AUTO_RESOLVE_SAFE_GLOBS_DEFAULT}"
22
+ PREFLIGHT_ENABLED_RAW="${GUARDEX_FINISH_PREFLIGHT:-true}"
23
+ PREFLIGHT_SCRIPT_RAW="${GUARDEX_FINISH_PREFLIGHT_SCRIPT:-scripts/agent-preflight.sh}"
24
+ AUTO_PROMOTE_DRAFT_RAW="${GUARDEX_FINISH_AUTO_PROMOTE:-true}"
25
+ # --skip-checks (or GUARDEX_FINISH_SKIP_CHECKS=1): append `--admin` to every
26
+ # `gh pr merge` invocation, bypassing required status checks. Requires admin
27
+ # permission on the target repo. Use when CI is wedged (e.g. runner billing,
28
+ # infrastructure outage) and the operator has accepted the risk.
29
+ SKIP_CHECKS_RAW="${GUARDEX_FINISH_SKIP_CHECKS:-false}"
19
30
 
20
31
  run_guardex_cli() {
21
32
  if [[ -n "$CLI_ENTRY" ]]; then
@@ -64,11 +75,84 @@ normalize_int() {
64
75
  printf '%s' "$value"
65
76
  }
66
77
 
67
- CLEANUP_AFTER_MERGE="$(normalize_bool "$CLEANUP_AFTER_MERGE_RAW" "0")"
68
- WAIT_FOR_MERGE="$(normalize_bool "$WAIT_FOR_MERGE_RAW" "0")"
78
+ # Resolve the pre-flight script path against the source worktree. The
79
+ # caller passes either the configured path (which may be relative) or
80
+ # an empty string; we return the absolute path if it exists and is
81
+ # executable, otherwise return empty.
82
+ resolve_preflight_script() {
83
+ local worktree="$1"
84
+ local configured="$2"
85
+ if [[ -z "$configured" ]]; then
86
+ configured="scripts/agent-preflight.sh"
87
+ fi
88
+ if [[ "$configured" = /* ]]; then
89
+ if [[ -x "$configured" ]]; then
90
+ printf '%s' "$configured"
91
+ fi
92
+ return 0
93
+ fi
94
+ local candidate="${worktree}/${configured}"
95
+ if [[ -x "$candidate" ]]; then
96
+ printf '%s' "$candidate"
97
+ fi
98
+ }
99
+
100
+ # Run the pre-flight verification gate in the agent worktree before
101
+ # any push happens. Returns 0 on success or when no gate is
102
+ # configured; returns non-zero (and prints a hint) on failure, which
103
+ # the caller propagates so the push is refused.
104
+ run_preflight() {
105
+ local worktree="$1"
106
+ if [[ "$PREFLIGHT_ENABLED" -ne 1 ]]; then
107
+ return 0
108
+ fi
109
+ local script_path
110
+ script_path="$(resolve_preflight_script "$worktree" "$PREFLIGHT_SCRIPT_RAW")"
111
+ if [[ -z "$script_path" ]]; then
112
+ echo "[agent-branch-finish] No executable pre-flight script at ${PREFLIGHT_SCRIPT_RAW} (in ${worktree}); skipping pre-flight." >&2
113
+ return 0
114
+ fi
115
+ echo "[agent-branch-finish] Running pre-flight: ${script_path}" >&2
116
+ if ( cd "$worktree" && "$script_path" ); then
117
+ echo "[agent-branch-finish] Pre-flight passed." >&2
118
+ return 0
119
+ fi
120
+ echo "[agent-branch-finish] Pre-flight FAILED; refusing push. Override with --no-preflight if you really mean it." >&2
121
+ return 1
122
+ }
123
+
124
+ # After a PR exists, if it is in draft and auto-promote is enabled,
125
+ # mark it ready-for-review. With the budget-friendly CI defaults
126
+ # (draft PRs skip CI), this is the moment when CI is allowed to fire.
127
+ maybe_auto_promote_pr() {
128
+ local pr_url="$1"
129
+ if [[ -z "$pr_url" ]] || [[ "$AUTO_PROMOTE_DRAFT" -ne 1 ]]; then
130
+ return 0
131
+ fi
132
+ if ! command -v "$GH_BIN" >/dev/null 2>&1; then
133
+ return 0
134
+ fi
135
+ local is_draft
136
+ is_draft="$("$GH_BIN" pr view "$pr_url" --json isDraft --jq '.isDraft' 2>/dev/null || true)"
137
+ if [[ "$is_draft" != "true" ]]; then
138
+ return 0
139
+ fi
140
+ echo "[agent-branch-finish] PR is draft; promoting to ready-for-review (pre-flight passed)." >&2
141
+ if "$GH_BIN" pr ready "$pr_url" >/dev/null 2>&1; then
142
+ echo "[agent-branch-finish] PR marked ready-for-review." >&2
143
+ else
144
+ echo "[agent-branch-finish] gh pr ready failed; PR left in draft. Promote manually if intended." >&2
145
+ fi
146
+ }
147
+
148
+ CLEANUP_AFTER_MERGE="$(normalize_bool "$CLEANUP_AFTER_MERGE_RAW" "1")"
149
+ WAIT_FOR_MERGE="$(normalize_bool "$WAIT_FOR_MERGE_RAW" "1")"
69
150
  WAIT_TIMEOUT_SECONDS="$(normalize_int "$WAIT_TIMEOUT_SECONDS_RAW" "1800" "30")"
70
151
  WAIT_POLL_SECONDS="$(normalize_int "$WAIT_POLL_SECONDS_RAW" "10" "0")"
71
152
  PARENT_GITLINK_AUTO_COMMIT="$(normalize_bool "$PARENT_GITLINK_AUTO_COMMIT_RAW" "1")"
153
+ PREFLIGHT_ENABLED="$(normalize_bool "$PREFLIGHT_ENABLED_RAW" "1")"
154
+ AUTO_PROMOTE_DRAFT="$(normalize_bool "$AUTO_PROMOTE_DRAFT_RAW" "1")"
155
+ SKIP_CHECKS="$(normalize_bool "$SKIP_CHECKS_RAW" "0")"
72
156
 
73
157
  while [[ $# -gt 0 ]]; do
74
158
  case "$1" in
@@ -139,9 +223,54 @@ while [[ $# -gt 0 ]]; do
139
223
  MERGE_MODE="direct"
140
224
  shift
141
225
  ;;
226
+ --auto-resolve)
227
+ if [[ "${2:-}" =~ ^(none|safe|full)$ ]]; then
228
+ AUTO_RESOLVE_MODE_RAW="$2"
229
+ shift 2
230
+ else
231
+ AUTO_RESOLVE_MODE_RAW="safe"
232
+ shift
233
+ fi
234
+ ;;
235
+ --auto-resolve=*)
236
+ AUTO_RESOLVE_MODE_RAW="${1#--auto-resolve=}"
237
+ shift
238
+ ;;
239
+ --no-auto-resolve)
240
+ AUTO_RESOLVE_MODE_RAW="none"
241
+ shift
242
+ ;;
243
+ --no-preflight)
244
+ PREFLIGHT_ENABLED_RAW="false"
245
+ shift
246
+ ;;
247
+ --preflight)
248
+ PREFLIGHT_ENABLED_RAW="true"
249
+ shift
250
+ ;;
251
+ --preflight-script)
252
+ PREFLIGHT_SCRIPT_RAW="${2:-}"
253
+ shift 2
254
+ ;;
255
+ --skip-checks)
256
+ SKIP_CHECKS=1
257
+ shift
258
+ ;;
259
+ --no-skip-checks)
260
+ SKIP_CHECKS=0
261
+ shift
262
+ ;;
263
+ --no-auto-promote)
264
+ AUTO_PROMOTE_DRAFT_RAW="false"
265
+ shift
266
+ ;;
267
+ --auto-promote)
268
+ AUTO_PROMOTE_DRAFT_RAW="true"
269
+ shift
270
+ ;;
142
271
  *)
143
272
  echo "[agent-branch-finish] Unknown argument: $1" >&2
144
- 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>] [--parent-gitlink-commit|--no-parent-gitlink-commit] [--keep-remote-branch|--delete-remote-branch] [--mode auto|direct|pr|--via-pr|--direct-only]" >&2
273
+ 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>] [--parent-gitlink-commit|--no-parent-gitlink-commit] [--keep-remote-branch|--delete-remote-branch] [--mode auto|direct|pr|--via-pr|--direct-only] [--auto-resolve[=none|safe|full]|--no-auto-resolve] [--no-preflight|--preflight] [--preflight-script <path>] [--no-auto-promote|--auto-promote]" >&2
145
274
  exit 1
146
275
  ;;
147
276
  esac
@@ -159,12 +288,162 @@ case "$MERGE_MODE" in
159
288
  ;;
160
289
  esac
161
290
 
291
+ AUTO_RESOLVE_MODE="$(printf '%s' "$AUTO_RESOLVE_MODE_RAW" | tr '[:upper:]' '[:lower:]')"
292
+ case "$AUTO_RESOLVE_MODE" in
293
+ none|safe|full) ;;
294
+ *)
295
+ echo "[agent-branch-finish] Invalid --auto-resolve value: ${AUTO_RESOLVE_MODE_RAW} (expected none|safe|full)" >&2
296
+ exit 1
297
+ ;;
298
+ esac
299
+
300
+ path_matches_auto_resolve_safe_glob() {
301
+ local path="$1"
302
+ if [[ -z "${AUTO_RESOLVE_SAFE_GLOBS_RAW:-}" ]]; then
303
+ return 1
304
+ fi
305
+ local -a globs=()
306
+ IFS=':' read -ra globs <<< "$AUTO_RESOLVE_SAFE_GLOBS_RAW"
307
+ local pattern rewritten
308
+ for pattern in "${globs[@]}"; do
309
+ [[ -z "$pattern" ]] && continue
310
+ rewritten="${pattern%/**}"
311
+ if [[ "$rewritten" != "$pattern" ]]; then
312
+ if [[ "$path" == "$rewritten"/* ]]; then
313
+ return 0
314
+ fi
315
+ else
316
+ # shellcheck disable=SC2053
317
+ if [[ "$path" == $pattern ]]; then
318
+ return 0
319
+ fi
320
+ fi
321
+ done
322
+ return 1
323
+ }
324
+
325
+ # Resolve a conflicting submodule pointer if and only if one side is a strict
326
+ # ancestor of the other (fast-forward direction). Writes the resolved SHA via
327
+ # git update-index and prints the chosen SHA on stdout. Returns 0 on success,
328
+ # 1 on uninitialized/divergent/unreachable cases.
329
+ try_resolve_submodule_pointer_conflict() {
330
+ local repo_root_arg="$1"
331
+ local source_worktree_arg="$2"
332
+ local conflict_path="$3"
333
+
334
+ # Confirm registered submodule path.
335
+ if [[ ! -f "$repo_root_arg/.gitmodules" ]]; then
336
+ return 1
337
+ fi
338
+ if ! git -C "$repo_root_arg" config -f .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null \
339
+ | awk '{print $2}' | grep -Fxq -- "$conflict_path"; then
340
+ return 1
341
+ fi
342
+
343
+ # Read the three stages from the index.
344
+ local stage_out
345
+ stage_out="$(git -C "$source_worktree_arg" ls-files -u -- "$conflict_path" 2>/dev/null || true)"
346
+ if [[ -z "$stage_out" ]]; then
347
+ return 1
348
+ fi
349
+
350
+ local base_sha="" ours_sha="" theirs_sha=""
351
+ local mode_field stage_sha stage_num path_field
352
+ while IFS=$'\t' read -r meta path_field; do
353
+ [[ -z "$meta" || -z "$path_field" ]] && continue
354
+ # meta format: "<mode> <sha> <stage>"
355
+ read -r mode_field stage_sha stage_num <<< "$meta"
356
+ [[ "$mode_field" != "160000" ]] && return 1
357
+ case "$stage_num" in
358
+ 1) base_sha="$stage_sha" ;;
359
+ 2) ours_sha="$stage_sha" ;;
360
+ 3) theirs_sha="$stage_sha" ;;
361
+ esac
362
+ done <<< "$stage_out"
363
+
364
+ if [[ -z "$ours_sha" || -z "$theirs_sha" ]]; then
365
+ return 1
366
+ fi
367
+
368
+ # Pick a working clone for the submodule. Three sources, in order:
369
+ # 1) checked-out submodule worktree (cheap, no network)
370
+ # 2) cached internal clone at .git/modules/<name>
371
+ # 3) temp bare clone from the submodule URL (last resort; needs network)
372
+ local sub_query_dir=""
373
+ local sub_dir="$source_worktree_arg/$conflict_path"
374
+ if [[ -d "$sub_dir" ]] && git -C "$sub_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
375
+ sub_query_dir="$sub_dir"
376
+ else
377
+ local repo_git_dir
378
+ repo_git_dir="$(git -C "$source_worktree_arg" rev-parse --git-common-dir 2>/dev/null || true)"
379
+ if [[ -n "$repo_git_dir" && -d "$repo_git_dir/modules/$conflict_path" ]]; then
380
+ sub_query_dir="$repo_git_dir/modules/$conflict_path"
381
+ fi
382
+ fi
383
+
384
+ local temp_sub_clone=""
385
+ cleanup_temp_sub_clone() {
386
+ [[ -n "$temp_sub_clone" && -d "$temp_sub_clone" ]] && rm -rf "$temp_sub_clone"
387
+ }
388
+ trap cleanup_temp_sub_clone RETURN
389
+
390
+ if [[ -z "$sub_query_dir" ]]; then
391
+ local sub_url
392
+ sub_url="$(git -C "$repo_root_arg" config -f .gitmodules --get "submodule.${conflict_path}.url" 2>/dev/null || true)"
393
+ if [[ -z "$sub_url" ]]; then
394
+ return 1
395
+ fi
396
+ temp_sub_clone="$(mktemp -d -t gx-submod-resolve-XXXXXX 2>/dev/null || true)"
397
+ if [[ -z "$temp_sub_clone" || ! -d "$temp_sub_clone" ]]; then
398
+ return 1
399
+ fi
400
+ if ! git clone --quiet --bare "$sub_url" "$temp_sub_clone" >/dev/null 2>&1; then
401
+ return 1
402
+ fi
403
+ sub_query_dir="$temp_sub_clone"
404
+ fi
405
+
406
+ if ! git -C "$sub_query_dir" cat-file -e "${ours_sha}^{commit}" 2>/dev/null \
407
+ || ! git -C "$sub_query_dir" cat-file -e "${theirs_sha}^{commit}" 2>/dev/null; then
408
+ git -C "$sub_query_dir" fetch --quiet --all 2>/dev/null || true
409
+ fi
410
+ if ! git -C "$sub_query_dir" cat-file -e "${ours_sha}^{commit}" 2>/dev/null \
411
+ || ! git -C "$sub_query_dir" cat-file -e "${theirs_sha}^{commit}" 2>/dev/null; then
412
+ return 1
413
+ fi
414
+
415
+ local chosen_sha=""
416
+ if [[ "$ours_sha" == "$theirs_sha" ]]; then
417
+ chosen_sha="$ours_sha"
418
+ elif git -C "$sub_query_dir" merge-base --is-ancestor "$ours_sha" "$theirs_sha" 2>/dev/null; then
419
+ chosen_sha="$theirs_sha"
420
+ elif git -C "$sub_query_dir" merge-base --is-ancestor "$theirs_sha" "$ours_sha" 2>/dev/null; then
421
+ chosen_sha="$ours_sha"
422
+ else
423
+ # Divergent histories; refuse.
424
+ return 1
425
+ fi
426
+
427
+ if ! git -C "$source_worktree_arg" update-index --cacheinfo "160000,${chosen_sha},${conflict_path}" >/dev/null 2>&1; then
428
+ return 1
429
+ fi
430
+
431
+ printf '%s' "$chosen_sha"
432
+ return 0
433
+ }
434
+
162
435
  if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
163
436
  echo "[agent-branch-finish] Not inside a git repository." >&2
164
437
  exit 1
165
438
  fi
166
439
 
167
440
  repo_root="$(git rev-parse --show-toplevel)"
441
+ finish_active_cwd="${GUARDEX_FINISH_ACTIVE_CWD:-$(pwd -P)}"
442
+ if [[ -d "$finish_active_cwd" ]]; then
443
+ finish_active_cwd="$(cd "$finish_active_cwd" && pwd -P)"
444
+ else
445
+ finish_active_cwd=""
446
+ fi
168
447
  # The physical cwd may be a subdirectory inside the source worktree. Cleanup
169
448
  # decisions need the enclosing worktree root, otherwise finishing from `src/`
170
449
  # can delete the caller's cwd and turn a successful merge into a false shell
@@ -178,6 +457,36 @@ else
178
457
  fi
179
458
  repo_common_root="$(cd "$common_git_dir/.." && pwd -P)"
180
459
 
460
+ resolve_same_repo_worktree_for_cwd() {
461
+ local active_cwd="$1"
462
+ [[ -n "$active_cwd" && -d "$active_cwd" ]] || return 0
463
+ git -C "$active_cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
464
+
465
+ local active_worktree=""
466
+ active_worktree="$(git -C "$active_cwd" rev-parse --show-toplevel 2>/dev/null || true)"
467
+ [[ -n "$active_worktree" ]] || return 0
468
+
469
+ local active_common_raw=""
470
+ local active_common_dir=""
471
+ active_common_raw="$(git -C "$active_worktree" rev-parse --git-common-dir 2>/dev/null || true)"
472
+ [[ -n "$active_common_raw" ]] || return 0
473
+ if [[ "$active_common_raw" == /* ]]; then
474
+ active_common_dir="$active_common_raw"
475
+ else
476
+ active_common_dir="${active_worktree}/${active_common_raw}"
477
+ fi
478
+ active_common_dir="$(cd "$active_common_dir" 2>/dev/null && pwd -P)" || return 0
479
+
480
+ if [[ "$active_common_dir" == "$common_git_dir" ]]; then
481
+ cd "$active_worktree" 2>/dev/null && pwd -P
482
+ fi
483
+ }
484
+
485
+ active_cwd_worktree="$(resolve_same_repo_worktree_for_cwd "$finish_active_cwd")"
486
+ if [[ -n "$active_cwd_worktree" ]]; then
487
+ current_worktree="$active_cwd_worktree"
488
+ fi
489
+
181
490
  if [[ -z "$SOURCE_BRANCH" ]]; then
182
491
  SOURCE_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
183
492
  fi
@@ -336,6 +645,22 @@ is_clean_worktree() {
336
645
  && git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"
337
646
  }
338
647
 
648
+ refresh_clean_base_worktree() {
649
+ local wt="$1"
650
+ [[ -z "$wt" || "$PUSH_ENABLED" -ne 1 ]] && return 0
651
+
652
+ if ! is_clean_worktree "$wt"; then
653
+ echo "[agent-branch-finish] Warning: local ${BASE_BRANCH} worktree is dirty; skipping 'git pull --ff-only origin ${BASE_BRANCH}' for ${wt}." >&2
654
+ return 0
655
+ fi
656
+
657
+ if GUARDEX_DISABLE_POST_MERGE_CLEANUP=1 GUARDEX_PRUNE_ACTIVE_CWD="$finish_active_cwd" git -C "$wt" pull --ff-only origin "$BASE_BRANCH" >/dev/null; then
658
+ echo "[agent-branch-finish] Refreshed local ${BASE_BRANCH} worktree with 'git pull --ff-only origin ${BASE_BRANCH}': ${wt}"
659
+ else
660
+ echo "[agent-branch-finish] Warning: failed to refresh local ${BASE_BRANCH} worktree with 'git pull --ff-only origin ${BASE_BRANCH}': ${wt}" >&2
661
+ fi
662
+ }
663
+
339
664
  remove_stale_source_probe_worktrees "$SOURCE_BRANCH"
340
665
  source_worktree="$(get_worktree_for_branch "$SOURCE_BRANCH")"
341
666
  created_source_probe=0
@@ -346,6 +671,7 @@ merge_completed=0
346
671
  merge_status="pr"
347
672
  direct_push_error=""
348
673
  pr_url=""
674
+ changed_submodule_push_done=0
349
675
 
350
676
  cleanup() {
351
677
  if [[ -n "$integration_worktree" && -d "$integration_worktree" ]]; then
@@ -432,20 +758,97 @@ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRA
432
758
 
433
759
  if ! git -C "$source_worktree" merge --no-commit --no-ff "origin/${BASE_BRANCH}" >/dev/null 2>&1; then
434
760
  conflict_files="$(git -C "$source_worktree" diff --name-only --diff-filter=U || true)"
435
- git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
436
761
 
437
- echo "[agent-branch-finish] Preflight conflict detected between '${SOURCE_BRANCH}' and latest origin/${BASE_BRANCH}." >&2
438
- if [[ -n "$conflict_files" ]]; then
439
- echo "[agent-branch-finish] Conflicting files:" >&2
440
- while IFS= read -r file; do
441
- [[ -n "$file" ]] && echo " - ${file}" >&2
762
+ if [[ "$AUTO_RESOLVE_MODE" != "none" && -n "$conflict_files" ]]; then
763
+ auto_resolve_unresolved=""
764
+ auto_resolve_resolved_state=""
765
+ auto_resolve_resolved_submodules=""
766
+ while IFS= read -r conflict_path; do
767
+ [[ -z "$conflict_path" ]] && continue
768
+ if path_matches_auto_resolve_safe_glob "$conflict_path"; then
769
+ if git -C "$source_worktree" checkout --theirs -- "$conflict_path" >/dev/null 2>&1 \
770
+ && git -C "$source_worktree" add -- "$conflict_path" >/dev/null 2>&1; then
771
+ auto_resolve_resolved_state+="${conflict_path}"$'\n'
772
+ continue
773
+ fi
774
+ fi
775
+ if [[ "$AUTO_RESOLVE_MODE" == "full" ]]; then
776
+ if chosen_sha="$(try_resolve_submodule_pointer_conflict "$repo_root" "$source_worktree" "$conflict_path")"; then
777
+ auto_resolve_resolved_submodules+="${conflict_path}@${chosen_sha}"$'\n'
778
+ continue
779
+ fi
780
+ fi
781
+ auto_resolve_unresolved+="${conflict_path}"$'\n'
442
782
  done <<< "$conflict_files"
783
+
784
+ if [[ -n "$auto_resolve_unresolved" ]]; then
785
+ git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
786
+ echo "[agent-branch-finish] --auto-resolve=${AUTO_RESOLVE_MODE}: some conflicts are outside the safe allowlist (or submodule histories diverge); aborting." >&2
787
+ echo "[agent-branch-finish] Unresolved conflicts:" >&2
788
+ while IFS= read -r unresolved_path; do
789
+ [[ -n "$unresolved_path" ]] && echo " - ${unresolved_path}" >&2
790
+ done <<< "$auto_resolve_unresolved"
791
+ echo "[agent-branch-finish] State-file allowlist (GUARDEX_FINISH_AUTO_RESOLVE_SAFE_GLOBS): ${AUTO_RESOLVE_SAFE_GLOBS_RAW}" >&2
792
+ if [[ "$AUTO_RESOLVE_MODE" != "full" ]]; then
793
+ echo "[agent-branch-finish] Submodule pointer auto-resolve requires --auto-resolve=full; not enabled for this run." >&2
794
+ fi
795
+ exit 1
796
+ fi
797
+
798
+ # Claim resolved paths so the pre-commit lock guard accepts the merge.
799
+ auto_resolve_claim_paths=()
800
+ while IFS= read -r resolved_path; do
801
+ [[ -n "$resolved_path" ]] && auto_resolve_claim_paths+=("$resolved_path")
802
+ done <<< "$auto_resolve_resolved_state"
803
+ while IFS= read -r resolved_entry; do
804
+ [[ -z "$resolved_entry" ]] && continue
805
+ auto_resolve_claim_paths+=("${resolved_entry%@*}")
806
+ done <<< "$auto_resolve_resolved_submodules"
807
+ if [[ "${#auto_resolve_claim_paths[@]}" -gt 0 ]]; then
808
+ run_guardex_cli locks claim --branch "$SOURCE_BRANCH" "${auto_resolve_claim_paths[@]}" >/dev/null 2>&1 || true
809
+ fi
810
+
811
+ auto_resolve_commit_msg="Merge origin/${BASE_BRANCH} into ${SOURCE_BRANCH} (gx --auto-resolve=${AUTO_RESOLVE_MODE}; state files -> base, submodule pointers fast-forwarded)"
812
+ if ! git -C "$source_worktree" commit -m "$auto_resolve_commit_msg" >/dev/null 2>&1; then
813
+ git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
814
+ echo "[agent-branch-finish] --auto-resolve=${AUTO_RESOLVE_MODE}: failed to commit resolved merge (pre-commit hook may have rejected it; verify file locks)." >&2
815
+ exit 1
816
+ fi
817
+
818
+ state_count=0
819
+ submod_count=0
820
+ [[ -n "$auto_resolve_resolved_state" ]] && state_count="$(printf '%s' "$auto_resolve_resolved_state" | grep -c '^[^[:space:]]')"
821
+ [[ -n "$auto_resolve_resolved_submodules" ]] && submod_count="$(printf '%s' "$auto_resolve_resolved_submodules" | grep -c '^[^[:space:]]')"
822
+ echo "[agent-branch-finish] --auto-resolve=${AUTO_RESOLVE_MODE}: resolved ${state_count} state-file conflict(s), ${submod_count} submodule pointer conflict(s)." >&2
823
+ if [[ -n "$auto_resolve_resolved_state" ]]; then
824
+ echo "[agent-branch-finish] State files (resolved to base):" >&2
825
+ while IFS= read -r resolved_path; do
826
+ [[ -n "$resolved_path" ]] && echo " - ${resolved_path}" >&2
827
+ done <<< "$auto_resolve_resolved_state"
828
+ fi
829
+ if [[ -n "$auto_resolve_resolved_submodules" ]]; then
830
+ echo "[agent-branch-finish] Submodule pointers (fast-forwarded):" >&2
831
+ while IFS= read -r resolved_entry; do
832
+ [[ -n "$resolved_entry" ]] && echo " - ${resolved_entry%@*} -> ${resolved_entry##*@}" >&2
833
+ done <<< "$auto_resolve_resolved_submodules"
834
+ fi
835
+ else
836
+ git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
837
+
838
+ echo "[agent-branch-finish] Preflight conflict detected between '${SOURCE_BRANCH}' and latest origin/${BASE_BRANCH}." >&2
839
+ if [[ -n "$conflict_files" ]]; then
840
+ echo "[agent-branch-finish] Conflicting files:" >&2
841
+ while IFS= read -r file; do
842
+ [[ -n "$file" ]] && echo " - ${file}" >&2
843
+ done <<< "$conflict_files"
844
+ fi
845
+ echo "[agent-branch-finish] Rebase/merge '${BASE_BRANCH}' into '${SOURCE_BRANCH}' and resolve conflicts before finishing." >&2
846
+ echo "[agent-branch-finish] Or rerun with --auto-resolve=safe (state files) or --auto-resolve=full (state files + fast-forward-able submodule pointers)." >&2
847
+ exit 1
443
848
  fi
444
- echo "[agent-branch-finish] Rebase/merge '${BASE_BRANCH}' into '${SOURCE_BRANCH}' and resolve conflicts before finishing." >&2
445
- exit 1
849
+ else
850
+ git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
446
851
  fi
447
-
448
- git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
449
852
  fi
450
853
 
451
854
  should_create_integration_helper=1
@@ -682,14 +1085,89 @@ maybe_auto_commit_parent_gitlink() {
682
1085
  echo "[agent-branch-finish] Parent gitlink auto-committed '${subrepo_rel}' in ${super_root}."
683
1086
  }
684
1087
 
1088
+ maybe_push_changed_submodule_branches() {
1089
+ local base_ref="${1:-}"
1090
+ local source_ref="${2:-}"
1091
+ local changed_path=""
1092
+ local gitlink_mode=""
1093
+ local gitlink_sha=""
1094
+ local submodule_dir=""
1095
+ local branch_name=""
1096
+ local candidate_branch=""
1097
+ local remote_name=""
1098
+ local push_output=""
1099
+
1100
+ if [[ "$PUSH_ENABLED" -ne 1 || "$changed_submodule_push_done" -eq 1 ]]; then
1101
+ return 0
1102
+ fi
1103
+ changed_submodule_push_done=1
1104
+ if [[ -z "$base_ref" || -z "$source_ref" ]]; then
1105
+ return 0
1106
+ fi
1107
+
1108
+ while IFS= read -r changed_path; do
1109
+ [[ -n "$changed_path" ]] || continue
1110
+
1111
+ gitlink_mode="$(git -C "$source_worktree" ls-tree "$source_ref" -- "$changed_path" | awk 'NR == 1 { print $1 }')"
1112
+ if [[ "$gitlink_mode" != "160000" ]]; then
1113
+ continue
1114
+ fi
1115
+ gitlink_sha="$(git -C "$source_worktree" ls-tree "$source_ref" -- "$changed_path" | awk 'NR == 1 { print $3 }')"
1116
+ if [[ -z "$gitlink_sha" ]]; then
1117
+ continue
1118
+ fi
1119
+
1120
+ submodule_dir="${source_worktree}/${changed_path}"
1121
+ if ! git -C "$submodule_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
1122
+ echo "[agent-branch-finish] Warning: changed gitlink '${changed_path}' has no checked-out submodule at ${submodule_dir}; cannot auto-push submodule commit ${gitlink_sha}." >&2
1123
+ return 1
1124
+ fi
1125
+ if ! git -C "$submodule_dir" cat-file -e "${gitlink_sha}^{commit}" >/dev/null 2>&1; then
1126
+ echo "[agent-branch-finish] Warning: changed gitlink '${changed_path}' points at ${gitlink_sha}, but that commit is not present in ${submodule_dir}; cannot auto-push it." >&2
1127
+ return 1
1128
+ fi
1129
+
1130
+ branch_name="$(git -C "$submodule_dir" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
1131
+ if [[ -z "$branch_name" || "$branch_name" == "HEAD" ]] || ! git -C "$submodule_dir" merge-base --is-ancestor "$gitlink_sha" "$branch_name" >/dev/null 2>&1; then
1132
+ candidate_branch="$(git -C "$submodule_dir" for-each-ref --contains "$gitlink_sha" --format='%(refname:short)' refs/heads | head -n 1)"
1133
+ branch_name="$candidate_branch"
1134
+ fi
1135
+ if [[ -z "$branch_name" || "$branch_name" == "HEAD" ]]; then
1136
+ echo "[agent-branch-finish] Warning: changed gitlink '${changed_path}' points at ${gitlink_sha}, but no local submodule branch contains it; cannot choose a safe remote branch to push." >&2
1137
+ return 1
1138
+ fi
1139
+
1140
+ remote_name="$(git -C "$submodule_dir" config --get "branch.${branch_name}.remote" || true)"
1141
+ if [[ -z "$remote_name" ]]; then
1142
+ remote_name="origin"
1143
+ fi
1144
+ if ! git -C "$submodule_dir" remote get-url "$remote_name" >/dev/null 2>&1; then
1145
+ echo "[agent-branch-finish] Warning: changed gitlink '${changed_path}' branch '${branch_name}' has no usable remote '${remote_name}'; cannot auto-push submodule commit ${gitlink_sha}." >&2
1146
+ return 1
1147
+ fi
1148
+
1149
+ if push_output="$(git -C "$submodule_dir" push -u "$remote_name" "${branch_name}:${branch_name}" 2>&1)"; then
1150
+ echo "[agent-branch-finish] Pushed changed submodule '${changed_path}' branch '${branch_name}' to '${remote_name}' before parent finish."
1151
+ else
1152
+ echo "[agent-branch-finish] Changed submodule '${changed_path}' must be pushed before the parent branch can be finished." >&2
1153
+ [[ -n "$push_output" ]] && echo "$push_output" >&2
1154
+ return 1
1155
+ fi
1156
+ done < <(git -C "$source_worktree" diff --name-only "$base_ref" "$source_ref" -- 2>/dev/null || true)
1157
+ }
1158
+
685
1159
  wait_for_pr_merge() {
686
1160
  local deadline
687
1161
  deadline=$(( $(date +%s) + WAIT_TIMEOUT_SECONDS ))
688
1162
  local wait_notice_printed=0
689
1163
  local merge_output=""
1164
+ local -a merge_flags=(--squash --delete-branch)
1165
+ if [[ "$SKIP_CHECKS" -eq 1 ]]; then
1166
+ merge_flags+=(--admin)
1167
+ fi
690
1168
 
691
1169
  while true; do
692
- if merge_output="$("$GH_BIN" pr merge "$SOURCE_BRANCH" --squash --delete-branch 2>&1)"; then
1170
+ if merge_output="$("$GH_BIN" pr merge "$SOURCE_BRANCH" "${merge_flags[@]}" 2>&1)"; then
693
1171
  return 0
694
1172
  fi
695
1173
  if is_local_branch_delete_error "$merge_output"; then
@@ -752,6 +1230,7 @@ run_pr_flow() {
752
1230
  return 0
753
1231
  fi
754
1232
 
1233
+ maybe_push_changed_submodule_branches "$start_ref" "$SOURCE_BRANCH"
755
1234
  git -C "$source_worktree" push -u origin "$SOURCE_BRANCH"
756
1235
 
757
1236
  pr_title="$(git -C "$repo_root" log -1 --pretty=%s "$SOURCE_BRANCH" 2>/dev/null || true)"
@@ -760,16 +1239,45 @@ run_pr_flow() {
760
1239
  fi
761
1240
  pr_body="Automated by gx branch finish (PR flow)."
762
1241
 
763
- "$GH_BIN" pr create \
1242
+ pr_create_output=""
1243
+ if pr_create_output="$("$GH_BIN" pr create \
764
1244
  --base "$BASE_BRANCH" \
765
1245
  --head "$SOURCE_BRANCH" \
766
1246
  --title "$pr_title" \
767
- --body "$pr_body" >/dev/null 2>&1 || true
1247
+ --body "$pr_body" 2>&1)"; then
1248
+ :
1249
+ else
1250
+ # Idempotent: a PR already opened for this head is fine — fall through
1251
+ # to `gh pr view` so we still capture the URL. Anything else is a real
1252
+ # failure and the user needs to see it.
1253
+ if ! grep -qiE 'already exists|a pull request for branch' <<<"$pr_create_output"; then
1254
+ echo "[agent-branch-finish] gh pr create failed:" >&2
1255
+ echo "${pr_create_output}" >&2
1256
+ fi
1257
+ fi
768
1258
 
769
1259
  pr_url="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json url --jq '.url' 2>/dev/null || true)"
770
1260
 
1261
+ if [[ -z "$pr_url" ]]; then
1262
+ echo "[agent-branch-finish] No PR found for '${SOURCE_BRANCH}' after gh pr create; cannot proceed with PR merge." >&2
1263
+ if [[ -n "$pr_create_output" ]]; then
1264
+ echo "[agent-branch-finish] Last gh pr create output:" >&2
1265
+ echo "${pr_create_output}" >&2
1266
+ fi
1267
+ return 1
1268
+ fi
1269
+ echo "[agent-branch-finish] PR URL: ${pr_url}" >&2
1270
+
1271
+ # Pre-flight already passed by the time we reach the PR; promote any
1272
+ # existing draft so the budget-friendly CI gate fires once.
1273
+ maybe_auto_promote_pr "$pr_url"
1274
+
771
1275
  merge_output=""
772
- if merge_output="$("$GH_BIN" pr merge "$SOURCE_BRANCH" --squash --delete-branch 2>&1)"; then
1276
+ local -a merge_flags=(--squash --delete-branch)
1277
+ if [[ "$SKIP_CHECKS" -eq 1 ]]; then
1278
+ merge_flags+=(--admin)
1279
+ fi
1280
+ if merge_output="$("$GH_BIN" pr merge "$SOURCE_BRANCH" "${merge_flags[@]}" 2>&1)"; then
773
1281
  return 0
774
1282
  fi
775
1283
  if is_local_branch_delete_error "$merge_output"; then
@@ -783,7 +1291,11 @@ run_pr_flow() {
783
1291
  fi
784
1292
 
785
1293
  auto_output=""
786
- if auto_output="$("$GH_BIN" pr merge "$SOURCE_BRANCH" --squash --delete-branch --auto 2>&1)"; then
1294
+ local -a auto_flags=(--squash --delete-branch --auto)
1295
+ if [[ "$SKIP_CHECKS" -eq 1 ]]; then
1296
+ auto_flags+=(--admin)
1297
+ fi
1298
+ if auto_output="$("$GH_BIN" pr merge "$SOURCE_BRANCH" "${auto_flags[@]}" 2>&1)"; then
787
1299
  echo "[agent-branch-finish] PR auto-merge enabled; waiting for required checks/reviews." >&2
788
1300
  return 2
789
1301
  fi
@@ -799,7 +1311,11 @@ run_pr_flow() {
799
1311
  }
800
1312
 
801
1313
  if [[ "$PUSH_ENABLED" -eq 1 ]]; then
1314
+ if ! run_preflight "$source_worktree"; then
1315
+ exit 1
1316
+ fi
802
1317
  if [[ "$MERGE_MODE" != "pr" ]]; then
1318
+ maybe_push_changed_submodule_branches "$start_ref" "$SOURCE_BRANCH"
803
1319
  if ! direct_push_output="$(git -C "$integration_worktree" push origin "HEAD:${BASE_BRANCH}" 2>&1)"; then
804
1320
  direct_push_error="$direct_push_output"
805
1321
  merge_completed=0
@@ -831,7 +1347,7 @@ if [[ "$PUSH_ENABLED" -eq 1 ]]; then
831
1347
  echo "[agent-branch-finish] Merge did not complete within wait window; keeping branch open." >&2
832
1348
  exit 1
833
1349
  fi
834
- echo "[agent-branch-finish] Merge pending review/check policy. Branch cleanup skipped for now." >&2
1350
+ echo "[agent-branch-finish] PR pending review/check policy. Worktree retained for now; the autofinish watcher (or 'gx worktree prune --include-pr-merged --delete-branches') will prune it after merge. Verify with 'git worktree list' before claiming the worktree is still on disk." >&2
835
1351
  exit 0
836
1352
  fi
837
1353
  echo "[agent-branch-finish] PR flow failed." >&2
@@ -847,9 +1363,7 @@ fi
847
1363
  run_guardex_cli locks release --branch "$SOURCE_BRANCH" >/dev/null 2>&1 || true
848
1364
 
849
1365
  base_worktree="$(get_worktree_for_branch "$BASE_BRANCH")"
850
- if [[ -n "$base_worktree" ]] && is_clean_worktree "$base_worktree" && [[ "$PUSH_ENABLED" -eq 1 ]]; then
851
- git -C "$base_worktree" pull --ff-only origin "$BASE_BRANCH" >/dev/null 2>&1 || true
852
- fi
1366
+ refresh_clean_base_worktree "$base_worktree"
853
1367
  maybe_auto_commit_parent_gitlink "$base_worktree"
854
1368
 
855
1369
  # Pivot out of the agent worktree before prune calls that may remove it.
@@ -861,6 +1375,10 @@ pivot_to_repo_root_before_prune() {
861
1375
  fi
862
1376
  }
863
1377
 
1378
+ run_guardex_prune() {
1379
+ GUARDEX_PRUNE_ACTIVE_CWD="$finish_active_cwd" run_guardex_cli worktree prune "$@"
1380
+ }
1381
+
864
1382
  if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
865
1383
  if [[ "$source_worktree" == "$repo_root" ]]; then
866
1384
  if is_clean_worktree "$source_worktree"; then
@@ -871,7 +1389,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
871
1389
  git -C "$source_worktree" checkout --detach >/dev/null 2>&1 || true
872
1390
  fi
873
1391
  if [[ "$switched_to_base" -eq 1 && "$PUSH_ENABLED" -eq 1 ]] && git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
874
- git -C "$source_worktree" pull --ff-only origin "$BASE_BRANCH" >/dev/null 2>&1 || true
1392
+ refresh_clean_base_worktree "$source_worktree"
875
1393
  fi
876
1394
  fi
877
1395
  elif [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${agent_worktree_root}"/* ]]; then
@@ -906,7 +1424,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
906
1424
  fi
907
1425
 
908
1426
  pivot_to_repo_root_before_prune
909
- if ! run_guardex_cli worktree prune "${prune_args[@]}"; then
1427
+ if ! run_guardex_prune "${prune_args[@]}"; then
910
1428
  echo "[agent-branch-finish] Warning: automatic worktree prune failed." >&2
911
1429
  echo "[agent-branch-finish] You can run manual cleanup: gx cleanup --base ${BASE_BRANCH}" >&2
912
1430
  fi
@@ -920,7 +1438,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
920
1438
  fi
921
1439
  else
922
1440
  pivot_to_repo_root_before_prune
923
- if ! run_guardex_cli worktree prune --base "$BASE_BRANCH"; then
1441
+ if ! run_guardex_prune --base "$BASE_BRANCH"; then
924
1442
  echo "[agent-branch-finish] Warning: temporary worktree prune failed." >&2
925
1443
  fi
926
1444