@imdeadpool/guardex 7.0.41 → 7.1.0

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 (118) hide show
  1. package/README.md +94 -13
  2. package/package.json +3 -1
  3. package/skills/gitguardex/SKILL.md +13 -0
  4. package/skills/guardex-merge-skills-to-dev/SKILL.md +59 -0
  5. package/skills/gx-act/SKILL.md +82 -0
  6. package/src/agents/cleanup-sessions.js +126 -0
  7. package/src/agents/finish.js +172 -0
  8. package/src/agents/inspect.js +202 -0
  9. package/src/agents/launch.js +249 -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 +146 -0
  15. package/src/agents/terminal.js +152 -0
  16. package/src/budget/index.js +344 -0
  17. package/src/ci-init/index.js +265 -0
  18. package/src/cli/args.js +357 -3
  19. package/src/cli/commands/agents.js +364 -0
  20. package/src/cli/commands/bootstrap.js +92 -0
  21. package/src/cli/commands/branch.js +127 -0
  22. package/src/cli/commands/claude.js +674 -0
  23. package/src/cli/commands/doctor.js +268 -0
  24. package/src/cli/commands/finish.js +26 -0
  25. package/src/cli/commands/mcp.js +122 -0
  26. package/src/cli/commands/misc.js +304 -0
  27. package/src/cli/commands/pr.js +439 -0
  28. package/src/cli/commands/prompt.js +92 -0
  29. package/src/cli/commands/release.js +305 -0
  30. package/src/cli/commands/report.js +244 -0
  31. package/src/cli/commands/review.js +32 -0
  32. package/src/cli/commands/setup.js +242 -0
  33. package/src/cli/commands/status.js +338 -0
  34. package/src/cli/commands/watch.js +234 -0
  35. package/src/cli/main.js +85 -3613
  36. package/src/cli/shared/repo-env.js +161 -0
  37. package/src/cli/shared/sandbox.js +417 -0
  38. package/src/cli/shared/scaffolding.js +535 -0
  39. package/src/cli/shared/toolchain-shims.js +420 -0
  40. package/src/cockpit/action-runner.js +3 -0
  41. package/src/cockpit/actions.js +80 -0
  42. package/src/cockpit/control.js +1121 -0
  43. package/src/cockpit/index.js +426 -0
  44. package/src/cockpit/kitty-layout.js +549 -0
  45. package/src/cockpit/kitty-tree.js +144 -0
  46. package/src/cockpit/logs-reader.js +182 -0
  47. package/src/cockpit/menu.js +204 -0
  48. package/src/cockpit/pane-actions.js +597 -0
  49. package/src/cockpit/pane-menu.js +387 -0
  50. package/src/cockpit/projects-finder.js +178 -0
  51. package/src/cockpit/render.js +215 -0
  52. package/src/cockpit/settings-render.js +128 -0
  53. package/src/cockpit/settings.js +124 -0
  54. package/src/cockpit/shortcuts.js +24 -0
  55. package/src/cockpit/sidebar.js +311 -0
  56. package/src/cockpit/state.js +72 -0
  57. package/src/cockpit/theme.js +128 -0
  58. package/src/cockpit/welcome.js +266 -0
  59. package/src/context.js +304 -43
  60. package/src/core/runtime.js +6 -1
  61. package/src/doctor/index.js +45 -15
  62. package/src/finish/index.js +186 -7
  63. package/src/finish/preflight.js +177 -0
  64. package/src/finish/review-gate.js +182 -0
  65. package/src/git/index.js +511 -4
  66. package/src/hooks/index.js +0 -64
  67. package/src/kitty/command.js +101 -0
  68. package/src/kitty/runtime.js +250 -0
  69. package/src/mcp/collect.js +370 -0
  70. package/src/mcp/server.js +157 -0
  71. package/src/output/index.js +68 -2
  72. package/src/pr-review.js +264 -0
  73. package/src/pr.js +381 -0
  74. package/src/sandbox/index.js +13 -2
  75. package/src/scaffold/agent-worktree-prep.js +213 -0
  76. package/src/scaffold/index.js +127 -10
  77. package/src/speckit/index.js +226 -0
  78. package/src/submodule/index.js +288 -0
  79. package/src/terminal/index.js +45 -0
  80. package/src/terminal/kitty.js +622 -0
  81. package/src/terminal/tmux.js +125 -0
  82. package/src/tmux/command.js +27 -0
  83. package/src/tmux/session.js +89 -0
  84. package/src/toolchain/index.js +20 -0
  85. package/templates/AGENTS.monorepo-apps.md +26 -0
  86. package/templates/AGENTS.multiagent-safety.md +63 -323
  87. package/templates/AGENTS.multiagent-safety.min.md +11 -0
  88. package/templates/codex/skills/gitguardex/SKILL.md +2 -0
  89. package/templates/codex/skills/gx-act/SKILL.md +82 -0
  90. package/templates/githooks/pre-commit +44 -20
  91. package/templates/github/workflows/README.md +87 -0
  92. package/templates/github/workflows/ci-full.yml +55 -0
  93. package/templates/github/workflows/ci.yml +56 -0
  94. package/templates/github/workflows/cr.yml +20 -1
  95. package/templates/scripts/agent-branch-finish.sh +519 -23
  96. package/templates/scripts/agent-branch-merge.sh +4 -1
  97. package/templates/scripts/agent-branch-start.sh +176 -24
  98. package/templates/scripts/agent-preflight.sh +115 -0
  99. package/templates/scripts/agent-worktree-prune.sh +96 -5
  100. package/templates/scripts/codex-agent.sh +41 -97
  101. package/templates/scripts/openspec/init-plan-workspace.sh +43 -0
  102. package/templates/scripts/review-bot-watch.sh +31 -2
  103. package/templates/scripts/agent-session-state.js +0 -171
  104. package/templates/scripts/install-vscode-active-agents-extension.js +0 -135
  105. package/templates/vscode/guardex-active-agents/README.md +0 -34
  106. package/templates/vscode/guardex-active-agents/extension.js +0 -3782
  107. package/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +0 -54
  108. package/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +0 -5
  109. package/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +0 -7
  110. package/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +0 -4
  111. package/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +0 -4
  112. package/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +0 -5
  113. package/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +0 -4
  114. package/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +0 -5
  115. package/templates/vscode/guardex-active-agents/icon.png +0 -0
  116. package/templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg +0 -14
  117. package/templates/vscode/guardex-active-agents/package.json +0 -169
  118. package/templates/vscode/guardex-active-agents/session-schema.js +0 -1348
