diff --git a/README.md b/README.md index dc4faef..9b28310 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ gx sync # continuously monitor open PRs targeting current branch and dispatch codex-agent review/merge tasks bash scripts/review-bot-watch.sh --interval 30 -# cleanup merged agent branches/worktrees +# cleanup merged agent branches and hide clean stale agent worktrees gx cleanup # scan/report @@ -239,6 +239,12 @@ npm pack --dry-run ## Release notes +### v5.0.6 + +- `gx cleanup` and auto-finish cleanup now prune clean agent worktrees by default, so VS Code Source Control focuses on your local branch plus worktrees with active changes. +- Added `gx cleanup --keep-clean-worktrees` to opt out and keep clean worktrees visible. +- Bumped package version from `5.0.5` to `5.0.6` for the next npm publish. + ### v5.0.5 - Bumped package version from `5.0.4` to `5.0.5` so npm publish can proceed with the next patch release. diff --git a/bin/multiagent-safety.js b/bin/multiagent-safety.js index 8ae36aa..f6ae168 100755 --- a/bin/multiagent-safety.js +++ b/bin/multiagent-safety.js @@ -1851,6 +1851,7 @@ function parseCleanupArgs(rawArgs) { dryRun: false, forceDirty: false, keepRemote: false, + keepCleanWorktrees: false, }; for (let index = 0; index < rawArgs.length; index += 1) { @@ -1894,6 +1895,10 @@ function parseCleanupArgs(rawArgs) { options.keepRemote = true; continue; } + if (arg === '--keep-clean-worktrees') { + options.keepCleanWorktrees = true; + continue; + } throw new Error(`Unknown option: ${arg}`); } @@ -3029,6 +3034,9 @@ function cleanup(rawArgs) { if (options.dryRun) { args.push('--dry-run'); } + if (!options.keepCleanWorktrees) { + args.push('--only-dirty-worktrees'); + } args.push('--delete-branches'); if (!options.keepRemote) { args.push('--delete-remote-branches'); diff --git a/package-lock.json b/package-lock.json index f8cdf5f..e26eba9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imdeadpool/guardex", - "version": "5.0.5", + "version": "5.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imdeadpool/guardex", - "version": "5.0.5", + "version": "5.0.6", "license": "MIT", "bin": { "guardex": "bin/multiagent-safety.js", diff --git a/package.json b/package.json index d86b82d..46700bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imdeadpool/guardex", - "version": "5.0.5", + "version": "5.0.6", "description": "GuardeX: the Guardian T-Rex for your repo, with hardened multi-agent git guardrails.", "license": "MIT", "preferGlobal": true, diff --git a/scripts/agent-branch-finish.sh b/scripts/agent-branch-finish.sh index c151729..3ec071e 100755 --- a/scripts/agent-branch-finish.sh +++ b/scripts/agent-branch-finish.sh @@ -545,7 +545,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then fi if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then - prune_args=(--base "$BASE_BRANCH" --delete-branches) + prune_args=(--base "$BASE_BRANCH" --only-dirty-worktrees --delete-branches) if [[ "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then prune_args+=(--delete-remote-branches) fi diff --git a/scripts/agent-worktree-prune.sh b/scripts/agent-worktree-prune.sh index 7a08c28..ee82df1 100755 --- a/scripts/agent-worktree-prune.sh +++ b/scripts/agent-worktree-prune.sh @@ -7,6 +7,7 @@ DRY_RUN=0 FORCE_DIRTY=0 DELETE_BRANCHES=0 DELETE_REMOTE_BRANCHES=0 +ONLY_DIRTY_WORKTREES=0 TARGET_BRANCH="" if [[ -n "$BASE_BRANCH" ]]; then @@ -36,13 +37,17 @@ while [[ $# -gt 0 ]]; do DELETE_REMOTE_BRANCHES=1 shift ;; + --only-dirty-worktrees) + ONLY_DIRTY_WORKTREES=1 + shift + ;; --branch) TARGET_BRANCH="${2:-}" shift 2 ;; *) echo "[agent-worktree-prune] Unknown argument: $1" >&2 - echo "Usage: $0 [--base ] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--branch ]" >&2 + echo "Usage: $0 [--base ] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch ]" >&2 exit 1 ;; esac @@ -165,6 +170,8 @@ process_entry() { if [[ "$DELETE_BRANCHES" -eq 1 ]]; then remove_reason="merged-agent-branch" fi + elif [[ "$ONLY_DIRTY_WORKTREES" -eq 1 ]] && is_clean_worktree "$wt"; then + remove_reason="clean-agent-worktree" fi elif [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]; then remove_reason="temporary-worktree" diff --git a/templates/scripts/agent-branch-finish.sh b/templates/scripts/agent-branch-finish.sh index c151729..3ec071e 100755 --- a/templates/scripts/agent-branch-finish.sh +++ b/templates/scripts/agent-branch-finish.sh @@ -545,7 +545,7 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then fi if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then - prune_args=(--base "$BASE_BRANCH" --delete-branches) + prune_args=(--base "$BASE_BRANCH" --only-dirty-worktrees --delete-branches) if [[ "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then prune_args+=(--delete-remote-branches) fi diff --git a/templates/scripts/agent-worktree-prune.sh b/templates/scripts/agent-worktree-prune.sh index 7a08c28..ee82df1 100644 --- a/templates/scripts/agent-worktree-prune.sh +++ b/templates/scripts/agent-worktree-prune.sh @@ -7,6 +7,7 @@ DRY_RUN=0 FORCE_DIRTY=0 DELETE_BRANCHES=0 DELETE_REMOTE_BRANCHES=0 +ONLY_DIRTY_WORKTREES=0 TARGET_BRANCH="" if [[ -n "$BASE_BRANCH" ]]; then @@ -36,13 +37,17 @@ while [[ $# -gt 0 ]]; do DELETE_REMOTE_BRANCHES=1 shift ;; + --only-dirty-worktrees) + ONLY_DIRTY_WORKTREES=1 + shift + ;; --branch) TARGET_BRANCH="${2:-}" shift 2 ;; *) echo "[agent-worktree-prune] Unknown argument: $1" >&2 - echo "Usage: $0 [--base ] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--branch ]" >&2 + echo "Usage: $0 [--base ] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch ]" >&2 exit 1 ;; esac @@ -165,6 +170,8 @@ process_entry() { if [[ "$DELETE_BRANCHES" -eq 1 ]]; then remove_reason="merged-agent-branch" fi + elif [[ "$ONLY_DIRTY_WORKTREES" -eq 1 ]] && is_clean_worktree "$wt"; then + remove_reason="clean-agent-worktree" fi elif [[ "$branch" == __agent_integrate_* || "$branch" == __source-probe-* ]]; then remove_reason="temporary-worktree" diff --git a/templates/scripts/codex-agent.sh b/templates/scripts/codex-agent.sh index a7d9f1c..567b432 100755 --- a/templates/scripts/codex-agent.sh +++ b/templates/scripts/codex-agent.sh @@ -419,7 +419,7 @@ if [[ -x "${repo_root}/scripts/agent-worktree-prune.sh" ]]; then prune_args+=(--base "$BASE_BRANCH") fi if [[ "$AUTO_CLEANUP" -eq 1 && "$auto_finish_completed" -eq 1 ]]; then - prune_args+=(--delete-branches --delete-remote-branches) + prune_args+=(--only-dirty-worktrees --delete-branches --delete-remote-branches) fi if ! bash "${repo_root}/scripts/agent-worktree-prune.sh" "${prune_args[@]}"; then echo "[codex-agent] Warning: automatic worktree cleanup failed." >&2 diff --git a/test/install.test.js b/test/install.test.js index d8cf89d..ebc3010 100644 --- a/test/install.test.js +++ b/test/install.test.js @@ -2266,6 +2266,30 @@ test('worktree prune preserves dirty agent worktrees unless --force-dirty is use assert.equal(fs.existsSync(worktreePath), false, 'dirty worktree should be removable with --force-dirty'); }); +test('worktree prune --only-dirty-worktrees removes clean agent worktrees but keeps unmerged branch refs', () => { + const repoDir = initRepo(); + let result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + seedCommit(repoDir); + + const worktreePath = path.join(repoDir, '.omx', 'agent-worktrees', 'agent__test-clean-worktree-prune'); + result = runCmd('git', ['worktree', 'add', '-b', 'agent/test-clean-worktree-prune', worktreePath, 'dev'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + + fs.writeFileSync(path.join(worktreePath, 'unmerged.txt'), 'keep branch, drop clean worktree\n', 'utf8'); + result = runCmd('git', ['-C', worktreePath, 'add', 'unmerged.txt'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + result = runCmd('git', ['-C', worktreePath, 'commit', '-m', 'unmerged clean worktree commit'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + + result = runCmd('bash', ['scripts/agent-worktree-prune.sh', '--only-dirty-worktrees'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.equal(fs.existsSync(worktreePath), false, 'clean agent worktree should be removed'); + + const branchResult = runCmd('git', ['show-ref', '--verify', '--quiet', 'refs/heads/agent/test-clean-worktree-prune'], repoDir); + assert.equal(branchResult.status, 0, 'unmerged branch ref should remain'); +}); + test('cleanup command removes merged agent branch/worktree and remote ref', () => { const repoDir = initRepo(); seedCommit(repoDir); @@ -2298,6 +2322,31 @@ test('cleanup command removes merged agent branch/worktree and remote ref', () = assert.equal(fs.existsSync(worktreePath), false, 'cleanup should remove worktree'); }); +test('cleanup command keeps unmerged agent branch refs but removes clean agent worktrees', () => { + const repoDir = initRepo(); + seedCommit(repoDir); + + let result = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + + const worktreePath = path.join(repoDir, '.omx', 'agent-worktrees', 'agent__cleanup-keep-branch'); + result = runCmd('git', ['worktree', 'add', '-b', 'agent/test-cleanup-keep-branch', worktreePath, 'dev'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + + fs.writeFileSync(path.join(worktreePath, 'feature.txt'), 'feature branch commit\n', 'utf8'); + result = runCmd('git', ['-C', worktreePath, 'add', 'feature.txt'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + result = runCmd('git', ['-C', worktreePath, 'commit', '-m', 'feature commit'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + + result = runNode(['cleanup', '--target', repoDir, '--branch', 'agent/test-cleanup-keep-branch', '--keep-remote'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.equal(fs.existsSync(worktreePath), false, 'cleanup should remove clean worktree by default'); + + const localBranch = runCmd('git', ['show-ref', '--verify', '--quiet', 'refs/heads/agent/test-cleanup-keep-branch'], repoDir); + assert.equal(localBranch.status, 0, 'cleanup should keep unmerged local branch'); +}); + test('release fails outside the maintainer repo path', () => { const repoDir = initRepoOnBranch('main'); const result = runNode(['release'], repoDir);