Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
- `gx branch finish` creates `__source-probe-*` and `__integrate-*` helper worktrees under `.omx/agent-worktrees` / `.omc/agent-worktrees`, so VS Code surfaces them beside real agent lanes.
- The finish flow removes temporary integration worktrees but can still leave stale `__agent_integrate_*` refs behind, which stacks up noisy helper branches after repeated doctor/finish runs.
- Repo-level Git scan ignores currently cover only durable agent worktree roots, so moving helper worktrees into a dedicated internal temp root also needs settings parity.
- Even after moving helpers under `.tmp-worktrees`, `gx branch finish --via-pr` still creates a throwaway `__integrate-*` worktree before it falls into the PR path. That keeps opening noisy temporary repos in Source Control even when direct integration is intentionally disabled.

## What changes

- Move temporary finish helpers into `.omx/.tmp-worktrees` and `.omc/.tmp-worktrees` instead of the user-visible agent worktree roots.
- Delete temporary integration refs directly during finish cleanup and sweep any older stale temp refs in `gx cleanup`.
- Extend managed VS Code repo-scan ignores and focused regressions for the new temp-helper placement and stale-temp-branch cleanup.
- Skip creating `__integrate-*` helper worktrees entirely when finish runs in explicit PR-only mode (`--mode pr` / `--via-pr`) and prove it with a focused regression.

## Scope

- Affected runtime/scripts: `scripts/agent-branch-finish.sh`, `scripts/agent-worktree-prune.sh`
- Affected template mirrors: `templates/scripts/*`
- Affected config/test surface: `src/context.js`, `test/finish.test.js`, `test/worktree.test.js`, `test/setup.test.js`
- Follow-up runtime/scripts in this extension: `scripts/agent-branch-finish.sh`, `templates/scripts/agent-branch-finish.sh`
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,16 @@ Guardex-managed VS Code repo scan ignores SHALL include `.omx/.tmp-worktrees` an
- **WHEN** `gx setup` or `gx doctor` refreshes the managed settings
- **THEN** the existing user-defined entries remain
- **AND** the resulting ignore list includes `.omx/.tmp-worktrees`, `**/.omx/.tmp-worktrees`, `.omc/.tmp-worktrees`, and `**/.omc/.tmp-worktrees`

### Requirement: explicit PR mode skips temporary integration helpers

`gx branch finish` SHALL skip creating temporary `__integrate-*` worktrees and refs when the operator explicitly selects PR-only finish mode (`--mode pr` or `--via-pr`).

#### Scenario: PR-only finish merges without an integration helper worktree

- **GIVEN** an agent branch is finished with `--mode pr` or `--via-pr`
- **AND** the source branch already has a normal attached worktree
- **WHEN** `gx branch finish` prepares the PR flow
- **THEN** it does not create a temporary `__integrate-*` worktree
- **AND** it does not create a temporary `__agent_integrate_*` ref
- **AND** the PR flow still pushes the source branch and opens or merges the PR
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@

- [x] 2.1 Update finish/worktree regressions for `.tmp-worktrees` helper paths and stale temp-ref cleanup.
- [x] 2.2 Update setup expectations for the expanded repo-scan ignore list.
- [x] 2.3 Add a PR-mode finish regression proving `--via-pr` skips `__integrate-*` helper creation.

## 3. Implementation

- [x] 3.1 Move temporary finish helper worktrees into runtime-scoped `.tmp-worktrees` roots.
- [x] 3.2 Delete temporary integration refs at finish exit and sweep stale helper refs in `gx cleanup`.
- [x] 3.3 Extend repo scan ignore settings for temporary helper roots.
- [x] 3.4 Skip temporary integration helper creation in explicit PR-only finish mode.

## 4. Verification

- [x] 4.1 Run focused tests for finish/prune/setup behavior.
- [x] 4.2 Run `openspec validate agent-codex-fix-temp-helper-worktree-cleanup-2026-04-23-11-56 --type change --strict`.
- [x] 4.3 Re-run focused finish regressions covering explicit PR mode after the helper-skip patch.

## 5. Cleanup

Expand Down
57 changes: 33 additions & 24 deletions scripts/agent-branch-finish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ created_source_probe=0
source_probe_path=""
integration_worktree=""
integration_branch=""
merge_completed=0
merge_status="pr"
direct_push_error=""
pr_url=""

