From 489574dd0e71dc32d47b428a0cdbb8daa5b8d5f4 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Sat, 16 May 2026 00:40:37 +0200 Subject: [PATCH 1/4] Auto-finish: agent/claude/skip-merged-worktree-on-reuse-2026-05-16-00-37 --- .../.openspec.yaml | 2 ++ .../proposal.md | 11 ++++++ .../skip-merged-worktree-on-reuse/spec.md | 9 +++++ .../tasks.md | 34 +++++++++++++++++++ templates/scripts/agent-branch-start.sh | 32 ++++++++++++++++- 5 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/.openspec.yaml create mode 100644 openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/proposal.md create mode 100644 openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/specs/skip-merged-worktree-on-reuse/spec.md create mode 100644 openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/tasks.md diff --git a/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/.openspec.yaml b/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/.openspec.yaml new file mode 100644 index 00000000..9f708669 --- /dev/null +++ b/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-15 diff --git a/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/proposal.md b/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/proposal.md new file mode 100644 index 00000000..313eca34 --- /dev/null +++ b/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/proposal.md @@ -0,0 +1,11 @@ +## Why + +- TODO: describe the user/problem outcome this change addresses. + +## What Changes + +- TODO: summarize the intended behavior and scope. + +## Impact + +- TODO: call out risks, rollout notes, and affected surfaces. diff --git a/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/specs/skip-merged-worktree-on-reuse/spec.md b/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/specs/skip-merged-worktree-on-reuse/spec.md new file mode 100644 index 00000000..bdc1e482 --- /dev/null +++ b/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/specs/skip-merged-worktree-on-reuse/spec.md @@ -0,0 +1,9 @@ +## ADDED Requirements + +### Requirement: skip-merged-worktree-on-reuse behavior +The system SHALL enforce skip-merged-worktree-on-reuse behavior as defined by this change. + +#### Scenario: Baseline acceptance +- **WHEN** skip-merged-worktree-on-reuse behavior is exercised +- **THEN** the expected outcome is produced +- **AND** regressions are covered by tests. diff --git a/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/tasks.md b/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/tasks.md new file mode 100644 index 00000000..da519523 --- /dev/null +++ b/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/tasks.md @@ -0,0 +1,34 @@ +## Definition of Done + +This change is complete only when **all** of the following are true: + +- Every checkbox below is checked. +- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff. +- 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. + +## Handoff + +- Handoff: change=`agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37`; branch=`agent//`; scope=`TODO`; action=`continue this sandbox or finish cleanup after a usage-limit/manual takeover`. +- Copy prompt: Continue `agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37` on branch `agent//`. 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// --base dev --via-pr --wait-for-merge --cleanup`. + +## 1. Specification + +- [ ] 1.1 Finalize proposal scope and acceptance criteria for `agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37`. +- [ ] 1.2 Define normative requirements in `specs/skip-merged-worktree-on-reuse/spec.md`. + +## 2. Implementation + +- [ ] 2.1 Implement scoped behavior changes. +- [ ] 2.2 Add/update focused regression coverage. + +## 3. Verification + +- [ ] 3.1 Run targeted project verification commands. +- [ ] 3.2 Run `openspec validate agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37 --type change --strict`. +- [ ] 3.3 Run `openspec validate --specs`. + +## 4. Cleanup (mandatory; run before claiming completion) + +- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent// --base dev --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation. +- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff. +- [ ] 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). diff --git a/templates/scripts/agent-branch-start.sh b/templates/scripts/agent-branch-start.sh index e9c67503..d4885e5f 100755 --- a/templates/scripts/agent-branch-start.sh +++ b/templates/scripts/agent-branch-start.sh @@ -508,6 +508,26 @@ managed_worktree_roots() { done } +branch_tip_is_merged_into_protected_base() { + local repo="$1" + local entry_worktree="$2" + local protected_raw="$3" + local head_sha base ref + head_sha="$(git -C "$entry_worktree" rev-parse --verify HEAD 2>/dev/null || true)" + if [[ -z "$head_sha" ]]; then + return 1 + fi + for base in $protected_raw; do + for ref in "refs/heads/${base}" "refs/remotes/origin/${base}"; do + git -C "$repo" show-ref --verify --quiet "$ref" || continue + if git -C "$repo" merge-base --is-ancestor "$head_sha" "$ref" >/dev/null 2>&1; then + return 0 + fi + done + done + return 1 +} + find_matching_dirty_agent_worktree() { local repo="$1" local worktree_root_rel="$2" @@ -517,7 +537,8 @@ find_matching_dirty_agent_worktree() { local best_branch="" local best_worktree="" local best_count=0 - local root entry branch descriptor score + local root entry branch descriptor score protected_raw + protected_raw="$(resolve_protected_branches "$repo")" while IFS= read -r root; do [[ -d "$root" ]] || continue @@ -528,6 +549,15 @@ find_matching_dirty_agent_worktree() { fi [[ "$branch" == "agent/${agent_slug}/"* ]] || continue has_local_changes "$entry" || continue + # Skip worktrees whose branch tip is already reachable from a + # protected base. After `gx branch finish --via-pr --cleanup` the + # worktree directory can linger on disk (e.g. the operator's shell + # is still cwd'd inside it). Reusing such a worktree for a new + # task would silently hand the next agent a stale, merged HEAD. + if branch_tip_is_merged_into_protected_base "$repo" "$entry" "$protected_raw"; then + echo "[agent-branch-start] Skipping merged-and-cleaned worktree: ${entry} (branch ${branch} already in base)" >&2 + continue + fi descriptor="${branch#agent/${agent_slug}/}" score="$(token_match_score "$task_slug" "$descriptor")" From 7f477ec540c54144c181fc8d3b2506c8b126f244 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Sat, 16 May 2026 00:43:08 +0200 Subject: [PATCH 2/4] Auto-finish: agent/claude/skip-merged-worktree-on-reuse-2026-05-16-00-37 --- templates/scripts/agent-branch-start.sh | 44 +++++++++++-------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/templates/scripts/agent-branch-start.sh b/templates/scripts/agent-branch-start.sh index d4885e5f..6b87ec55 100755 --- a/templates/scripts/agent-branch-start.sh +++ b/templates/scripts/agent-branch-start.sh @@ -508,24 +508,23 @@ managed_worktree_roots() { done } -branch_tip_is_merged_into_protected_base() { +branch_published_then_remote_pruned() { + # Detect the post-`gx branch finish --via-pr --cleanup` state: the agent + # branch was published (so upstream config is set), but the remote-tracking + # ref no longer exists (because `push origin --delete` ran during cleanup). + # That combination only arises after a finish that successfully merged the + # PR and pruned the remote branch — a freshly-created agent branch never + # has an upstream until publish, so this never false-positives on the + # "started, dirty, no commits yet" case that we want to keep reusable. local repo="$1" - local entry_worktree="$2" - local protected_raw="$3" - local head_sha base ref - head_sha="$(git -C "$entry_worktree" rev-parse --verify HEAD 2>/dev/null || true)" - if [[ -z "$head_sha" ]]; then + local branch="$2" + local upstream + upstream="$(git -C "$repo" config --get "branch.${branch}.remote" 2>/dev/null || true)" + [[ -n "$upstream" ]] || return 1 + if git -C "$repo" show-ref --verify --quiet "refs/remotes/${upstream}/${branch}"; then return 1 fi - for base in $protected_raw; do - for ref in "refs/heads/${base}" "refs/remotes/origin/${base}"; do - git -C "$repo" show-ref --verify --quiet "$ref" || continue - if git -C "$repo" merge-base --is-ancestor "$head_sha" "$ref" >/dev/null 2>&1; then - return 0 - fi - done - done - return 1 + return 0 } find_matching_dirty_agent_worktree() { @@ -537,8 +536,7 @@ find_matching_dirty_agent_worktree() { local best_branch="" local best_worktree="" local best_count=0 - local root entry branch descriptor score protected_raw - protected_raw="$(resolve_protected_branches "$repo")" + local root entry branch descriptor score while IFS= read -r root; do [[ -d "$root" ]] || continue @@ -549,13 +547,11 @@ find_matching_dirty_agent_worktree() { fi [[ "$branch" == "agent/${agent_slug}/"* ]] || continue has_local_changes "$entry" || continue - # Skip worktrees whose branch tip is already reachable from a - # protected base. After `gx branch finish --via-pr --cleanup` the - # worktree directory can linger on disk (e.g. the operator's shell - # is still cwd'd inside it). Reusing such a worktree for a new - # task would silently hand the next agent a stale, merged HEAD. - if branch_tip_is_merged_into_protected_base "$repo" "$entry" "$protected_raw"; then - echo "[agent-branch-start] Skipping merged-and-cleaned worktree: ${entry} (branch ${branch} already in base)" >&2 + # Skip merged-and-cleaned worktrees that happen to still be on disk + # (e.g. operator's shell is cwd'd inside; cleanup deferred). Reusing + # such a worktree would silently hand the next agent a stale HEAD. + if branch_published_then_remote_pruned "$repo" "$branch"; then + echo "[agent-branch-start] Skipping merged-and-cleaned worktree: ${entry} (branch ${branch} has no remote tracking ref)" >&2 continue fi From d342fd9e9a9fb64f479a84d1429d0f32fe1e48d0 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Sat, 16 May 2026 00:44:24 +0200 Subject: [PATCH 3/4] Auto-finish: agent/claude/skip-merged-worktree-on-reuse-2026-05-16-00-37 --- test/branch.test.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/branch.test.js b/test/branch.test.js index 70ae1ae3..dd61adb9 100644 --- a/test/branch.test.js +++ b/test/branch.test.js @@ -176,6 +176,38 @@ test('agent-branch-start reuses a single dirty matching managed worktree from th ); }); +test('agent-branch-start skips a merged-and-cleaned worktree instead of reusing it for a new task', () => { + const { repoDir } = createBootstrappedRepo({ committed: true }); + + let result = runBranchStart(['--tier', 'T1', 'tui plan watcher validator', 'bot'], repoDir, { + GUARDEX_OPENSPEC_AUTO_INIT: 'true', + }); + assert.equal(result.status, 0, result.stderr || result.stdout); + const firstBranch = extractCreatedBranch(result.stdout); + const firstWorktree = extractCreatedWorktree(result.stdout); + + // Simulate post-`gx branch finish --via-pr --cleanup` state on the + // primary checkout: the branch had been published (upstream config set) + // and the remote-tracking ref was then deleted by `push --delete`. + let cmd = runCmd('git', ['config', `branch.${firstBranch}.remote`, 'origin'], repoDir); + assert.equal(cmd.status, 0, cmd.stderr || cmd.stdout); + cmd = runCmd('git', ['config', `branch.${firstBranch}.merge`, `refs/heads/${firstBranch}`], repoDir); + assert.equal(cmd.status, 0, cmd.stderr || cmd.stdout); + // Leave the worktree dirty so it would otherwise match the reuse heuristic. + fs.writeFileSync(path.join(firstWorktree, 'leftover.txt'), 'merged-but-not-yet-pruned\n', 'utf8'); + + // A new task whose slug tokens overlap with the merged-and-cleaned branch. + result = runBranchStart(['--tier', 'T1', 'tui plan validator', 'bot'], repoDir, { + GUARDEX_OPENSPEC_AUTO_INIT: 'true', + }); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.doesNotMatch(result.stdout, /Matched dirty managed worktree for requested task/); + assert.match(result.stderr, /Skipping merged-and-cleaned worktree/); + assert.match(result.stdout, /Created branch: agent\/codex\/tui-plan-validator-/); + assert.notEqual(extractCreatedBranch(result.stdout), firstBranch); + assert.notEqual(extractCreatedWorktree(result.stdout), firstWorktree); +}); + test('agent-branch-start creates a fresh branch when dirty matching worktrees are ambiguous', () => { const { repoDir } = createBootstrappedRepo({ committed: true }); From 851eb4e43d12e2245b14371d30b3e748f6745ca4 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Sat, 16 May 2026 00:45:36 +0200 Subject: [PATCH 4/4] Auto-finish: agent/claude/skip-merged-worktree-on-reuse-2026-05-16-00-37 --- .../proposal.md | 12 +++++++++--- test/branch.test.js | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/proposal.md b/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/proposal.md index 313eca34..bea8265f 100644 --- a/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/proposal.md +++ b/openspec/changes/agent-claude-skip-merged-worktree-on-reuse-2026-05-16-00-37/proposal.md @@ -1,11 +1,17 @@ ## Why -- TODO: describe the user/problem outcome this change addresses. +`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//` 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. ## What Changes -- TODO: summarize the intended behavior and scope. +- 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//` ref no longer exists locally (so `gx branch finish ... --cleanup` already pruned the remote). +- 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. +- Emit a clear stderr line (`Skipping merged-and-cleaned worktree: …`) so operators see why a stale lane was bypassed. +- 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. ## Impact -- TODO: call out risks, rollout notes, and affected surfaces. +- Affected surfaces: `gx branch start` worktree-reuse heuristic only. Finish flow, prune flow, and lock-claim flow are untouched. +- Risk: low. The filter is gated on both a config key (`branch..remote`) AND the absence of `refs/remotes//`. The existing "fresh agent, dirty, no commits yet" reuse test (`branch.test.js`) continues to pass because that scenario never sets `branch..remote`. +- 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. +- No version bump required (bug fix in shell logic; no CLI surface change, no schema/API change). diff --git a/test/branch.test.js b/test/branch.test.js index dd61adb9..51eb5bec 100644 --- a/test/branch.test.js +++ b/test/branch.test.js @@ -203,7 +203,7 @@ test('agent-branch-start skips a merged-and-cleaned worktree instead of reusing assert.equal(result.status, 0, result.stderr || result.stdout); assert.doesNotMatch(result.stdout, /Matched dirty managed worktree for requested task/); assert.match(result.stderr, /Skipping merged-and-cleaned worktree/); - assert.match(result.stdout, /Created branch: agent\/codex\/tui-plan-validator-/); + assert.match(result.stdout, /Created branch: agent\/(codex|claude)\/tui-plan-validator-/); assert.notEqual(extractCreatedBranch(result.stdout), firstBranch); assert.notEqual(extractCreatedWorktree(result.stdout), firstWorktree); });