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',
);
});