Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 4dd5671

Browse files
authored
fix(git): keep handoff packs on the repository filesystem
Generated-By: PostHog Code Task-Id: b73fc361-26b5-4bae-8c3c-6e4682d0db91
1 parent 959ea07 commit 4dd5671

4 files changed

Lines changed: 84 additions & 19 deletions

File tree

packages/agent/src/handoff-checkpoint.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { readdir } from "node:fs/promises";
2+
import path from "node:path";
13
import { afterEach, describe, expect, it } from "vitest";
24
import { HandoffCheckpointTracker } from "./handoff-checkpoint";
35
import {
@@ -147,6 +149,18 @@ describe("HandoffCheckpointTracker", () => {
147149
expect(checkpoint).not.toBeNull();
148150
if (!checkpoint) return;
149151
expect(Object.keys(store.artifacts).length).toBeGreaterThan(0);
152+
const gitCommonDirRaw = await cloudRepo.git([
153+
"rev-parse",
154+
"--git-common-dir",
155+
]);
156+
const gitCommonDir = path.isAbsolute(gitCommonDirRaw)
157+
? gitCommonDirRaw
158+
: path.resolve(cloudRepo.path, gitCommonDirRaw);
159+
expect(
160+
(await readdir(gitCommonDir)).filter((entry) =>
161+
entry.startsWith("posthog-code-handoff-"),
162+
),
163+
).toEqual([]);
150164

151165
const applyTracker = createTracker(localRepo.path, apiClient);
152166
await applyTracker.applyFromHandoff(checkpoint);

packages/agent/src/handoff-checkpoint.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
22
import { tmpdir } from "node:os";
3-
import { dirname, join } from "node:path";
3+
import { join } from "node:path";
44
import {
55
type GitHandoffBranchDivergence,
66
type GitHandoffCheckpoint,
@@ -101,12 +101,10 @@ export class HandoffCheckpointTracker {
101101
indexArtifactPath: uploads.index?.storagePath,
102102
};
103103
} finally {
104-
const tempDir = capture.headPack?.path
105-
? dirname(capture.headPack.path)
106-
: dirname(capture.indexFile.path);
107-
await this.removeIfPresent(capture.headPack?.path);
108-
await this.removeIfPresent(capture.indexFile.path);
109-
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
104+
await rm(capture.artifactDirectory, {
105+
recursive: true,
106+
force: true,
107+
}).catch(() => {});
110108
}
111109
}
112110

packages/git/src/handoff.test.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,9 @@ async function makeCloudChanges(
103103
}
104104

105105
async function cleanupCapture(capture: GitHandoffCaptureResult): Promise<void> {
106-
if (capture.headPack?.path) {
107-
await rm(capture.headPack.path, { force: true }).catch(() => {});
108-
}
109-
await rm(capture.indexFile.path, { force: true }).catch(() => {});
106+
await rm(capture.artifactDirectory, { recursive: true, force: true }).catch(
107+
() => {},
108+
);
110109
}
111110

112111
async function captureAndApply(
@@ -145,6 +144,41 @@ async function captureAndApply(
145144
}
146145

147146
describe("GitHandoffTracker", () => {
147+
it("stores capture artifacts beside the git object store", async () => {
148+
await withRepos(async (repos) => {
149+
await makeCloudChanges(repos.cloudRepo, repos.cloudGit);
150+
151+
const captureTracker = new GitHandoffTracker({
152+
repositoryPath: repos.cloudRepo,
153+
});
154+
const capture = await captureTracker.captureForHandoff(
155+
repos.localGitState,
156+
);
157+
const gitCommonDir = (
158+
await repos.cloudGit.raw([
159+
"rev-parse",
160+
"--path-format=absolute",
161+
"--git-common-dir",
162+
])
163+
).trim();
164+
165+
try {
166+
expect(path.dirname(capture.artifactDirectory)).toBe(gitCommonDir);
167+
expect(path.dirname(path.dirname(capture.indexFile.path))).toBe(
168+
gitCommonDir,
169+
);
170+
if (!capture.headPack) {
171+
throw new Error("Expected handoff capture to include a pack file");
172+
}
173+
expect(path.dirname(path.dirname(capture.headPack.path))).toBe(
174+
gitCommonDir,
175+
);
176+
} finally {
177+
await cleanupCapture(capture);
178+
}
179+
});
180+
}, 15000);
181+
148182
it("captures and reapplies head, worktree, and index state from local files", async () => {
149183
await withRepos(async (repos) => {
150184
await makeCloudChanges(repos.cloudRepo, repos.cloudGit);

packages/git/src/handoff.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { spawn } from "node:child_process";
22
import { copyFile, mkdtemp, readFile, rm, stat } from "node:fs/promises";
3-
import { tmpdir } from "node:os";
43
import path from "node:path";
54
import type {
65
GitHandoffCheckpoint,
@@ -26,6 +25,7 @@ export interface GitHandoffArtifactFile {
2625

2726
export interface GitHandoffCaptureResult {
2827
checkpoint: GitHandoffCheckpoint;
28+
artifactDirectory: string;
2929
headPack?: GitHandoffArtifactFile;
3030
indexFile: GitHandoffArtifactFile;
3131
totalBytes: number;
@@ -92,7 +92,7 @@ export class GitHandoffTracker {
9292

9393
const checkpoint = result.data;
9494
const git = createGitClient(this.repositoryPath);
95-
const tempDir = await this.createTempDir(checkpoint.checkpointId);
95+
const tempDir = await this.createTempDir(git, checkpoint.checkpointId);
9696
const checkpointRef = `${CHECKPOINT_REF_PREFIX}${checkpoint.checkpointId}`;
9797

9898
try {
@@ -137,10 +137,14 @@ export class GitHandoffTracker {
137137
upstreamMergeRef: tracking.upstreamMergeRef,
138138
remoteUrl: tracking.remoteUrl,
139139
},
140+
artifactDirectory: tempDir,
140141
headPack,
141142
indexFile,
142143
totalBytes: (headPack?.rawBytes ?? 0) + indexFile.rawBytes,
143144
};
145+
} catch (error) {
146+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
147+
throw error;
144148
} finally {
145149
await deleteCheckpoint(git, checkpoint.checkpointId).catch(() => {});
146150
}
@@ -544,8 +548,27 @@ export class GitHandoffTracker {
544548
return exitCode === 0;
545549
}
546550

547-
private async createTempDir(checkpointId: string): Promise<string> {
548-
return mkdtemp(joinTempPrefix(checkpointId));
551+
private async createTempDir(
552+
git: GitClient,
553+
checkpointId: string,
554+
): Promise<string> {
555+
// Git stages packs in the object store, so the destination must share its filesystem.
556+
const gitCommonDir = await this.resolveGitCommonDir(git);
557+
return mkdtemp(
558+
path.join(gitCommonDir, `posthog-code-handoff-${checkpointId}-`),
559+
);
560+
}
561+
562+
private async resolveGitCommonDir(git: GitClient): Promise<string> {
563+
const raw = await git.raw([
564+
"rev-parse",
565+
"--path-format=absolute",
566+
"--git-common-dir",
567+
]);
568+
const resolved = raw.trim() || ".git";
569+
return path.isAbsolute(resolved)
570+
? resolved
571+
: path.resolve(this.repositoryPath, resolved);
549572
}
550573

551574
private async getGitPath(git: GitClient, gitPath: string): Promise<string> {
@@ -669,10 +692,6 @@ export class GitHandoffTracker {
669692
}
670693
}
671694

672-
function joinTempPrefix(checkpointId: string): string {
673-
return path.join(tmpdir(), `posthog-code-handoff-${checkpointId}-`);
674-
}
675-
676695
export async function readHandoffLocalGitState(
677696
repositoryPath: string,
678697
): Promise<HandoffLocalGitState> {

0 commit comments

Comments
 (0)