Skip to content
Closed
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions bin/multiagent-safety.js
Original file line number Diff line number Diff line change
Expand Up @@ -1851,6 +1851,7 @@ function parseCleanupArgs(rawArgs) {
dryRun: false,
forceDirty: false,
keepRemote: false,
keepCleanWorktrees: false,
};

for (let index = 0; index < rawArgs.length; index += 1) {
Expand Down Expand Up @@ -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}`);
}

Expand Down Expand Up @@ -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');
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion scripts/agent-branch-finish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion scripts/agent-worktree-prune.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--branch <agent/...>]" >&2
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...>]" >&2
exit 1
;;
esac
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion templates/scripts/agent-branch-finish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion templates/scripts/agent-worktree-prune.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--branch <agent/...>]" >&2
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...>]" >&2
exit 1
;;
esac
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion templates/scripts/codex-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions test/install.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Loading