Skip to content

Ralph/eternity loop bun rewrite#13

Merged
robertherber merged 23 commits intomainfrom
ralph/eternity-loop-bun-rewrite
Mar 5, 2026
Merged

Ralph/eternity loop bun rewrite#13
robertherber merged 23 commits intomainfrom
ralph/eternity-loop-bun-rewrite

Conversation

@robertherber
Copy link
Member

No description provided.

robertherber and others added 18 commits March 4, 2026 13:37
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ain loop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 4, 2026 13:37
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a Bun + TypeScript implementation of the “eternity-loop” automation, replacing/modernizing the previous shell-driven approach with typed workflows that integrate Linear, Git, and GitHub.

Changes:

  • Adds a Bun/TypeScript CLI (scripts/eternity-loop/index.ts) that bootstraps a tmux + git worktree session and runs an infinite workflow loop.
  • Implements three workflows (review, CI-fix, new-feature) with Linear querying + GitHub PR/CI interactions and Claude runner prompts.
  • Adds project-level Bun/TS plumbing (package.json, bun.lock, tsconfig.json) and prompt templates under scripts/eternity-loop/eternity-loop-prompts/.

Reviewed changes

Copilot reviewed 25 out of 28 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tsconfig.json TypeScript config for Bun-based scripts build/typecheck.
package.json Declares Bun/TS dependencies for the new eternity-loop tool.
bun.lock Locks dependency graph for the new Bun package.
.gitignore Ignores node_modules/.
scripts/eternity-loop/index.ts Main loop runner (settings bootstrap, workflow selection, Ralph execution).
scripts/eternity-loop/bootstrap.ts tmux + worktree bootstrap and cleanup logic.
scripts/eternity-loop/config.ts Persists/loads loop settings under .eternity-loop/.
scripts/eternity-loop/logger.ts Timestamped logging helpers.
scripts/eternity-loop/prompts.ts Loads prompt templates and skill guidelines.
scripts/eternity-loop/ai-runner.ts Claude CLI runner abstraction.
scripts/eternity-loop/git.ts Git helpers for branch management and metadata.
scripts/eternity-loop/types.ts Shared types for settings, issues, and workflow context.
scripts/eternity-loop/providers/types.ts Provider interface + filtering model.
scripts/eternity-loop/providers/linear.ts Linear provider implementation for issue discovery and transitions.
scripts/eternity-loop/github/client.ts Octokit client creation + repo owner/name discovery.
scripts/eternity-loop/github/pr.ts PR lookup/creation/commenting + PR comment harvesting logic.
scripts/eternity-loop/github/ci.ts CI failure detection + failure detail extraction.
scripts/eternity-loop/ralph.ts Ralph execution loop, progress archiving, and notifications.
scripts/eternity-loop/workflows/types.ts Workflow interface definition.
scripts/eternity-loop/workflows/review.ts Review workflow: detect new human comments, generate PRD, reply summary.
scripts/eternity-loop/workflows/ci-fix.ts CI-fix workflow: detect failures, generate CI-fix PRD, post progress.
scripts/eternity-loop/workflows/new-feature.ts New-feature workflow: move Linear issue to in-progress, generate PRD, create PR.
scripts/eternity-loop/eternity-loop-prompts/* Prompt templates for PRD creation, PR creation, and comment replies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to +68
const argsStr = args.join(" ");
const envVars = [
`ETERNITY_LOOP_INSIDE=1`,
`ETERNITY_LOOP_WORKTREE='${worktreePath}'`,
`ETERNITY_LOOP_REPO_ROOT='${repoRoot}'`,
`LINEAR_API_KEY='${process.env.LINEAR_API_KEY ?? ""}'`,
].join(" ");

const cmd = `cd '${worktreePath}' && ${envVars} bun '${scriptPath}' ${argsStr}; echo 'Eternity loop exited. Press enter to close.'; read`;
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tmux command embeds LINEAR_API_KEY directly into the shell command string. This leaks the secret via process listings and (depending on tmux config) can persist in scrollback/logs. Pass secrets via the spawned process environment (or tmux set-environment) rather than interpolating them into cmd.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +68
// Build the command to run inside tmux
const argsStr = args.join(" ");
const envVars = [
`ETERNITY_LOOP_INSIDE=1`,
`ETERNITY_LOOP_WORKTREE='${worktreePath}'`,
`ETERNITY_LOOP_REPO_ROOT='${repoRoot}'`,
`LINEAR_API_KEY='${process.env.LINEAR_API_KEY ?? ""}'`,
].join(" ");

const cmd = `cd '${worktreePath}' && ${envVars} bun '${scriptPath}' ${argsStr}; echo 'Eternity loop exited. Press enter to close.'; read`;
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

argsStr = args.join(" ") is inserted into a shell command executed by tmux. This breaks when args contain spaces/shell metacharacters and also allows shell injection from crafted arguments. Prefer passing arguments without going through a shell string (e.g., invoke tmux new-session ... -- bun scriptPath ...args if supported) or robustly shell-escape each arg.

Copilot uses AI. Check for mistakes.
const repoRoot = (await $`git rev-parse --show-toplevel`.text()).trim();
const projectName = repoRoot.split("/").pop() ?? "unknown";
const tmuxSession = `eternity-loop-${projectName}`;
const worktreePath = join(repoRoot, ".claude/worktrees/eternity-loop");
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This worktree path is inside the repo under .claude/worktrees/..., but existing session tooling in this repo places worktrees under $HOME/code/worktrees/<repo-name>/... (see scripts/session-init.sh:50-51). Using a different location can lead to duplicated conventions and accidental repo pollution; consider aligning the location or making it configurable.

Suggested change
const worktreePath = join(repoRoot, ".claude/worktrees/eternity-loop");
const defaultWorktreeRoot = join(repoRoot, ".claude/worktrees");
const externalWorktreeRoot = process.env.HOME
? join(process.env.HOME, "code", "worktrees", projectName)
: defaultWorktreeRoot;
const worktreePath = join(externalWorktreeRoot, "eternity-loop");

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +31
export async function checkoutBranch(workDir: string, branch: string): Promise<void> {
// Try checking out existing branch first, create from remote if available
try {
await $`git -C ${workDir} checkout ${branch}`.quiet();
} catch {
try {
await $`git -C ${workDir} checkout -b ${branch} origin/${branch}`.quiet();
} catch {
await $`git -C ${workDir} checkout -b ${branch}`;
}
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkoutBranch never fetches from origin. In a long-running loop, newly created/updated remote branches may not exist in local origin/* refs, causing git checkout -b ... origin/branch to fail or the agent to work on stale commits. Fetch (optionally targeted to the branch) before attempting the checkout, or fetch once per loop iteration.

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +133
const { data: logData } = await octokit.actions.downloadJobLogsForWorkflowRun({
owner,
repo,
job_id: failedJob.id,
});

const log = typeof logData === "string" ? logData : String(logData);
const lines = log.split("\n");

if (lines.length > 500) {
const first100 = lines.slice(0, 100).join("\n");
const last400 = lines.slice(-400).join("\n");
parts.push("```");
parts.push(first100);
parts.push("\n... (truncated) ...\n");
parts.push(last400);
parts.push("```");
} else {
parts.push("```");
parts.push(log);
parts.push("```");
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

downloadJobLogsForWorkflowRun returns a zipped/binary payload (and often via redirect), not plain text. Converting it with String(logData) and splitting on newlines will usually produce unusable output (e.g., [object ArrayBuffer]) and won’t yield CI logs. Either omit inline logs and include details_url, or download and unzip the logs before parsing, or use an API that returns text output.

Suggested change
const { data: logData } = await octokit.actions.downloadJobLogsForWorkflowRun({
owner,
repo,
job_id: failedJob.id,
});
const log = typeof logData === "string" ? logData : String(logData);
const lines = log.split("\n");
if (lines.length > 500) {
const first100 = lines.slice(0, 100).join("\n");
const last400 = lines.slice(-400).join("\n");
parts.push("```");
parts.push(first100);
parts.push("\n... (truncated) ...\n");
parts.push(last400);
parts.push("```");
} else {
parts.push("```");
parts.push(log);
parts.push("```");
parts.push("Logs are available in GitHub Actions:");
if (check.details_url) {
parts.push(check.details_url);
} else {
parts.push("(No details URL available for this check)");

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +91
await Bun.$`MESSAGE=${message} ${scriptPath}`.env({
...process.env,
DISABLE_PUSHOVER_NOTIFICATIONS: undefined as unknown as string,
RALPH_LOOP: undefined as unknown as string,
MESSAGE: message,
}).quiet();
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using undefined as unknown as string to "unset" env vars is type-unsafe and may not actually remove the variables (it can end up setting them to the literal string "undefined" depending on how Bun handles the env map). Build an env object, delete the keys you want to remove, and pass the cleaned object to .env() instead.

Suggested change
await Bun.$`MESSAGE=${message} ${scriptPath}`.env({
...process.env,
DISABLE_PUSHOVER_NOTIFICATIONS: undefined as unknown as string,
RALPH_LOOP: undefined as unknown as string,
MESSAGE: message,
}).quiet();
const env = {
...process.env,
MESSAGE: message,
};
delete (env as Record<string, string | undefined>).DISABLE_PUSHOVER_NOTIFICATIONS;
delete (env as Record<string, string | undefined>).RALPH_LOOP;
await Bun.$`MESSAGE=${message} ${scriptPath}`.env(env).quiet();

Copilot uses AI. Check for mistakes.
robertherber and others added 5 commits March 4, 2026 23:02
The eternity-loop has been fully rewritten in TypeScript/Bun.
The prompts now live at scripts/eternity-loop/eternity-loop-prompts/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@robertherber robertherber merged commit cd4dd61 into main Mar 5, 2026
2 checks passed
@robertherber robertherber deleted the ralph/eternity-loop-bun-rewrite branch March 5, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants