diff --git a/openspec/changes/agent-codex-fix-temp-helper-worktree-cleanup-2026-04-23-11-56/proposal.md b/openspec/changes/agent-codex-fix-temp-helper-worktree-cleanup-2026-04-23-11-56/proposal.md index 240b883..33282d3 100644 --- a/openspec/changes/agent-codex-fix-temp-helper-worktree-cleanup-2026-04-23-11-56/proposal.md +++ b/openspec/changes/agent-codex-fix-temp-helper-worktree-cleanup-2026-04-23-11-56/proposal.md @@ -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` diff --git a/openspec/changes/agent-codex-fix-temp-helper-worktree-cleanup-2026-04-23-11-56/specs/temp-helper-worktree-cleanup/spec.md b/openspec/changes/agent-codex-fix-temp-helper-worktree-cleanup-2026-04-23-11-56/specs/temp-helper-worktree-cleanup/spec.md index b06b02c..4fb302f 100644 --- a/openspec/changes/agent-codex-fix-temp-helper-worktree-cleanup-2026-04-23-11-56/specs/temp-helper-worktree-cleanup/spec.md +++ b/openspec/changes/agent-codex-fix-temp-helper-worktree-cleanup-2026-04-23-11-56/specs/temp-helper-worktree-cleanup/spec.md @@ -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 diff --git a/openspec/changes/agent-codex-fix-temp-helper-worktree-cleanup-2026-04-23-11-56/tasks.md b/openspec/changes/agent-codex-fix-temp-helper-worktree-cleanup-2026-04-23-11-56/tasks.md index d859cbe..69f5beb 100644 --- a/openspec/changes/agent-codex-fix-temp-helper-worktree-cleanup-2026-04-23-11-56/tasks.md +++ b/openspec/changes/agent-codex-fix-temp-helper-worktree-cleanup-2026-04-23-11-56/tasks.md @@ -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 diff --git a/scripts/agent-branch-finish.sh b/scripts/agent-branch-finish.sh index 48e3ff6..88d2604 100755 --- a/scripts/agent-branch-finish.sh +++ b/scripts/agent-branch-finish.sh @@ -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 @@ -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 @@ -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" diff --git a/templates/scripts/agent-branch-finish.sh b/templates/scripts/agent-branch-finish.sh index 48e3ff6..88d2604 100755 --- a/templates/scripts/agent-branch-finish.sh +++ b/templates/scripts/agent-branch-finish.sh @@ -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 @@ -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 @@ -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" diff --git a/test/finish.test.js b/test/finish.test.js index 37a600c..09fb408 100644 --- a/test/finish.test.js +++ b/test/finish.test.js @@ -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);