diff --git a/openspec/changes/agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02/.openspec.yaml b/openspec/changes/agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02/.openspec.yaml new file mode 100644 index 0000000..34f9314 --- /dev/null +++ b/openspec/changes/agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-29 diff --git a/openspec/changes/agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02/proposal.md b/openspec/changes/agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02/proposal.md new file mode 100644 index 0000000..7ae3092 --- /dev/null +++ b/openspec/changes/agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02/proposal.md @@ -0,0 +1,15 @@ +## Why + +- After a Guardex PR merge, local base checkouts can remain behind `origin/` and VS Code shows pending sync/pull state even though the branch was already pushed and merged. +- Guardex already refreshes clean local base worktrees, but a harmless dirty edit currently prevents the automatic `git pull --ff-only` and leaves the operator stuck doing manual cleanup. + +## What Changes + +- Let `agent-branch-finish.sh` attempt the post-merge base refresh even when the local base worktree has dirty edits. +- Keep the refresh guarded: use `git pull --ff-only` with Git autostash disabled, and treat failures as warnings so Git never stashes, rebases, or overwrites local work. +- Update focused finish-flow regression coverage for dirty/non-conflicting and dirty/conflicting base worktrees. + +## Impact + +- Affects `gx branch finish` and `gx finish` only after a successful merge path reaches the base-worktree refresh step. +- Dirty base worktrees may now fast-forward automatically when Git can preserve local edits. If Git detects an overwrite/conflict risk, Guardex leaves the worktree untouched and reports the failed refresh. diff --git a/openspec/changes/agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02/specs/auto-pull-base-after-pr-merge/spec.md b/openspec/changes/agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02/specs/auto-pull-base-after-pr-merge/spec.md new file mode 100644 index 0000000..73d5eb1 --- /dev/null +++ b/openspec/changes/agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02/specs/auto-pull-base-after-pr-merge/spec.md @@ -0,0 +1,18 @@ +## ADDED Requirements + +### Requirement: Guarded Post-Merge Base Pull + +After `gx branch finish` successfully merges an agent branch and local base worktree refresh is enabled, Guardex SHALL attempt to fast-forward the local base worktree from `origin/` using `git pull --ff-only` even when the base worktree has local modifications. + +#### Scenario: Dirty base worktree can be fast-forwarded +- **GIVEN** a local base worktree has dirty edits that do not conflict with the merged remote changes +- **WHEN** `gx branch finish` completes a merge and refreshes the local base worktree +- **THEN** Guardex SHALL run the guarded fast-forward pull +- **AND** the base worktree SHALL contain the merged remote change +- **AND** the dirty local edits SHALL remain intact. + +#### Scenario: Dirty base worktree cannot be safely fast-forwarded +- **GIVEN** a local base worktree has dirty edits that Git would overwrite during the post-merge pull +- **WHEN** `gx branch finish` attempts the guarded fast-forward pull +- **THEN** Guardex SHALL leave the base worktree dirty edits untouched +- **AND** Guardex SHALL emit a warning instead of stashing, rebasing, or forcing the update. diff --git a/openspec/changes/agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02/tasks.md b/openspec/changes/agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02/tasks.md new file mode 100644 index 0000000..ffdad3a --- /dev/null +++ b/openspec/changes/agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02/tasks.md @@ -0,0 +1,36 @@ +## 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-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02`; branch=`agent/codex/auto-pull-base-after-pr-merge-2026-06-29-23-02`; scope=`guarded post-merge base pull for dirty worktrees`; action=`continue this sandbox or finish cleanup after a usage-limit/manual takeover`. +- Copy prompt: Continue `agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02` on branch `agent/codex/auto-pull-base-after-pr-merge-2026-06-29-23-02`. Work inside the existing sandbox, review `openspec/changes/agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02/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/codex/auto-pull-base-after-pr-merge-2026-06-29-23-02 --base main --via-pr --wait-for-merge --cleanup`. + +## 1. Specification + +- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02`. +- [x] 1.2 Define normative requirements in `specs/auto-pull-base-after-pr-merge/spec.md`. + +## 2. Implementation + +- [x] 2.1 Implement scoped behavior changes. +- [x] 2.2 Add/update focused regression coverage. + +## 3. Verification + +- [x] 3.1 Run targeted project verification commands. + - `node --test --test-reporter=spec --test-name-pattern "dirty local base worktree" test/finish.test.js` + - `bash -n templates/scripts/agent-branch-finish.sh` +- [x] 3.2 Run `openspec validate agent-codex-auto-pull-base-after-pr-merge-2026-06-29-23-02 --type change --strict`. +- [x] 3.3 Run `openspec validate --specs`. + +## 4. Cleanup (mandatory; run before claiming completion) + +- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent/codex/auto-pull-base-after-pr-merge-2026-06-29-23-02 --base main --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-finish.sh b/templates/scripts/agent-branch-finish.sh index 47c0ffc..32201cb 100755 --- a/templates/scripts/agent-branch-finish.sh +++ b/templates/scripts/agent-branch-finish.sh @@ -643,17 +643,16 @@ is_clean_worktree() { refresh_clean_base_worktree() { local wt="$1" + local pull_output="" [[ -z "$wt" || "$PUSH_ENABLED" -ne 1 ]] && return 0 - if ! is_clean_worktree "$wt"; then - echo "[agent-branch-finish] Warning: local ${BASE_BRANCH} worktree is dirty; skipping 'git pull --ff-only origin ${BASE_BRANCH}' for ${wt}." >&2 - return 0 - fi - - if GUARDEX_DISABLE_POST_MERGE_CLEANUP=1 GUARDEX_PRUNE_ACTIVE_CWD="$finish_active_cwd" git -C "$wt" pull --ff-only origin "$BASE_BRANCH" >/dev/null; then + if pull_output="$(GUARDEX_DISABLE_POST_MERGE_CLEANUP=1 GUARDEX_PRUNE_ACTIVE_CWD="$finish_active_cwd" git -C "$wt" -c rebase.autoStash=false -c merge.autostash=false pull --ff-only origin "$BASE_BRANCH" 2>&1)"; then echo "[agent-branch-finish] Refreshed local ${BASE_BRANCH} worktree with 'git pull --ff-only origin ${BASE_BRANCH}': ${wt}" else echo "[agent-branch-finish] Warning: failed to refresh local ${BASE_BRANCH} worktree with 'git pull --ff-only origin ${BASE_BRANCH}': ${wt}" >&2 + if [[ -n "$pull_output" ]]; then + echo "$pull_output" >&2 + fi fi } diff --git a/test/finish.test.js b/test/finish.test.js index 14e051b..c576a70 100644 --- a/test/finish.test.js +++ b/test/finish.test.js @@ -1180,7 +1180,7 @@ exit 1 }); -test('agent-branch-finish warns instead of pulling dirty local base worktree', () => { +test('agent-branch-finish guarded-pulls dirty local base worktree when Git can preserve edits', () => { const repoDir = initRepo(); seedCommit(repoDir); attachOriginRemote(repoDir); @@ -1211,13 +1211,60 @@ test('agent-branch-finish warns instead of pulling dirty local base worktree', ( ); assert.equal(finish.status, 0, finish.stderr || finish.stdout); assert.match( - finish.stderr, - /Warning: local dev worktree is dirty; skipping 'git pull --ff-only origin dev' for /, + finish.stdout, + /Refreshed local dev worktree with 'git pull --ff-only origin dev': /, ); assert.equal( fs.existsSync(path.join(auxWorktree, 'agent-dirty-base-refresh.txt')), - false, - 'dirty local dev worktree should not be pulled implicitly', + true, + 'dirty local dev worktree should still fast-forward when Git can preserve local edits', + ); + assert.equal( + fs.readFileSync(path.join(auxWorktree, 'package.json'), 'utf8'), + '{"dirty":true}\n', + 'guarded pull must preserve unrelated dirty local edits', + ); +}); + + +test('agent-branch-finish leaves dirty local base worktree untouched when guarded pull would overwrite', () => { + 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-dirty-base-refresh-conflict'], repoDir); + assert.equal(result.status, 0, result.stderr); + commitFile(repoDir, 'package.json', '{"from":"agent"}\n', 'agent conflicting package change'); + + const auxWorktree = path.join(path.dirname(repoDir), 'aux-dirty-conflict-dev'); + result = runCmd('git', ['worktree', 'add', auxWorktree, 'dev'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + fs.writeFileSync(path.join(auxWorktree, 'package.json'), '{"dirty":true}\n', 'utf8'); + + const finish = runBranchFinish( + ['--branch', 'agent/test-dirty-base-refresh-conflict', '--base', 'dev', '--direct-only', '--no-cleanup'], + repoDir, + ); + assert.equal(finish.status, 0, finish.stderr || finish.stdout); + assert.match( + finish.stderr, + /Warning: failed to refresh local dev worktree with 'git pull --ff-only origin dev': /, + ); + assert.equal( + fs.readFileSync(path.join(auxWorktree, 'package.json'), 'utf8'), + '{"dirty":true}\n', + 'guarded pull must leave conflicting dirty local edits untouched', ); });