Skip to content

Commit e95660e

Browse files
committed
screenshot and worktree fixes
1 parent 9cd0bfc commit e95660e

5 files changed

Lines changed: 74 additions & 59 deletions

File tree

scripts/eternity-loop/bootstrap.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,36 @@ export async function bootstrap(args: string[]): Promise<void> {
3737

3838
// Remove existing worktree
3939
try {
40-
await $`git worktree remove --force ${worktreePath}`.quiet();
40+
await $`git -C ${repoRoot} worktree remove --force --force ${worktreePath}`.quiet();
4141
} catch {
42-
try {
43-
await $`rm -rf ${worktreePath}`.quiet();
44-
} catch {
45-
// Already gone
46-
}
42+
// Continue with hard cleanup below.
43+
}
44+
45+
// Clear stale worktree metadata and path left by interrupted runs.
46+
try {
47+
await $`git -C ${repoRoot} worktree prune --expire now`.quiet();
48+
} catch {
49+
// Best effort
50+
}
51+
52+
try {
53+
await $`rm -rf ${worktreePath}`.quiet();
54+
} catch {
55+
// Already gone
4756
}
4857

4958
// Create fresh worktree detached on origin/main
5059
await $`mkdir -p ${dirname(worktreePath)}`;
51-
await $`git fetch origin`.quiet();
60+
await $`git -C ${repoRoot} fetch origin`.quiet();
5261

5362
let mainRef = "origin/main";
5463
try {
55-
await $`git rev-parse --verify origin/main`.quiet();
64+
await $`git -C ${repoRoot} rev-parse --verify origin/main`.quiet();
5665
} catch {
5766
mainRef = "origin/master";
5867
}
5968

60-
await $`git worktree add ${worktreePath} --detach ${mainRef}`;
69+
await $`git -C ${repoRoot} worktree add --force ${worktreePath} --detach ${mainRef}`;
6170

6271
spinner.stop();
6372

scripts/eternity-loop/eternity-loop-prompts/ralph-claude-md.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,4 @@ If there are still stories with `passes: false`, end your response normally (ano
8181
- Keep CI green
8282
- Do NOT switch branches - stay on the current branch at all times
8383
- Read the Codebase Patterns section in progress.txt before starting
84+
- Never commit ralph files

scripts/eternity-loop/git.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,13 @@ export async function ensureMainBranch(workDir: string): Promise<void> {
2121
export async function checkoutBranch(workDir: string, branch: string): Promise<void> {
2222
await $`git -C ${workDir} fetch origin`.quiet();
2323

24-
// Try checking out existing branch first, create from remote if available
24+
// Force-create (or reset) the local branch to match the remote, then check it out.
25+
// Using -B avoids errors when the branch already exists (e.g. checked out in another worktree).
2526
try {
26-
await $`git -C ${workDir} checkout ${branch}`.quiet();
27+
await $`git -C ${workDir} checkout -B ${branch} origin/${branch}`.quiet();
2728
} catch {
28-
try {
29-
await $`git -C ${workDir} checkout -b ${branch} origin/${branch}`.quiet();
30-
} catch {
31-
await $`git -C ${workDir} checkout -b ${branch}`;
32-
}
33-
}
34-
35-
// Pull latest from origin if the remote branch exists
36-
try {
37-
await $`git -C ${workDir} reset --hard origin/${branch}`.quiet();
38-
} catch {
39-
// Remote branch may not exist yet for new branches
29+
// Remote branch doesn't exist yet — create a new local branch
30+
await $`git -C ${workDir} checkout -B ${branch}`.quiet();
4031
}
4132
}
4233

@@ -52,8 +43,9 @@ export async function getCurrentBranch(workDir: string): Promise<string> {
5243

5344
export async function getLatestCommitDate(workDir: string, branch?: string): Promise<string> {
5445
const ref = branch ? `origin/${branch}` : "HEAD";
55-
const result = await $`git -C ${workDir} log -1 --format=%ad --date=format-local:%Y-%m-%dT%H:%M:%SZ ${ref}`.text();
56-
return result.trim();
46+
const result = await $`git -C ${workDir} log -1 --format=%ct ${ref}`.text();
47+
const epochSeconds = Number.parseInt(result.trim(), 10);
48+
return new Date(epochSeconds * 1000).toISOString();
5749
}
5850

5951
export async function getLatestCommitMessage(workDir: string, branch?: string): Promise<string> {

scripts/eternity-loop/github/pr.ts

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { Octokit } from "@octokit/rest";
22
import { graphql } from "@octokit/graphql";
33
import { join, basename } from "node:path";
4+
import { logDebug } from "../logger";
5+
import { log } from "node:console";
46

57
export async function findPrByBranch(
68
octokit: Octokit,
@@ -235,6 +237,8 @@ export async function getPrComments(
235237
const allComments = await fetchAllPrComments(octokit, owner, repo, prNumber);
236238
const humanComments = allComments.filter((c) => !c.isBot);
237239

240+
logDebug(`[allComments] Fetched ${allComments.length} total comments (${humanComments.length} human) for PR #${prNumber}, latest cutoff: ${cutoffDate}, latest comment date: ${humanComments.map(c => c.createdAt).sort().reverse()}`);
241+
238242
if (cutoffDate) {
239243
const cutoff = new Date(cutoffDate);
240244
const newComments = humanComments.filter((c) => new Date(c.createdAt) > cutoff);
@@ -253,6 +257,7 @@ export async function checkForNewHumanComments(
253257
latestCommitDate: string,
254258
): Promise<boolean> {
255259
const { new: newComments } = await getPrComments(octokit, owner, repo, prNumber, latestCommitDate);
260+
logDebug(`[checkForNewHumanComments] Found ${newComments.length} new human comment(s) since ${latestCommitDate}`);
256261
return newComments.length > 0;
257262
}
258263

@@ -315,27 +320,19 @@ export function extractScreenshotPaths(progressContent: string, workDir: string)
315320
return paths;
316321
}
317322

318-
/**
319-
* Get the relative path of a file within a git repo.
320-
*/
321-
async function getRelativePath(workDir: string, absolutePath: string): Promise<string> {
322-
// If path is already relative (starts within workDir), extract relative portion
323-
if (absolutePath.startsWith(workDir)) {
324-
return absolutePath.slice(workDir.length).replace(/^\//, "");
325-
}
326-
// Otherwise try git to resolve
327-
try {
328-
const result = await Bun.$`git -C ${workDir} ls-files --full-name ${absolutePath}`.text();
329-
return result.trim();
330-
} catch {
331-
return basename(absolutePath);
332-
}
323+
function getImageMimeType(filePath: string): string | null {
324+
const lower = filePath.toLowerCase();
325+
if (lower.endsWith(".png")) return "image/png";
326+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
327+
if (lower.endsWith(".gif")) return "image/gif";
328+
if (lower.endsWith(".webp")) return "image/webp";
329+
if (lower.endsWith(".svg")) return "image/svg+xml";
330+
return null;
333331
}
334332

335333
/**
336334
* Upload screenshots referenced in progress.txt to the PR as a comment.
337-
* Screenshots that are committed to the branch are referenced via raw GitHub URLs.
338-
* Screenshots not yet tracked are committed first, then referenced.
335+
* This does not commit files; it only posts screenshots to the PR conversation.
339336
*/
340337
export async function uploadProgressScreenshots(
341338
octokit: Octokit,
@@ -344,44 +341,56 @@ export async function uploadProgressScreenshots(
344341
prNumber: number,
345342
progressContent: string,
346343
workDir: string,
347-
branch: string,
344+
_branch: string,
348345
): Promise<void> {
349346
const screenshotPaths = extractScreenshotPaths(progressContent, workDir);
350347
if (screenshotPaths.length === 0) return;
351348

352-
const imageEntries: Array<{ name: string; url: string }> = [];
349+
const imageEntries: Array<{ name: string; dataUrl: string }> = [];
350+
const skippedEntries: string[] = [];
351+
const MAX_IMAGE_BYTES = 1_500_000;
353352

354353
for (const filePath of screenshotPaths) {
355354
const file = Bun.file(filePath);
356355
if (!(await file.exists())) continue;
357356

358-
const relativePath = await getRelativePath(workDir, filePath);
359357
const name = basename(filePath);
358+
const mimeType = getImageMimeType(filePath);
359+
if (!mimeType) {
360+
skippedEntries.push(`${name} (unsupported image type)`);
361+
continue;
362+
}
363+
364+
const size = file.size;
365+
if (typeof size === "number" && size > MAX_IMAGE_BYTES) {
366+
skippedEntries.push(`${name} (too large: ${(size / 1024 / 1024).toFixed(2)} MB)`);
367+
continue;
368+
}
360369

361-
// Check if file is tracked in git
362370
try {
363-
await Bun.$`git -C ${workDir} ls-files --error-unmatch ${filePath}`.quiet();
371+
const arrayBuffer = await file.arrayBuffer();
372+
const base64 = Buffer.from(arrayBuffer).toString("base64");
373+
const dataUrl = `data:${mimeType};base64,${base64}`;
374+
imageEntries.push({ name, dataUrl });
364375
} catch {
365-
// File not tracked - add and commit it
366-
try {
367-
await Bun.$`git -C ${workDir} add ${filePath}`.quiet();
368-
await Bun.$`git -C ${workDir} commit -m ${"chore: add screenshot " + name}`.quiet();
369-
} catch {
370-
continue; // Skip if we can't commit
371-
}
376+
skippedEntries.push(`${name} (failed to read file)`);
377+
continue;
372378
}
373-
374-
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${relativePath}`;
375-
imageEntries.push({ name, url: rawUrl });
376379
}
377380

378-
if (imageEntries.length === 0) return;
381+
if (imageEntries.length === 0 && skippedEntries.length === 0) return;
379382

380383
const body = [
381384
`🤖 **eternity-loop bot:** Screenshots from this run:\n`,
382385
...imageEntries.map((entry, i) =>
383-
`### Screenshot ${i + 1}\n![${entry.name}](${entry.url})\n`
386+
`### Screenshot ${i + 1}\n![${entry.name}](${entry.dataUrl})\n`
384387
),
388+
...(skippedEntries.length > 0
389+
? [
390+
"### Skipped files",
391+
...skippedEntries.map((entry) => `- ${entry}`),
392+
]
393+
: []),
385394
].join("\n");
386395

387396
await postPrComment(octokit, owner, repo, prNumber, body);

scripts/eternity-loop/workflows/review.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,16 @@ export class ReviewWorkflow implements Workflow {
3333
});
3434

3535
const candidates = [...inReview, ...inProgress];
36+
logDebug(`[review] Found ${candidates.length} candidate(s) in "In Review" or "In Progress" with prd label`);
3637
const octokit = createGitHubClient();
3738
const { owner, repo } = await getRepoInfo(ctx.workDir);
3839

40+
logDebug(`[review] Checking candidates for new PR comments since last commit in ${owner}/${repo}...`);
41+
3942
let commentCount = 0;
4043
for (const issue of candidates) {
4144
const pr = await findPrByBranch(octokit, owner, repo, issue.branchName);
45+
logDebug(`[review] Checking issue ${issue.identifier} (branch: ${issue.branchName}) - found PR: ${pr ? pr.number : "none"}`);
4246
if (!pr) continue;
4347

4448
const latestCommitDate = await getLatestCommitDate(ctx.workDir, issue.branchName);

0 commit comments

Comments
 (0)