Skip to content

Commit 62f05a8

Browse files
NagyViktNagyVikt
andauthored
Auto-finish: agent/claude/skip-merged-worktree-on-reuse-2026-05-16-00-37 (#584)
* Auto-finish: agent/claude/skip-merged-worktree-on-reuse-2026-05-16-00-37 * Auto-finish: agent/claude/skip-merged-worktree-on-reuse-2026-05-16-00-37 * Auto-finish: agent/claude/skip-merged-worktree-on-reuse-2026-05-16-00-37 * Auto-finish: agent/claude/skip-merged-worktree-on-reuse-2026-05-16-00-37 --------- Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
1 parent 4861794 commit 62f05a8

6 files changed

Lines changed: 120 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-05-15
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
## Why
2+
3+
`gx branch start` could silently hand a second agent a stale worktree whose branch had already been merged and cleaned. The dirty-worktree-reuse heuristic only filtered on `agent/<slug>/` prefix, dirty state, and token-match — it never asked "has this branch already landed?". In a recent 12-lane parallel dispatch, agent B's `gx branch start` matched the still-on-disk worktree of agent A (whose PR had merged minutes earlier), pointing the new task at agent A's merged HEAD. The mismatch was visible on first edit, so it was caught manually, but the same race in an autonomous fleet would have produced a PR-time conflict instead of a clean lane.
4+
5+
## What Changes
6+
7+
- Add a `branch_published_then_remote_pruned` filter inside `find_matching_dirty_agent_worktree` (`templates/scripts/agent-branch-start.sh`). A candidate worktree is skipped when its branch's upstream config is set (so the branch was pushed at some point) but the matching `refs/remotes/<remote>/<branch>` ref no longer exists locally (so `gx branch finish ... --cleanup` already pruned the remote).
8+
- That combination only arises after a finish that successfully published, merged, and pruned the branch — a freshly-created agent branch never has an upstream until `push -u`, so the "started, dirty, no commits yet" reuse case keeps working unchanged.
9+
- Emit a clear stderr line (`Skipping merged-and-cleaned worktree: …`) so operators see why a stale lane was bypassed.
10+
- Add a regression test (`test/branch.test.js`) that simulates the post-cleanup state (upstream config + missing remote-tracking ref + dirty file) and asserts a fresh lane is created instead of the merged one being reused.
11+
12+
## Impact
13+
14+
- Affected surfaces: `gx branch start` worktree-reuse heuristic only. Finish flow, prune flow, and lock-claim flow are untouched.
15+
- Risk: low. The filter is gated on both a config key (`branch.<branch>.remote`) AND the absence of `refs/remotes/<remote>/<branch>`. The existing "fresh agent, dirty, no commits yet" reuse test (`branch.test.js`) continues to pass because that scenario never sets `branch.<branch>.remote`.
16+
- Rollout: no flag; once shipped, every `gx branch start` call benefits. Operator-visible change is the new "Skipping merged-and-cleaned worktree" stderr line in the case it triggers; otherwise output is unchanged.
17+
- No version bump required (bug fix in shell logic; no CLI surface change, no schema/API change).
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
## ADDED Requirements
2+
3+
### Requirement: skip-merged-worktree-on-reuse behavior
4+
The system SHALL enforce skip-merged-worktree-on-reuse behavior as defined by this change.
5+
6+
#### Scenario: Baseline acceptance
7+
- **WHEN** skip-merged-worktree-on-reuse behavior is exercised
8+
- **THEN** the expected outcome is produced
9+
- **AND** regressions are covered by tests.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## Definition of Done
2+
3+
This change is complete only when **all** of the following are true:
4+
5+
- Every checkbox below is checked.
6+
- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff.
7+
- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline.
8+
9+
## Handoff
10+
11+
- Handoff: change=`agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37`; branch=`agent/<your-name>/<branch-slug>`; scope=`TODO`; action=`continue this sandbox or finish cleanup after a usage-limit/manual takeover`.
12+
- Copy prompt: Continue `agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37` on branch `agent/<your-name>/<branch-slug>`. Work inside the existing sandbox, review `openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/<your-name>/<branch-slug> --base dev --via-pr --wait-for-merge --cleanup`.
13+
14+
## 1. Specification
15+
16+
- [ ] 1.1 Finalize proposal scope and acceptance criteria for `agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37`.
17+
- [ ] 1.2 Define normative requirements in `specs/skip-merged-worktree-on-reuse/spec.md`.
18+
19+
## 2. Implementation
20+
21+
- [ ] 2.1 Implement scoped behavior changes.
22+
- [ ] 2.2 Add/update focused regression coverage.
23+
24+
## 3. Verification
25+
26+
- [ ] 3.1 Run targeted project verification commands.
27+
- [ ] 3.2 Run `openspec validate agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37 --type change --strict`.
28+
- [ ] 3.3 Run `openspec validate --specs`.
29+
30+
## 4. Cleanup (mandatory; run before claiming completion)
31+
32+
- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent/<your-name>/<branch-slug> --base dev --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation.
33+
- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff.
34+
- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch).

