Skip to content

Commit 32a4667

Browse files
NagyViktNagyVikt
andauthored
Prevent cwd loss during branch-finish prune (#424)
The prior cleanup fix only pivoted before the cleanup prune path, leaving the no-cleanup temporary prune able to spawn gx from an agent worktree cwd that may have disappeared during nested finish cleanup. This moves the cwd pivot behind a shared helper, calls it before both prune paths, restores scripts/template parity, and mirrors the legacy frontend script copy. Constraint: gx worktree prune may run after merge-side cleanup or parent gitlink handling changes the active worktree state. Rejected: stderr filtering in wrapper scripts | it would hide the warning without fixing the package behavior. Confidence: high Scope-risk: narrow Directive: Keep branch-finish prune calls preceded by the cwd pivot when running from active agent worktrees. Tested: bash -n scripts/agent-branch-finish.sh; bash -n templates/scripts/agent-branch-finish.sh; bash -n frontend/scripts/agent-branch-finish.sh; node --test --test-name-pattern 'agent-branch-finish pivots' test/metadata.test.js; node --test test/finish.test.js; openspec validate --specs Not-tested: full metadata.test.js remains blocked by unrelated vscode/guardex-active-agents/extension.js template drift Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
1 parent 166d6c1 commit 32a4667

4 files changed

Lines changed: 51 additions & 11 deletions

File tree

frontend/scripts/agent-branch-finish.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,15 @@ if [[ -n "$base_worktree" ]] && is_clean_worktree "$base_worktree" && [[ "$PUSH_
526526
git -C "$base_worktree" pull --ff-only origin "$BASE_BRANCH" >/dev/null 2>&1 || true
527527
fi
528528

529+
# Pivot out of the agent worktree before prune calls that may remove it.
530+
# Without this, subprocess spawns can fail with ENOENT uv_cwd after cwd
531+
# disappears even when the merge succeeded.
532+
pivot_to_repo_root_before_prune() {
533+
if [[ "$current_worktree" == "$source_worktree" && "$source_worktree" == "${agent_worktree_root}"/* ]]; then
534+
cd "$repo_root" 2>/dev/null || true
535+
fi
536+
}
537+
529538
if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
530539
if [[ "$source_worktree" == "$repo_root" ]]; then
531540
if is_clean_worktree "$source_worktree"; then
@@ -560,6 +569,8 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
560569
if [[ "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
561570
prune_args+=(--delete-remote-branches)
562571
fi
572+
573+
pivot_to_repo_root_before_prune
563574
if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then
564575
echo "[agent-branch-finish] Warning: automatic worktree prune failed." >&2
565576
echo "[agent-branch-finish] You can run manual cleanup: bash scripts/agent-worktree-prune.sh --base ${BASE_BRANCH} --delete-branches" >&2
@@ -573,6 +584,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
573584
fi
574585
else
575586
if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then
587+
pivot_to_repo_root_before_prune
576588
if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" --base "$BASE_BRANCH"; then
577589
echo "[agent-branch-finish] Warning: temporary worktree prune failed." >&2
578590
fi

scripts/agent-branch-finish.sh

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,15 @@ if [[ -n "$base_worktree" ]] && is_clean_worktree "$base_worktree" && [[ "$PUSH_
787787
fi
788788
maybe_auto_commit_parent_gitlink "$base_worktree"
789789

790+
# Pivot out of the agent worktree before prune calls that may remove it.
791+
# Without this, subprocess spawns can fail with ENOENT uv_cwd after cwd
792+
# disappears even when the merge succeeded.
793+
pivot_to_repo_root_before_prune() {
794+
if [[ "$current_worktree" == "$source_worktree" && "$source_worktree" == "${agent_worktree_root}"/* ]]; then
795+
cd "$repo_root" 2>/dev/null || true
796+
fi
797+
}
798+
790799
if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
791800
if [[ "$source_worktree" == "$repo_root" ]]; then
792801
if is_clean_worktree "$source_worktree"; then
@@ -831,16 +840,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
831840
prune_args+=(--delete-remote-branches)
832841
fi
833842

834-
# Pivot out of the agent worktree before the prune may remove it. The prune
835-
# subprocess inherits cwd=repo_root from the CLI caller, so its own active
836-
# cwd check can't see that our shell is sitting in the worktree it is about
837-
# to delete. Without this pivot the directory disappears mid-call, every
838-
# subsequent subprocess spawn fails with ENOENT uv_cwd, and `set -e` flips
839-
# the script exit code to 1 even though the merge succeeded.
840-
if [[ "$current_worktree" == "$source_worktree" && "$source_worktree" == "${agent_worktree_root}"/* ]]; then
841-
cd "$repo_root" 2>/dev/null || true
842-
fi
843-
843+
pivot_to_repo_root_before_prune
844844
if ! run_guardex_cli worktree prune "${prune_args[@]}"; then
845845
echo "[agent-branch-finish] Warning: automatic worktree prune failed." >&2
846846
echo "[agent-branch-finish] You can run manual cleanup: gx cleanup --base ${BASE_BRANCH}" >&2
@@ -852,6 +852,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
852852
echo "[agent-branch-finish] Leave this directory, then run: gx cleanup --base ${BASE_BRANCH}" >&2
853853
fi
854854
else
855+
pivot_to_repo_root_before_prune
855856
if ! run_guardex_cli worktree prune --base "$BASE_BRANCH"; then
856857
echo "[agent-branch-finish] Warning: temporary worktree prune failed." >&2
857858
fi

templates/scripts/agent-branch-finish.sh

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,15 @@ if [[ -n "$base_worktree" ]] && is_clean_worktree "$base_worktree" && [[ "$PUSH_
787787
fi
788788
maybe_auto_commit_parent_gitlink "$base_worktree"
789789

790+
# Pivot out of the agent worktree before prune calls that may remove it.
791+
# Without this, subprocess spawns can fail with ENOENT uv_cwd after cwd
792+
# disappears even when the merge succeeded.
793+
pivot_to_repo_root_before_prune() {
794+
if [[ "$current_worktree" == "$source_worktree" && "$source_worktree" == "${agent_worktree_root}"/* ]]; then
795+
cd "$repo_root" 2>/dev/null || true
796+
fi
797+
}
798+
790799
if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
791800
if [[ "$source_worktree" == "$repo_root" ]]; then
792801
if is_clean_worktree "$source_worktree"; then
@@ -830,17 +839,20 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
830839
if [[ "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
831840
prune_args+=(--delete-remote-branches)
832841
fi
842+
843+
pivot_to_repo_root_before_prune
833844
if ! run_guardex_cli worktree prune "${prune_args[@]}"; then
834845
echo "[agent-branch-finish] Warning: automatic worktree prune failed." >&2
835846
echo "[agent-branch-finish] You can run manual cleanup: gx cleanup --base ${BASE_BRANCH}" >&2
836847
fi
837848

838849
echo "[agent-branch-finish] Merged '${SOURCE_BRANCH}' into '${BASE_BRANCH}' via ${merge_status} flow and cleaned source branch/worktree."
839-
if [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${agent_worktree_root}"/* ]]; then
850+
if [[ "$source_worktree" == "$current_worktree" && "$source_worktree" == "${agent_worktree_root}"/* && -d "$source_worktree" ]]; then
840851
echo "[agent-branch-finish] Current worktree '${source_worktree}' still exists because it is the active shell cwd." >&2
841852
echo "[agent-branch-finish] Leave this directory, then run: gx cleanup --base ${BASE_BRANCH}" >&2
842853
fi
843854
else
855+
pivot_to_repo_root_before_prune
844856
if ! run_guardex_cli worktree prune --base "$BASE_BRANCH"; then
845857
echo "[agent-branch-finish] Warning: temporary worktree prune failed." >&2
846858
fi

test/metadata.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,21 @@ test('critical runtime helper scripts and active-agents sources stay in sync wit
232232
}
233233
});
234234

235+
test('agent-branch-finish pivots out of active agent cwd before every prune path', () => {
236+
const script = fs.readFileSync(path.join(repoRoot, 'scripts', 'agent-branch-finish.sh'), 'utf8');
237+
238+
assert.match(script, /pivot_to_repo_root_before_prune\(\) \{\n\s+if \[\[ "\$current_worktree" == "\$source_worktree"/);
239+
assert.match(script, /cd "\$repo_root" 2>\/dev\/null \|\| true/);
240+
assert.match(
241+
script,
242+
/pivot_to_repo_root_before_prune\n\s+if ! run_guardex_cli worktree prune "\$\{prune_args\[@\]\}"; then/,
243+
);
244+
assert.match(
245+
script,
246+
/else\n\s+pivot_to_repo_root_before_prune\n\s+if ! run_guardex_cli worktree prune --base "\$BASE_BRANCH"; then/,
247+
);
248+
});
249+
235250
test('thin CLI entrypoint delegates to src/cli runtime', () => {
236251
const entryPath = path.join(repoRoot, 'bin', 'multiagent-safety.js');
237252
const entrySource = fs.readFileSync(entryPath, 'utf8');

0 commit comments

Comments
 (0)