@@ -11,11 +11,17 @@ 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/**:.codex/settings.local.json:.claude/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}"
19
25
 
20
26
  run_guardex_cli() {
21
27
  if [[ -n "$CLI_ENTRY" ]]; then
@@ -64,14 +70,90 @@ normalize_int() {
64
70
  printf '%s' "$value"
65
71
  }
66
72
 
67
- CLEANUP_AFTER_MERGE="$(normalize_bool "$CLEANUP_AFTER_MERGE_RAW" "0")"
68
- WAIT_FOR_MERGE="$(normalize_bool "$WAIT_FOR_MERGE_RAW" "0")"
73
+ # Resolve the pre-flight script path against the source worktree. The
74
+ # caller passes either the configured path (which may be relative) or
75
+ # an empty string; we return the absolute path if it exists and is
76
+ # executable, otherwise return empty.
77
+ resolve_preflight_script() {
78
+ local worktree="$1"
79
+ local configured="$2"
80
+ if [[ -z "$configured" ]]; then
81
+ configured="scripts/agent-preflight.sh"
82
+ fi
83
+ if [[ "$configured" = /* ]]; then
84
+ if [[ -x "$configured" ]]; then
85
+ printf '%s' "$configured"
86
+ fi
87
+ return 0
88
+ fi
89
+ local candidate="${worktree}/${configured}"
90
+ if [[ -x "$candidate" ]]; then
91
+ printf '%s' "$candidate"
92
+ fi
93
+ }
94
+
95
+ # Run the pre-flight verification gate in the agent worktree before
96
+ # any push happens. Returns 0 on success or when no gate is
97
+ # configured; returns non-zero (and prints a hint) on failure, which
98
+ # the caller propagates so the push is refused.
99
+ run_preflight() {
100
+ local worktree="$1"
101
+ if [[ "$PREFLIGHT_ENABLED" -ne 1 ]]; then
102
+ return 0
103
+ fi
104
+ local script_path
105
+ script_path="$(resolve_preflight_script "$worktree" "$PREFLIGHT_SCRIPT_RAW")"
106
+ if [[ -z "$script_path" ]]; then
107
+ echo "[agent-branch-finish] No executable pre-flight script at ${PREFLIGHT_SCRIPT_RAW} (in ${worktree}); skipping pre-flight." >&2
108
+ return 0
109
+ fi
110
+ echo "[agent-branch-finish] Running pre-flight: ${script_path}" >&2
111
+ if ( cd "$worktree" && "$script_path" ); then
112
+ echo "[agent-branch-finish] Pre-flight passed." >&2
113
+ return 0
114
+ fi
115
+ echo "[agent-branch-finish] Pre-flight FAILED; refusing push. Override with --no-preflight if you really mean it." >&2
116
+ return 1
117
+ }
118
+
119
+ # After a PR exists, if it is in draft and auto-promote is enabled,
120
+ # mark it ready-for-review. With the budget-friendly CI defaults
121
+ # (draft PRs skip CI), this is the moment when CI is allowed to fire.
122
+ maybe_auto_promote_pr() {
123
+ local pr_url="$1"
124
+ if [[ -z "$pr_url" ]] || [[ "$AUTO_PROMOTE_DRAFT" -ne 1 ]]; then
125
+ return 0
126
+ fi
127
+ if ! command -v "$GH_BIN" >/dev/null 2>&1; then
128
+ return 0
129
+ fi
130
+ local is_draft
131
+ is_draft="$("$GH_BIN" pr view "$pr_url" --json isDraft --jq '.isDraft' 2>/dev/null || true)"
132
+ if [[ "$is_draft" != "true" ]]; then
133
+ return 0
134
+ fi
135
+ echo "[agent-branch-finish] PR is draft; promoting to ready-for-review (pre-flight passed)." >&2
136
+ if "$GH_BIN" pr ready "$pr_url" >/dev/null 2>&1; then
137
+ echo "[agent-branch-finish] PR marked ready-for-review." >&2
138
+ else
139
+ echo "[agent-branch-finish] gh pr ready failed; PR left in draft. Promote manually if intended." >&2
140
+ fi
141
+ }
142
+
143
+ CLEANUP_AFTER_MERGE="$(normalize_bool "$CLEANUP_AFTER_MERGE_RAW" "1")"
144
+ WAIT_FOR_MERGE="$(normalize_bool "$WAIT_FOR_MERGE_RAW" "1")"
69
145
  WAIT_TIMEOUT_SECONDS="$(normalize_int "$WAIT_TIMEOUT_SECONDS_RAW" "1800" "30")"
70
146
  WAIT_POLL_SECONDS="$(normalize_int "$WAIT_POLL_SECONDS_RAW" "10" "0")"
71
147
  PARENT_GITLINK_AUTO_COMMIT="$(normalize_bool "$PARENT_GITLINK_AUTO_COMMIT_RAW" "1")"
148
+ PREFLIGHT_ENABLED="$(normalize_bool "$PREFLIGHT_ENABLED_RAW" "1")"
149
+ AUTO_PROMOTE_DRAFT="$(normalize_bool "$AUTO_PROMOTE_DRAFT_RAW" "1")"
72
150
 
73
151
  while [[ $# -gt 0 ]]; do
74
152
  case "$1" in
153
+ -h|--help)
154
+ 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]"
155
+ exit 0
156
+ ;;
75
157
  --base)
76
158
  BASE_BRANCH="${2:-}"
77
159
  BASE_BRANCH_EXPLICIT=1
@@ -139,9 +221,46 @@ while [[ $# -gt 0 ]]; do
139
221
  MERGE_MODE="direct"
140
222
  shift
141
223
  ;;
224
+ --auto-resolve)
225
+ if [[ "${2:-}" =~ ^(none|safe|full)$ ]]; then
226
+ AUTO_RESOLVE_MODE_RAW="$2"
227
+ shift 2
228
+ else
229
+ AUTO_RESOLVE_MODE_RAW="safe"
230
+ shift
231
+ fi
232
+ ;;
233
+ --auto-resolve=*)
234
+ AUTO_RESOLVE_MODE_RAW="${1#--auto-resolve=}"
235
+ shift
236
+ ;;
237
+ --no-auto-resolve)
238
+ AUTO_RESOLVE_MODE_RAW="none"
239
+ shift
240
+ ;;
241
+ --no-preflight)
242
+ PREFLIGHT_ENABLED_RAW="false"
243
+ shift
244
+ ;;
245
+ --preflight)
246
+ PREFLIGHT_ENABLED_RAW="true"
247
+ shift
248
+ ;;
249
+ --preflight-script)
250
+ PREFLIGHT_SCRIPT_RAW="${2:-}"
251
+ shift 2
252
+ ;;
253
+ --no-auto-promote)
254
+ AUTO_PROMOTE_DRAFT_RAW="false"
255
+ shift
256
+ ;;
257
+ --auto-promote)
258
+ AUTO_PROMOTE_DRAFT_RAW="true"
259
+ shift
260
+ ;;
142
261
  *)
143
262
  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
263
+ 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
264
  exit 1
146
265
  ;;
147
266
  esac
@@ -159,12 +278,162 @@ case "$MERGE_MODE" in
159
278
  ;;
160
279
  esac
161
280
 
281
+ AUTO_RESOLVE_MODE="$(printf '%s' "$AUTO_RESOLVE_MODE_RAW" | tr '[:upper:]' '[:lower:]')"
282
+ case "$AUTO_RESOLVE_MODE" in
283
+ none|safe|full) ;;
284
+ *)
285
+ echo "[agent-branch-finish] Invalid --auto-resolve value: ${AUTO_RESOLVE_MODE_RAW} (expected none|safe|full)" >&2
286
+ exit 1
287
+ ;;
288
+ esac
289
+
290
+ path_matches_auto_resolve_safe_glob() {
291
+ local path="$1"
292
+ if [[ -z "${AUTO_RESOLVE_SAFE_GLOBS_RAW:-}" ]]; then
293
+ return 1
294
+ fi
295
+ local -a globs=()
296
+ IFS=':' read -ra globs <<< "$AUTO_RESOLVE_SAFE_GLOBS_RAW"
297
+ local pattern rewritten
298
+ for pattern in "${globs[@]}"; do
299
+ [[ -z "$pattern" ]] && continue
300
+ rewritten="${pattern%/**}"
301
+ if [[ "$rewritten" != "$pattern" ]]; then
302
+ if [[ "$path" == "$rewritten"/* ]]; then
303
+ return 0
304
+ fi
305
+ else
306
+ # shellcheck disable=SC2053
307
+ if [[ "$path" == $pattern ]]; then
308
+ return 0
309
+ fi
310
+ fi
311
+ done
312
+ return 1
313
+ }
314
+
315
+ # Resolve a conflicting submodule pointer if and only if one side is a strict
316
+ # ancestor of the other (fast-forward direction). Writes the resolved SHA via
317
+ # git update-index and prints the chosen SHA on stdout. Returns 0 on success,
318
+ # 1 on uninitialized/divergent/unreachable cases.
319
+ try_resolve_submodule_pointer_conflict() {
320
+ local repo_root_arg="$1"
321
+ local source_worktree_arg="$2"
322
+ local conflict_path="$3"
323
+
324
+ # Confirm registered submodule path.
325
+ if [[ ! -f "$repo_root_arg/.gitmodules" ]]; then
326
+ return 1
327
+ fi
328
+ if ! git -C "$repo_root_arg" config -f .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null \
329
+ | awk '{print $2}' | grep -Fxq -- "$conflict_path"; then
330
+ return 1
331
+ fi
332
+
333
+ # Read the three stages from the index.
334
+ local stage_out
335
+ stage_out="$(git -C "$source_worktree_arg" ls-files -u -- "$conflict_path" 2>/dev/null || true)"
336
+ if [[ -z "$stage_out" ]]; then
337
+ return 1
338
+ fi
339
+
340
+ local base_sha="" ours_sha="" theirs_sha=""
341
+ local mode_field stage_sha stage_num path_field
342
+ while IFS=$'\t' read -r meta path_field; do
343
+ [[ -z "$meta" || -z "$path_field" ]] && continue
344
+ # meta format: "<mode> <sha> <stage>"
345
+ read -r mode_field stage_sha stage_num <<< "$meta"
346
+ [[ "$mode_field" != "160000" ]] && return 1
347
+ case "$stage_num" in
348
+ 1) base_sha="$stage_sha" ;;
349
+ 2) ours_sha="$stage_sha" ;;
350
+ 3) theirs_sha="$stage_sha" ;;
351
+ esac
352
+ done <<< "$stage_out"
353
+
354
+ if [[ -z "$ours_sha" || -z "$theirs_sha" ]]; then
355
+ return 1
356
+ fi
357
+
358
+ # Pick a working clone for the submodule. Three sources, in order:
359
+ # 1) checked-out submodule worktree (cheap, no network)
360
+ # 2) cached internal clone at .git/modules/<name>
361
+ # 3) temp bare clone from the submodule URL (last resort; needs network)
362
+ local sub_query_dir=""
363
+ local sub_dir="$source_worktree_arg/$conflict_path"
364
+ if [[ -d "$sub_dir" ]] && git -C "$sub_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
365
+ sub_query_dir="$sub_dir"
366
+ else
367
+ local repo_git_dir
368
+ repo_git_dir="$(git -C "$source_worktree_arg" rev-parse --git-common-dir 2>/dev/null || true)"
369
+ if [[ -n "$repo_git_dir" && -d "$repo_git_dir/modules/$conflict_path" ]]; then
370
+ sub_query_dir="$repo_git_dir/modules/$conflict_path"
371
+ fi
372
+ fi
373
+
374
+ local temp_sub_clone=""
375
+ cleanup_temp_sub_clone() {
376
+ [[ -n "$temp_sub_clone" && -d "$temp_sub_clone" ]] && rm -rf "$temp_sub_clone"
377
+ }
378
+ trap cleanup_temp_sub_clone RETURN
379
+
380
+ if [[ -z "$sub_query_dir" ]]; then
381
+ local sub_url
382
+ sub_url="$(git -C "$repo_root_arg" config -f .gitmodules --get "submodule.${conflict_path}.url" 2>/dev/null || true)"
383
+ if [[ -z "$sub_url" ]]; then
384
+ return 1
385
+ fi
386
+ temp_sub_clone="$(mktemp -d -t gx-submod-resolve-XXXXXX 2>/dev/null || true)"
387
+ if [[ -z "$temp_sub_clone" || ! -d "$temp_sub_clone" ]]; then
388
+ return 1
389
+ fi
390
+ if ! git clone --quiet --bare "$sub_url" "$temp_sub_clone" >/dev/null 2>&1; then
391
+ return 1
392
+ fi
393
+ sub_query_dir="$temp_sub_clone"
394
+ fi
395
+
396
+ if ! git -C "$sub_query_dir" cat-file -e "${ours_sha}^{commit}" 2>/dev/null \
397
+ || ! git -C "$sub_query_dir" cat-file -e "${theirs_sha}^{commit}" 2>/dev/null; then
398
+ git -C "$sub_query_dir" fetch --quiet --all 2>/dev/null || true
399
+ fi
400
+ if ! git -C "$sub_query_dir" cat-file -e "${ours_sha}^{commit}" 2>/dev/null \
401
+ || ! git -C "$sub_query_dir" cat-file -e "${theirs_sha}^{commit}" 2>/dev/null; then
402
+ return 1
403
+ fi
404
+
405
+ local chosen_sha=""
406
+ if [[ "$ours_sha" == "$theirs_sha" ]]; then
407
+ chosen_sha="$ours_sha"
408
+ elif git -C "$sub_query_dir" merge-base --is-ancestor "$ours_sha" "$theirs_sha" 2>/dev/null; then
409
+ chosen_sha="$theirs_sha"
410
+ elif git -C "$sub_query_dir" merge-base --is-ancestor "$theirs_sha" "$ours_sha" 2>/dev/null; then
411
+ chosen_sha="$ours_sha"
412
+ else
413
+ # Divergent histories; refuse.
414
+ return 1
415
+ fi
416
+
417
+ if ! git -C "$source_worktree_arg" update-index --cacheinfo "160000,${chosen_sha},${conflict_path}" >/dev/null 2>&1; then
418
+ return 1
419
+ fi
420
+
421
+ printf '%s' "$chosen_sha"
422
+ return 0
423
+ }
424
+
162
425
  if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
163
426
  echo "[agent-branch-finish] Not inside a git repository." >&2
164
427
  exit 1
165
428
  fi
166
429
 
167
430
  repo_root="$(git rev-parse --show-toplevel)"
431
+ finish_active_cwd="${GUARDEX_FINISH_ACTIVE_CWD:-$(pwd -P)}"
432
+ if [[ -d "$finish_active_cwd" ]]; then
433
+ finish_active_cwd="$(cd "$finish_active_cwd" && pwd -P)"
434
+ else
435
+ finish_active_cwd=""
436
+ fi
168
437
  # The physical cwd may be a subdirectory inside the source worktree. Cleanup
169
438
  # decisions need the enclosing worktree root, otherwise finishing from `src/`
170
439
  # can delete the caller's cwd and turn a successful merge into a false shell
@@ -178,6 +447,36 @@ else
178
447
  fi
179
448
  repo_common_root="$(cd "$common_git_dir/.." && pwd -P)"
180
449
 
450
+ resolve_same_repo_worktree_for_cwd() {
451
+ local active_cwd="$1"
452
+ [[ -n "$active_cwd" && -d "$active_cwd" ]] || return 0
453
+ git -C "$active_cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
454
+
455
+ local active_worktree=""
456
+ active_worktree="$(git -C "$active_cwd" rev-parse --show-toplevel 2>/dev/null || true)"
457
+ [[ -n "$active_worktree" ]] || return 0
458
+
459
+ local active_common_raw=""
460
+ local active_common_dir=""
461
+ active_common_raw="$(git -C "$active_worktree" rev-parse --git-common-dir 2>/dev/null || true)"
462
+ [[ -n "$active_common_raw" ]] || return 0
463
+ if [[ "$active_common_raw" == /* ]]; then
464
+ active_common_dir="$active_common_raw"
465
+ else
466
+ active_common_dir="${active_worktree}/${active_common_raw}"
467
+ fi
468
+ active_common_dir="$(cd "$active_common_dir" 2>/dev/null && pwd -P)" || return 0
469
+
470
+ if [[ "$active_common_dir" == "$common_git_dir" ]]; then
471
+ cd "$active_worktree" 2>/dev/null && pwd -P
472
+ fi
473
+ }
474
+
475
+ active_cwd_worktree="$(resolve_same_repo_worktree_for_cwd "$finish_active_cwd")"
476
+ if [[ -n "$active_cwd_worktree" ]]; then
477
+ current_worktree="$active_cwd_worktree"
478
+ fi
479
+
181
480
  if [[ -z "$SOURCE_BRANCH" ]]; then
182
481
  SOURCE_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
183
482
  fi
@@ -336,6 +635,22 @@ is_clean_worktree() {
336
635
  && git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"
337
636
  }
338
637
 
638
+ refresh_clean_base_worktree() {
639
+ local wt="$1"
640
+ [[ -z "$wt" || "$PUSH_ENABLED" -ne 1 ]] && return 0
641
+
642
+ if ! is_clean_worktree "$wt"; then
643
+ echo "[agent-branch-finish] Warning: local ${BASE_BRANCH} worktree is dirty; skipping 'git pull --ff-only origin ${BASE_BRANCH}' for ${wt}." >&2
644
+ return 0
645
+ fi
646
+
647
+ 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
648
+ echo "[agent-branch-finish] Refreshed local ${BASE_BRANCH} worktree with 'git pull --ff-only origin ${BASE_BRANCH}': ${wt}"
649
+ else
650
+ echo "[agent-branch-finish] Warning: failed to refresh local ${BASE_BRANCH} worktree with 'git pull --ff-only origin ${BASE_BRANCH}': ${wt}" >&2
651
+ fi
652
+ }
653
+
339
654
  remove_stale_source_probe_worktrees "$SOURCE_BRANCH"
340
655
  source_worktree="$(get_worktree_for_branch "$SOURCE_BRANCH")"
341
656
  created_source_probe=0
@@ -346,6 +661,7 @@ merge_completed=0
346
661
  merge_status="pr"
347
662
  direct_push_error=""
348
663
  pr_url=""
664
+ changed_submodule_push_done=0
349
665
 
350
666
  cleanup() {
351
667
  if [[ -n "$integration_worktree" && -d "$integration_worktree" ]]; then
@@ -432,20 +748,97 @@ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRA
432
748
 
433
749
  if ! git -C "$source_worktree" merge --no-commit --no-ff "origin/${BASE_BRANCH}" >/dev/null 2>&1; then
434
750
  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
751
 
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
752
+ if [[ "$AUTO_RESOLVE_MODE" != "none" && -n "$conflict_files" ]]; then
753
+ auto_resolve_unresolved=""
754
+ auto_resolve_resolved_state=""
755
+ auto_resolve_resolved_submodules=""
756
+ while IFS= read -r conflict_path; do
757
+ [[ -z "$conflict_path" ]] && continue
758
+ if path_matches_auto_resolve_safe_glob "$conflict_path"; then
759
+ if git -C "$source_worktree" checkout --theirs -- "$conflict_path" >/dev/null 2>&1 \
760
+ && git -C "$source_worktree" add -- "$conflict_path" >/dev/null 2>&1; then
761
+ auto_resolve_resolved_state+="${conflict_path}"$'\n'
762
+ continue
763
+ fi
764
+ fi
765
+ if [[ "$AUTO_RESOLVE_MODE" == "full" ]]; then
766
+ if chosen_sha="$(try_resolve_submodule_pointer_conflict "$repo_root" "$source_worktree" "$conflict_path")"; then
767
+ auto_resolve_resolved_submodules+="${conflict_path}@${chosen_sha}"$'\n'
768
+ continue
769
+ fi
770
+ fi
771
+ auto_resolve_unresolved+="${conflict_path}"$'\n'
442
772
  done <<< "$conflict_files"
773
+
774
+ if [[ -n "$auto_resolve_unresolved" ]]; then
775
+ git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
776
+ echo "[agent-branch-finish] --auto-resolve=${AUTO_RESOLVE_MODE}: some conflicts are outside the safe allowlist (or submodule histories diverge); aborting." >&2
777
+ echo "[agent-branch-finish] Unresolved conflicts:" >&2
778
+ while IFS= read -r unresolved_path; do
779
+ [[ -n "$unresolved_path" ]] && echo " - ${unresolved_path}" >&2
780
+ done <<< "$auto_resolve_unresolved"
781
+ echo "[agent-branch-finish] State-file allowlist (GUARDEX_FINISH_AUTO_RESOLVE_SAFE_GLOBS): ${AUTO_RESOLVE_SAFE_GLOBS_RAW}" >&2
782
+ if [[ "$AUTO_RESOLVE_MODE" != "full" ]]; then
783
+ echo "[agent-branch-finish] Submodule pointer auto-resolve requires --auto-resolve=full; not enabled for this run." >&2
784
+ fi
785
+ exit 1
786
+ fi
787
+
788
+ # Claim resolved paths so the pre-commit lock guard accepts the merge.
789
+ auto_resolve_claim_paths=()
790
+ while IFS= read -r resolved_path; do
791
+ [[ -n "$resolved_path" ]] && auto_resolve_claim_paths+=("$resolved_path")
792
+ done <<< "$auto_resolve_resolved_state"
793
+ while IFS= read -r resolved_entry; do
794
+ [[ -z "$resolved_entry" ]] && continue
795
+ auto_resolve_claim_paths+=("${resolved_entry%@*}")
796
+ done <<< "$auto_resolve_resolved_submodules"
797
+ if [[ "${#auto_resolve_claim_paths[@]}" -gt 0 ]]; then
798
+ run_guardex_cli locks claim --branch "$SOURCE_BRANCH" "${auto_resolve_claim_paths[@]}" >/dev/null 2>&1 || true
799
+ fi
800
+
801
+ auto_resolve_commit_msg="Merge origin/${BASE_BRANCH} into ${SOURCE_BRANCH} (gx --auto-resolve=${AUTO_RESOLVE_MODE}; state files -> base, submodule pointers fast-forwarded)"
802
+ if ! git -C "$source_worktree" commit -m "$auto_resolve_commit_msg" >/dev/null 2>&1; then
803
+ git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
804
+ 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
805
+ exit 1
806
+ fi
807
+
808
+ state_count=0
809
+ submod_count=0
810
+ [[ -n "$auto_resolve_resolved_state" ]] && state_count="$(printf '%s' "$auto_resolve_resolved_state" | grep -c '^[^[:space:]]')"
811
+ [[ -n "$auto_resolve_resolved_submodules" ]] && submod_count="$(printf '%s' "$auto_resolve_resolved_submodules" | grep -c '^[^[:space:]]')"
812
+ echo "[agent-branch-finish] --auto-resolve=${AUTO_RESOLVE_MODE}: resolved ${state_count} state-file conflict(s), ${submod_count} submodule pointer conflict(s)." >&2
813
+ if [[ -n "$auto_resolve_resolved_state" ]]; then
814
+ echo "[agent-branch-finish] State files (resolved to base):" >&2
815
+ while IFS= read -r resolved_path; do
816
+ [[ -n "$resolved_path" ]] && echo " - ${resolved_path}" >&2
817
+ done <<< "$auto_resolve_resolved_state"
818
+ fi
819
+ if [[ -n "$auto_resolve_resolved_submodules" ]]; then
820
+ echo "[agent-branch-finish] Submodule pointers (fast-forwarded):" >&2
821
+ while IFS= read -r resolved_entry; do
822
+ [[ -n "$resolved_entry" ]] && echo " - ${resolved_entry%@*} -> ${resolved_entry##*@}" >&2
823
+ done <<< "$auto_resolve_resolved_submodules"
824
+ fi
825
+ else
826
+ git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
827
+
828
+ echo "[agent-branch-finish] Preflight conflict detected between '${SOURCE_BRANCH}' and latest origin/${BASE_BRANCH}." >&2
829
+ if [[ -n "$conflict_files" ]]; then
830
+ echo "[agent-branch-finish] Conflicting files:" >&2
831
+ while IFS= read -r file; do
832
+ [[ -n "$file" ]] && echo " - ${file}" >&2
833
+ done <<< "$conflict_files"
834
+ fi
835
+ echo "[agent-branch-finish] Rebase/merge '${BASE_BRANCH}' into '${SOURCE_BRANCH}' and resolve conflicts before finishing." >&2
836
+ echo "[agent-branch-finish] Or rerun with --auto-resolve=safe (state files) or --auto-resolve=full (state files + fast-forward-able submodule pointers)." >&2
837
+ exit 1
443
838
  fi
444
- echo "[agent-branch-finish] Rebase/merge '${BASE_BRANCH}' into '${SOURCE_BRANCH}' and resolve conflicts before finishing." >&2
445
- exit 1
839
+ else
840
+ git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
446
841
  fi
447
-
448
- git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
449
842
  fi
450
843
 
451
844
  should_create_integration_helper=1
@@ -682,6 +1075,77 @@ maybe_auto_commit_parent_gitlink() {
682
1075
  echo "[agent-branch-finish] Parent gitlink auto-committed '${subrepo_rel}' in ${super_root}."
683
1076
  }
684
1077
 
1078
+ maybe_push_changed_submodule_branches() {
1079
+ local base_ref="${1:-}"
1080
+ local source_ref="${2:-}"
1081
+ local changed_path=""
1082
+ local gitlink_mode=""
1083
+ local gitlink_sha=""
1084
+ local submodule_dir=""
1085
+ local branch_name=""
1086
+ local candidate_branch=""
1087
+ local remote_name=""
1088
+ local push_output=""
1089
+
1090
+ if [[ "$PUSH_ENABLED" -ne 1 || "$changed_submodule_push_done" -eq 1 ]]; then
1091
+ return 0
1092
+ fi
1093
+ changed_submodule_push_done=1
1094
+ if [[ -z "$base_ref" || -z "$source_ref" ]]; then
1095
+ return 0
1096
+ fi
1097
+
1098
+ while IFS= read -r changed_path; do
1099
+ [[ -n "$changed_path" ]] || continue
1100
+
1101
+ gitlink_mode="$(git -C "$source_worktree" ls-tree "$source_ref" -- "$changed_path" | awk 'NR == 1 { print $1 }')"
1102
+ if [[ "$gitlink_mode" != "160000" ]]; then
1103
+ continue
1104
+ fi
1105
+ gitlink_sha="$(git -C "$source_worktree" ls-tree "$source_ref" -- "$changed_path" | awk 'NR == 1 { print $3 }')"
1106
+ if [[ -z "$gitlink_sha" ]]; then
1107
+ continue
1108
+ fi
1109
+
1110
+ submodule_dir="${source_worktree}/${changed_path}"
1111
+ if ! git -C "$submodule_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
1112
+ 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
1113
+ return 1
1114
+ fi
1115
+ if ! git -C "$submodule_dir" cat-file -e "${gitlink_sha}^{commit}" >/dev/null 2>&1; then
1116
+ 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
1117
+ return 1
1118
+ fi
1119
+
1120
+ branch_name="$(git -C "$submodule_dir" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
1121
+ if [[ -z "$branch_name" || "$branch_name" == "HEAD" ]] || ! git -C "$submodule_dir" merge-base --is-ancestor "$gitlink_sha" "$branch_name" >/dev/null 2>&1; then
1122
+ candidate_branch="$(git -C "$submodule_dir" for-each-ref --contains "$gitlink_sha" --format='%(refname:short)' refs/heads | head -n 1)"
1123
+ branch_name="$candidate_branch"
1124
+ fi
1125
+ if [[ -z "$branch_name" || "$branch_name" == "HEAD" ]]; then
1126
+ 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
1127
+ return 1
1128
+ fi
1129
+
1130
+ remote_name="$(git -C "$submodule_dir" config --get "branch.${branch_name}.remote" || true)"
1131
+ if [[ -z "$remote_name" ]]; then
1132
+ remote_name="origin"
1133
+ fi
1134
+ if ! git -C "$submodule_dir" remote get-url "$remote_name" >/dev/null 2>&1; then
1135
+ 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
1136
+ return 1
1137
+ fi
1138
+
1139
+ if push_output="$(git -C "$submodule_dir" push -u "$remote_name" "${branch_name}:${branch_name}" 2>&1)"; then
1140
+ echo "[agent-branch-finish] Pushed changed submodule '${changed_path}' branch '${branch_name}' to '${remote_name}' before parent finish."
1141
+ else
1142
+ echo "[agent-branch-finish] Changed submodule '${changed_path}' must be pushed before the parent branch can be finished." >&2
1143
+ [[ -n "$push_output" ]] && echo "$push_output" >&2
1144
+ return 1
1145
+ fi
1146
+ done < <(git -C "$source_worktree" diff --name-only "$base_ref" "$source_ref" -- 2>/dev/null || true)
1147
+ }
1148
+
685
1149
  wait_for_pr_merge() {
686
1150
  local deadline
687
1151
  deadline=$(( $(date +%s) + WAIT_TIMEOUT_SECONDS ))
@@ -752,6 +1216,7 @@ run_pr_flow() {
752
1216
  return 0
753
1217
  fi
754
1218
 
1219
+ maybe_push_changed_submodule_branches "$start_ref" "$SOURCE_BRANCH"
755
1220
  git -C "$source_worktree" push -u origin "$SOURCE_BRANCH"
756
1221
 
757
1222
  pr_title="$(git -C "$repo_root" log -1 --pretty=%s "$SOURCE_BRANCH" 2>/dev/null || true)"
@@ -760,14 +1225,39 @@ run_pr_flow() {
760
1225
  fi
761
1226
  pr_body="Automated by gx branch finish (PR flow)."
762
1227
 
763
- "$GH_BIN" pr create \
1228
+ pr_create_output=""
1229
+ if pr_create_output="$("$GH_BIN" pr create \
764
1230
  --base "$BASE_BRANCH" \
765
1231
  --head "$SOURCE_BRANCH" \
766
1232
  --title "$pr_title" \
767
- --body "$pr_body" >/dev/null 2>&1 || true
1233
+ --body "$pr_body" 2>&1)"; then
1234
+ :
1235
+ else
1236
+ # Idempotent: a PR already opened for this head is fine — fall through
1237
+ # to `gh pr view` so we still capture the URL. Anything else is a real
1238
+ # failure and the user needs to see it.
1239
+ if ! grep -qiE 'already exists|a pull request for branch' <<<"$pr_create_output"; then
1240
+ echo "[agent-branch-finish] gh pr create failed:" >&2
1241
+ echo "${pr_create_output}" >&2
1242
+ fi
1243
+ fi
768
1244
 
769
1245
  pr_url="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json url --jq '.url' 2>/dev/null || true)"
770
1246
 
1247
+ if [[ -z "$pr_url" ]]; then
1248
+ echo "[agent-branch-finish] No PR found for '${SOURCE_BRANCH}' after gh pr create; cannot proceed with PR merge." >&2
1249
+ if [[ -n "$pr_create_output" ]]; then
1250
+ echo "[agent-branch-finish] Last gh pr create output:" >&2
1251
+ echo "${pr_create_output}" >&2
1252
+ fi
1253
+ return 1
1254
+ fi
1255
+ echo "[agent-branch-finish] PR URL: ${pr_url}" >&2
1256
+
1257
+ # Pre-flight already passed by the time we reach the PR; promote any
1258
+ # existing draft so the budget-friendly CI gate fires once.
1259
+ maybe_auto_promote_pr "$pr_url"
1260
+
771
1261
  merge_output=""
772
1262
  if merge_output="$("$GH_BIN" pr merge "$SOURCE_BRANCH" --squash --delete-branch 2>&1)"; then
773
1263
  return 0
@@ -799,7 +1289,11 @@ run_pr_flow() {
799
1289
  }
800
1290
 
801
1291
  if [[ "$PUSH_ENABLED" -eq 1 ]]; then
1292
+ if ! run_preflight "$source_worktree"; then
1293
+ exit 1
1294
+ fi
802
1295
  if [[ "$MERGE_MODE" != "pr" ]]; then
1296
+ maybe_push_changed_submodule_branches "$start_ref" "$SOURCE_BRANCH"
803
1297
  if ! direct_push_output="$(git -C "$integration_worktree" push origin "HEAD:${BASE_BRANCH}" 2>&1)"; then
804
1298
  direct_push_error="$direct_push_output"
805
1299
  merge_completed=0
@@ -847,9 +1341,7 @@ fi
847
1341
  run_guardex_cli locks release --branch "$SOURCE_BRANCH" >/dev/null 2>&1 || true
848
1342
 
849
1343
  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
1344
+ refresh_clean_base_worktree "$base_worktree"
853
1345
  maybe_auto_commit_parent_gitlink "$base_worktree"
854
1346
 
855
1347
  # Pivot out of the agent worktree before prune calls that may remove it.
@@ -861,6 +1353,10 @@ pivot_to_repo_root_before_prune() {
861
1353
  fi
862
1354
  }
863
1355
 
1356
+ run_guardex_prune() {
1357
+ GUARDEX_PRUNE_ACTIVE_CWD="$finish_active_cwd" run_guardex_cli worktree prune "$@"
1358
+ }
1359
+
864
1360
  if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
865
1361
  if [[ "$source_worktree" == "$repo_root" ]]; then
866
1362
  if is_clean_worktree "$source_worktree"; then
@@ -871,7 +1367,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
871
1367
  git -C "$source_worktree" checkout --detach >/dev/null 2>&1 || true
872
1368
  fi
873
1369
  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
1370
+ refresh_clean_base_worktree "$source_worktree"
875
1371
  fi
876
1372
  fi
877
1373
  elif [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${agent_worktree_root}"/* ]]; then
@@ -906,7 +1402,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
906
1402
  fi
907
1403
 
908
1404
  pivot_to_repo_root_before_prune
909
- if ! run_guardex_cli worktree prune "${prune_args[@]}"; then
1405
+ if ! run_guardex_prune "${prune_args[@]}"; then
910
1406
  echo "[agent-branch-finish] Warning: automatic worktree prune failed." >&2
911
1407
  echo "[agent-branch-finish] You can run manual cleanup: gx cleanup --base ${BASE_BRANCH}" >&2
912
1408
  fi
@@ -920,7 +1416,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
920
1416
  fi
921
1417
  else
922
1418
  pivot_to_repo_root_before_prune
923
- if ! run_guardex_cli worktree prune --base "$BASE_BRANCH"; then
1419
+ if ! run_guardex_prune --base "$BASE_BRANCH"; then
924
1420
  echo "[agent-branch-finish] Warning: temporary worktree prune failed." >&2
925
1421
  fi
926
1422