templates/scripts/agent-branch-start.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,25 @@ managed_worktree_roots() {
508508
done
509509
}
510510

511+
branch_published_then_remote_pruned() {
512+
# Detect the post-`gx branch finish --via-pr --cleanup` state: the agent
513+
# branch was published (so upstream config is set), but the remote-tracking
514+
# ref no longer exists (because `push origin --delete` ran during cleanup).
515+
# That combination only arises after a finish that successfully merged the
516+
# PR and pruned the remote branch — a freshly-created agent branch never
517+
# has an upstream until publish, so this never false-positives on the
518+
# "started, dirty, no commits yet" case that we want to keep reusable.
519+
local repo="$1"
520+
local branch="$2"
521+
local upstream
522+
upstream="$(git -C "$repo" config --get "branch.${branch}.remote" 2>/dev/null || true)"
523+
[[ -n "$upstream" ]] || return 1
524+
if git -C "$repo" show-ref --verify --quiet "refs/remotes/${upstream}/${branch}"; then
525+
return 1
526+
fi
527+
return 0
528+
}
529+
511530
find_matching_dirty_agent_worktree() {
512531
local repo="$1"
513532
local worktree_root_rel="$2"
@@ -528,6 +547,13 @@ find_matching_dirty_agent_worktree() {
528547
fi
529548
[[ "$branch" == "agent/${agent_slug}/"* ]] || continue
530549
has_local_changes "$entry" || continue
550+
# Skip merged-and-cleaned worktrees that happen to still be on disk
551+
# (e.g. operator's shell is cwd'd inside; cleanup deferred). Reusing
552+
# such a worktree would silently hand the next agent a stale HEAD.
553+
if branch_published_then_remote_pruned "$repo" "$branch"; then
554+
echo "[agent-branch-start] Skipping merged-and-cleaned worktree: ${entry} (branch ${branch} has no remote tracking ref)" >&2
555+
continue
556+
fi
531557

532558
descriptor="${branch#agent/${agent_slug}/}"
533559
score="$(token_match_score "$task_slug" "$descriptor")"

test/branch.test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,38 @@ test('agent-branch-start reuses a single dirty matching managed worktree from th
176176
);
177177
});
178178

179+
test('agent-branch-start skips a merged-and-cleaned worktree instead of reusing it for a new task', () => {
180+
const { repoDir } = createBootstrappedRepo({ committed: true });
181+
182+
let result = runBranchStart(['--tier', 'T1', 'tui plan watcher validator', 'bot'], repoDir, {
183+
GUARDEX_OPENSPEC_AUTO_INIT: 'true',
184+
});
185+
assert.equal(result.status, 0, result.stderr || result.stdout);
186+
const firstBranch = extractCreatedBranch(result.stdout);
187+
const firstWorktree = extractCreatedWorktree(result.stdout);
188+
189+
// Simulate post-`gx branch finish --via-pr --cleanup` state on the
190+
// primary checkout: the branch had been published (upstream config set)
191+
// and the remote-tracking ref was then deleted by `push --delete`.
192+
let cmd = runCmd('git', ['config', `branch.${firstBranch}.remote`, 'origin'], repoDir);
193+
assert.equal(cmd.status, 0, cmd.stderr || cmd.stdout);
194+
cmd = runCmd('git', ['config', `branch.${firstBranch}.merge`, `refs/heads/${firstBranch}`], repoDir);
195+
assert.equal(cmd.status, 0, cmd.stderr || cmd.stdout);
196+
// Leave the worktree dirty so it would otherwise match the reuse heuristic.
197+
fs.writeFileSync(path.join(firstWorktree, 'leftover.txt'), 'merged-but-not-yet-pruned\n', 'utf8');
198+
199+
// A new task whose slug tokens overlap with the merged-and-cleaned branch.
200+
result = runBranchStart(['--tier', 'T1', 'tui plan validator', 'bot'], repoDir, {
201+
GUARDEX_OPENSPEC_AUTO_INIT: 'true',
202+
});
203+
assert.equal(result.status, 0, result.stderr || result.stdout);
204+
assert.doesNotMatch(result.stdout, /Matched dirty managed worktree for requested task/);
205+
assert.match(result.stderr, /Skipping merged-and-cleaned worktree/);
206+
assert.match(result.stdout, /Created branch: agent\/(codex|claude)\/tui-plan-validator-/);
207+
assert.notEqual(extractCreatedBranch(result.stdout), firstBranch);
208+
assert.notEqual(extractCreatedWorktree(result.stdout), firstWorktree);
209+
});
210+
179211
test('agent-branch-start creates a fresh branch when dirty matching worktrees are ambiguous', () => {
180212
const { repoDir } = createBootstrappedRepo({ committed: true });
181213

0 commit comments

Comments
 (0)