cleanup() {
if [[ -n "$integration_worktree" && -d "$integration_worktree" ]]; then
Expand Down Expand Up @@ -358,22 +362,6 @@ if [[ "$should_require_sync" -eq 1 ]] && git -C "$repo_root" show-ref --verify -
fi
fi

integration_stamp="$(date +%Y%m%d-%H%M%S)"
integration_worktree_base="${temp_worktree_root}/__integrate-${BASE_BRANCH//\//__}-${integration_stamp}"
integration_branch_base="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
integration_worktree="$integration_worktree_base"
integration_branch="$integration_branch_base"
integration_suffix=1
while [[ -e "$integration_worktree" ]] || git -C "$repo_root" show-ref --verify --quiet "refs/heads/${integration_branch}"; do
integration_worktree="${integration_worktree_base}-${integration_suffix}"
integration_branch="${integration_branch_base}_${integration_suffix}"
integration_suffix=$((integration_suffix + 1))
done
mkdir -p "$(dirname "$integration_worktree")"

git -C "$repo_root" worktree add "$integration_worktree" "$start_ref" >/dev/null
git -C "$integration_worktree" checkout -b "$integration_branch" >/dev/null

if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
git -C "$source_worktree" fetch origin "$BASE_BRANCH" --quiet

Expand All @@ -395,16 +383,37 @@ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRA
git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
fi

if ! git -C "$integration_worktree" merge --no-ff --no-edit "$SOURCE_BRANCH"; then
echo "[agent-branch-finish] Merge conflict detected while merging '${SOURCE_BRANCH}' into '${BASE_BRANCH}'." >&2
git -C "$integration_worktree" merge --abort >/dev/null 2>&1 || true
exit 1
should_create_integration_helper=1
if [[ "$MERGE_MODE" == "pr" && "$PUSH_ENABLED" -eq 1 ]]; then
should_create_integration_helper=0
fi

merge_completed=1
merge_status="direct"
direct_push_error=""
pr_url=""
if [[ "$should_create_integration_helper" -eq 1 ]]; then
integration_stamp="$(date +%Y%m%d-%H%M%S)"
integration_worktree_base="${temp_worktree_root}/__integrate-${BASE_BRANCH//\//__}-${integration_stamp}"
integration_branch_base="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
integration_worktree="$integration_worktree_base"
integration_branch="$integration_branch_base"
integration_suffix=1
while [[ -e "$integration_worktree" ]] || git -C "$repo_root" show-ref --verify --quiet "refs/heads/${integration_branch}"; do
integration_worktree="${integration_worktree_base}-${integration_suffix}"
integration_branch="${integration_branch_base}_${integration_suffix}"
integration_suffix=$((integration_suffix + 1))
done
mkdir -p "$(dirname "$integration_worktree")"

git -C "$repo_root" worktree add "$integration_worktree" "$start_ref" >/dev/null
git -C "$integration_worktree" checkout -b "$integration_branch" >/dev/null

if ! git -C "$integration_worktree" merge --no-ff --no-edit "$SOURCE_BRANCH"; then
echo "[agent-branch-finish] Merge conflict detected while merging '${SOURCE_BRANCH}' into '${BASE_BRANCH}'." >&2
git -C "$integration_worktree" merge --abort >/dev/null 2>&1 || true
exit 1
fi

merge_completed=1
merge_status="direct"
fi

is_local_branch_delete_error() {
local output="$1"
Expand Down
57 changes: 33 additions & 24 deletions templates/scripts/agent-branch-finish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ created_source_probe=0
source_probe_path=""
integration_worktree=""
integration_branch=""
merge_completed=0
merge_status="pr"
direct_push_error=""
pr_url=""

cleanup() {
if [[ -n "$integration_worktree" && -d "$integration_worktree" ]]; then
Expand Down Expand Up @@ -358,22 +362,6 @@ if [[ "$should_require_sync" -eq 1 ]] && git -C "$repo_root" show-ref --verify -
fi
fi

integration_stamp="$(date +%Y%m%d-%H%M%S)"
integration_worktree_base="${temp_worktree_root}/__integrate-${BASE_BRANCH//\//__}-${integration_stamp}"
integration_branch_base="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
integration_worktree="$integration_worktree_base"
integration_branch="$integration_branch_base"
integration_suffix=1
while [[ -e "$integration_worktree" ]] || git -C "$repo_root" show-ref --verify --quiet "refs/heads/${integration_branch}"; do
integration_worktree="${integration_worktree_base}-${integration_suffix}"
integration_branch="${integration_branch_base}_${integration_suffix}"
integration_suffix=$((integration_suffix + 1))
done
mkdir -p "$(dirname "$integration_worktree")"

git -C "$repo_root" worktree add "$integration_worktree" "$start_ref" >/dev/null
git -C "$integration_worktree" checkout -b "$integration_branch" >/dev/null

if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
git -C "$source_worktree" fetch origin "$BASE_BRANCH" --quiet

Expand All @@ -395,16 +383,37 @@ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRA
git -C "$source_worktree" merge --abort >/dev/null 2>&1 || true
fi

if ! git -C "$integration_worktree" merge --no-ff --no-edit "$SOURCE_BRANCH"; then
echo "[agent-branch-finish] Merge conflict detected while merging '${SOURCE_BRANCH}' into '${BASE_BRANCH}'." >&2
git -C "$integration_worktree" merge --abort >/dev/null 2>&1 || true
exit 1
should_create_integration_helper=1
if [[ "$MERGE_MODE" == "pr" && "$PUSH_ENABLED" -eq 1 ]]; then
should_create_integration_helper=0
fi

merge_completed=1
merge_status="direct"
direct_push_error=""
pr_url=""
if [[ "$should_create_integration_helper" -eq 1 ]]; then
integration_stamp="$(date +%Y%m%d-%H%M%S)"
integration_worktree_base="${temp_worktree_root}/__integrate-${BASE_BRANCH//\//__}-${integration_stamp}"
integration_branch_base="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
integration_worktree="$integration_worktree_base"
integration_branch="$integration_branch_base"
integration_suffix=1
while [[ -e "$integration_worktree" ]] || git -C "$repo_root" show-ref --verify --quiet "refs/heads/${integration_branch}"; do
integration_worktree="${integration_worktree_base}-${integration_suffix}"
integration_branch="${integration_branch_base}_${integration_suffix}"
integration_suffix=$((integration_suffix + 1))
done
mkdir -p "$(dirname "$integration_worktree")"

git -C "$repo_root" worktree add "$integration_worktree" "$start_ref" >/dev/null
git -C "$integration_worktree" checkout -b "$integration_branch" >/dev/null

if ! git -C "$integration_worktree" merge --no-ff --no-edit "$SOURCE_BRANCH"; then
echo "[agent-branch-finish] Merge conflict detected while merging '${SOURCE_BRANCH}' into '${BASE_BRANCH}'." >&2
git -C "$integration_worktree" merge --abort >/dev/null 2>&1 || true
exit 1
fi

merge_completed=1
merge_status="direct"
fi

is_local_branch_delete_error() {
local output="$1"
Expand Down
73 changes: 73 additions & 0 deletions test/finish.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,79 @@ exit 1
});


test('agent-branch-finish pr mode skips temporary integration helper creation', () => {
const repoDir = initRepo();
seedCommit(repoDir);
attachOriginRemote(repoDir);

let result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir);
assert.equal(result.status, 0, result.stderr || result.stdout);
result = runCmd('git', ['add', '.'], repoDir);
assert.equal(result.status, 0, result.stderr);
result = runCmd('git', ['commit', '-m', 'apply gx setup'], repoDir, {
ALLOW_COMMIT_ON_PROTECTED_BRANCH: '1',
});
assert.equal(result.status, 0, result.stderr);
result = runCmd('git', ['push', 'origin', 'dev'], repoDir);
assert.equal(result.status, 0, result.stderr);

result = runCmd('git', ['checkout', '-b', 'agent/test-pr-skip-integrate-helper'], repoDir);
assert.equal(result.status, 0, result.stderr);
commitFile(repoDir, 'agent-pr-skip-integrate.txt', 'agent change\n', 'agent change');

const { fakePath: fakeGhPath } = createFakeGhScript(`
if [[ "$1" == "pr" && "$2" == "create" ]]; then
exit 0
fi
if [[ "$1" == "pr" && "$2" == "view" ]]; then
if [[ " $* " == *" --json url "* ]]; then
echo "https://example.test/pr/skip-integrate"
exit 0
fi
echo "unexpected gh pr view args: $*" >&2
exit 1
fi
if [[ "$1" == "pr" && "$2" == "merge" ]]; then
exit 0
fi
echo "unexpected gh args: $*" >&2
exit 1
`);
const realGit = runCmd('bash', ['-lc', 'command -v git'], repoDir);
assert.equal(realGit.status, 0, realGit.stderr || realGit.stdout);
const realGitPath = realGit.stdout.trim();
const { fakeBin } = createFakeBin('git', `
real_git="${realGitPath}"
if [[ "$1" == "-C" && "$3" == "worktree" && "$4" == "add" ]]; then
case "$5" in
*"/.omx/.tmp-worktrees/__integrate-"*|*"/.omc/.tmp-worktrees/__integrate-"*)
echo "unexpected integration helper worktree in PR mode: $5" >&2
exit 99
;;
esac
fi
"$real_git" "$@"
`);

const finish = runBranchFinish(
['--branch', 'agent/test-pr-skip-integrate-helper', '--mode', 'pr', '--cleanup'],
repoDir,
{
GUARDEX_GH_BIN: fakeGhPath,
PATH: `${fakeBin}:${process.env.PATH || ''}`,
},
);
assert.equal(finish.status, 0, finish.stderr || finish.stdout);
assert.match(
finish.stdout,
/Merged 'agent\/test-pr-skip-integrate-helper' into 'dev' via pr flow and cleaned source branch\/worktree\./,
);

result = runCmd('git', ['branch', '--list', '__agent_integrate_dev_*'], repoDir);
assert.equal(result.stdout.trim(), '', 'temporary integrate branches should not be created in PR mode');
});


test('agent-branch-finish cleanup succeeds when remote delete reports an already-removed branch', () => {
const repoDir = initRepo();
seedCommit(repoDir);
Expand Down
Loading