From d7055af7833142ece9b9ae0c4aefe7e14e3b2195 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Fri, 3 Apr 2026 04:48:20 +0000 Subject: [PATCH 1/2] fix(core): fetch from remote on pool reuse when resolve is remote Pool reuse with `resolve: remote` was silently serving stale data. resetSlot() used `git reset --hard ` which only resets to the local branch, never contacting the remote. Now it does `git fetch origin ` + `git reset --hard FETCH_HEAD` so the pool slot reflects the current remote state at eval start. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/evaluation/workspace/pool-manager.ts | 15 ++- .../evaluation/workspace/pool-manager.test.ts | 91 +++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/packages/core/src/evaluation/workspace/pool-manager.ts b/packages/core/src/evaluation/workspace/pool-manager.ts index d01967ae1..7550c2761 100644 --- a/packages/core/src/evaluation/workspace/pool-manager.ts +++ b/packages/core/src/evaluation/workspace/pool-manager.ts @@ -373,7 +373,20 @@ export class WorkspacePoolManager { continue; } const ref = repo.checkout?.ref ?? 'HEAD'; - await git(['reset', '--hard', ref], { cwd: repoDir }); + const resolve = repo.checkout?.resolve ?? 'remote'; + + // When resolve is 'remote', fetch latest from origin before resetting + // so the pool slot reflects the current remote state at eval start. + if (resolve === 'remote') { + const fetchArgs = ['fetch', 'origin', ref]; + if (repo.clone?.depth) { + fetchArgs.splice(1, 0, '--depth', String(repo.clone.depth)); + } + await git(fetchArgs, { cwd: repoDir }); + await git(['reset', '--hard', 'FETCH_HEAD'], { cwd: repoDir }); + } else { + await git(['reset', '--hard', ref], { cwd: repoDir }); + } // strict removes .gitignored files (node_modules, build outputs). // fast preserves .gitignored files, letting before_all // build steps survive across pool reuse cycles and avoiding expensive rebuilds. diff --git a/packages/core/test/evaluation/workspace/pool-manager.test.ts b/packages/core/test/evaluation/workspace/pool-manager.test.ts index a23f2eea4..b7b3da0f5 100644 --- a/packages/core/test/evaluation/workspace/pool-manager.test.ts +++ b/packages/core/test/evaluation/workspace/pool-manager.test.ts @@ -759,6 +759,97 @@ describe('WorkspacePoolManager', () => { }); }); + describe('resolve: remote pool reuse', () => { + it('fetches latest from remote on pool reuse when resolve is remote', async () => { + const repoDir = path.join(tmpDir, 'source-repo'); + const initialSha = createTestRepo(repoDir, { 'hello.txt': 'v1' }); + // Rename default branch to 'main' (git init defaults to 'master') + execSync('git branch -M main', { cwd: repoDir, ...EXEC_OPTS }); + + const manager = new WorkspacePoolManager(poolRoot); + const repos: RepoConfig[] = [ + { + path: './my-repo', + source: { type: 'local', path: repoDir }, + checkout: { ref: 'main', resolve: 'remote' }, + }, + ]; + + // First acquisition — clones at initial commit + const slot1 = await manager.acquireWorkspace({ + repos, + maxSlots: 3, + repoManager, + }); + expect(slot1.isExisting).toBe(false); + expect(readFileSync(path.join(slot1.path, 'my-repo', 'hello.txt'), 'utf-8')).toBe('v1'); + await manager.releaseSlot(slot1); + + // Advance the source repo to a new commit + writeFileSync(path.join(repoDir, 'hello.txt'), 'v2'); + execSync('git add -A && git commit -m "v2"', { cwd: repoDir, ...EXEC_OPTS }); + const newSha = gitExec('git rev-parse HEAD', repoDir); + expect(newSha).not.toBe(initialSha); + + // Second acquisition — should reuse slot but fetch latest + const slot2 = await manager.acquireWorkspace({ + repos, + maxSlots: 3, + repoManager, + }); + expect(slot2.isExisting).toBe(true); + expect(slot2.path).toBe(slot1.path); + + // The repo should have the latest content from the source + expect(readFileSync(path.join(slot2.path, 'my-repo', 'hello.txt'), 'utf-8')).toBe('v2'); + const slot2Head = gitExec('git rev-parse HEAD', path.join(slot2.path, 'my-repo')); + expect(slot2Head).toBe(newSha); + + await manager.releaseSlot(slot2); + }); + + it('does not fetch from remote when resolve is local', async () => { + const repoDir = path.join(tmpDir, 'source-repo'); + createTestRepo(repoDir, { 'hello.txt': 'v1' }); + // Rename default branch to 'main' (git init defaults to 'master') + execSync('git branch -M main', { cwd: repoDir, ...EXEC_OPTS }); + + const manager = new WorkspacePoolManager(poolRoot); + const repos: RepoConfig[] = [ + { + path: './my-repo', + source: { type: 'local', path: repoDir }, + checkout: { ref: 'main', resolve: 'local' }, + }, + ]; + + // First acquisition + const slot1 = await manager.acquireWorkspace({ + repos, + maxSlots: 3, + repoManager, + }); + await manager.releaseSlot(slot1); + + // Advance the source repo + writeFileSync(path.join(repoDir, 'hello.txt'), 'v2'); + execSync('git add -A && git commit -m "v2"', { cwd: repoDir, ...EXEC_OPTS }); + + // Second acquisition — should reuse slot WITHOUT fetching + const slot2 = await manager.acquireWorkspace({ + repos, + maxSlots: 3, + repoManager, + }); + expect(slot2.isExisting).toBe(true); + + // The repo should still have the original content (no fetch) + expect(readFileSync(path.join(slot2.path, 'my-repo', 'hello.txt'), 'utf-8')).toBe('v1'); + + await manager.releaseSlot(slot2); + }); + }); + describe('releaseSlot', () => { it('removes lock file', async () => { const repoDir = path.join(tmpDir, 'source-repo'); From e0c026c1184e69276ba29f3863784dbfc6f9b0d8 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Fri, 3 Apr 2026 04:58:06 +0000 Subject: [PATCH 2/2] docs: update resetSlot docstring to reflect fetch-on-remote behavior Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/evaluation/workspace/pool-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/evaluation/workspace/pool-manager.ts b/packages/core/src/evaluation/workspace/pool-manager.ts index 7550c2761..86f2f3cef 100644 --- a/packages/core/src/evaluation/workspace/pool-manager.ts +++ b/packages/core/src/evaluation/workspace/pool-manager.ts @@ -354,7 +354,7 @@ export class WorkspacePoolManager { /** * Reset an existing slot for reuse: - * 1. Reset repos (git reset --hard {ref} && git clean -fd per repo) + * 1. Reset repos (fetch from origin when resolve=remote, then git reset --hard && git clean per repo) * 2. Re-copy template files (skip repo directories) */ private async resetSlot(