# Project export: Arena

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Arena makes agents compete to write the software that best serves you.
- Devpost: https://devpost.com/software/arena-lz2m84
- GitHub: https://github.com/TomBinford/treehacks/
- Video: https://www.youtube.com/embed/SbgnbmP28Js?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([Warp] Best Use of Warp Agents (Keychron Mechanical Keyboard for each team member))
- Team: 2 GitHub contributor(s) — Tom Binford (12 commits), Gavin Sonntag (10 commits)

## Devpost submission (written by the team)

### Inspiration

AI coding agents are powerful, but for frontend work they’re unreliable and hard to evaluate from diffs. We wanted a workflow where you can run multiple attempts in parallel and judge them by live UI previews, not PR spam.

### What it does

Arena lets you define a “job”, spawns multiple Warp Oz agents to implement the same frontend task on separate branches, monitors progress, and surfaces Vercel preview URLs of their attempts. You're able to try out each attempt on Vercel to see what you like, and then pick winner(s) that will open PRs to your repo. (Either for code review and approval or, with multiple candidates, to scrutinize their implementation approach and quality to pick a final winner).

### How we built it

Backend: Probot GitHub App + REST API for webhooks, getting data out of GitHub, and job management Agent execution: Warp Oz SDK to spawn and monitor parallel agent runs Deployment discovery: Polls GitHub deployments/workflows/commit statuses to detect Vercel preview URLs (optional Vercel API token to resolve dashboard URLs) Frontend: Next.js dashboard showing agent status, Warp session links, and iframe previews; selection UI + “Create PRs” action

### Challenges we ran into

Reliably detecting preview URLs across different GitHub/Vercel signals (deployments, workflow runs, commit statuses) Making sure our integrations (with Vercel and our custom GitHub App) were in place Orchestrating multiple long-running agent runs and deciding when a job is “done” in the presence of flakiness (We used a majority + idle timeout approach inspired by how to cook microwave popcorn)

### Accomplishments we're proud of

End-to-end workflow: job → parallel agents → monitoring → previews in the dashboard → PR creation from selected candidates Clean “preview-first” review experience (side-by-side iframes) that’s meaningfully better to use than clicking around a bunch of draft PRs Built a real orchestration layer around Warp agents with a clear architecture and extensible hooks

### What we learned

For frontend tasks, the best review artifact is a live deployment, not a diff Orchestrating agents is as much about observability and integration as it is about prompting Real-world reliability comes from handling messy edge cases in CI/deploy signals and timeouts

### What's next

Richer GitHub integration. Let users give an issue link instead of a prompt, for example, or leave comments on a draft PR to have the responsible agent make some changes. More control over agents. If you want something different, it should be easy to update the agents with more instructions or start over with changes to your prompt. Add persistence (DB) + branch cleanup + agent cancellation Enforce automated verification (running tests, but also tools like Stagehand by Browserbase to exercise UIs) to keep broken candidates from taking up human review

## README (from the GitHub repository)

Our TreeHacks project is Arena, an orchestration tool that makes agents compete to write the software that best serves you.

Traditional coding agents on GitHub result in one-shot PRs that require feedback iterations and sometimes are so bad that it's easier to start over. Arena overcomes this by 

To use:
- Have a repository in mind that is deployed to Vercel
- Follow the comments in .env.example to set up your backend .env
- From the repository root, run `npm install`, `npm run build` and `npm run dev`
- Go to localhost:3000 and add the bot to the repository you want the agents to work on
- Get a Warp account and set up an Oz environment with your preferred container. Use its ID for `WARP_ENVIRONMENT_ID`.
- Go to localhost:3001 and define a job!

Our devpost: https://devpost.com/software/arena-lz2m84


## Detected evidence (automated analysis)

Indexed codebase: 24 recognized source files, 96 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Express (technology) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (40 of 40)

```
.gitignore
backend/.dockerignore
backend/.env.example
backend/.gitignore
backend/app.yml
backend/CODE_OF_CONDUCT.md
backend/CONTRIBUTING.md
backend/Dockerfile
backend/LICENSE
backend/package.json
backend/README.md
backend/src/arena.ts
backend/src/github.ts
backend/src/index.ts
backend/src/store.ts
backend/src/vercel.ts
backend/src/warp.ts
backend/test/fixtures/issues.opened.json
backend/test/fixtures/mock-cert.pem
backend/test/index.test.ts
backend/tsconfig.json
backend/vitest.config.ts
frontend/.gitignore
frontend/app/globals.css
frontend/app/jobs/[id]/layout.tsx
frontend/app/jobs/[id]/page.tsx
frontend/app/layout.tsx
frontend/app/page.tsx
frontend/components/arena/AgentCard.tsx
frontend/components/arena/TerminalView.tsx
frontend/eslint.config.mjs
frontend/lib/api.ts
frontend/lib/types.ts
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/README.md
frontend/tsconfig.json
package.json
README.md
```

### Dependencies

- backend/package.json: @octokit/auth-app@^6.1.4, @octokit/rest@^22.0.1, @types/node@^20.0.0, @vercel/sdk@^1.19.1, concurrently@^9.0.0, nock@^14.0.5, oz-agent-sdk@^1.0.0-alpha.8, probot@^13.4.5, smee-client@^2.0.0, typescript@^5.8.3, vitest@^2.0.0
- frontend/package.json: @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.1.6, lucide-react@^0.564.0, next@16.1.6, react@19.2.3, react-dom@19.2.3, tailwindcss@^4, typescript@^5
- package.json: concurrently@^9.0.0

### Recent commits (newest first)

- Remove old .env.example stuff
- Update readme
- Improve .env.example
- Remove phrasing about github comments since we are skipping that
- Link to the actual Vercel deployments instead of the deployment details page
- Link to the actual Vercel deployments instead of the deployment details page
- added github pr creation and changed ux flow
- Clarify UI wording -- TODO link straight to the deployed code
- Don't default installation ID to anything because there is no sane default
- fix vercel
- merge
- possibly fixed backend
- Non-working integration with GitHub Actions trying to poll until the Vercel deployment is done
- remove mock data
- Merge branch 'main' of github.com:TomBinford/treehacks
- add multiple model support
- Put arena in the branch name
- Change prompt to stop opening PRs
- Redo package-lock files
- viz: add repo selector

## Key source files (fetched from GitHub, selected and truncated for size)

### backend/CONTRIBUTING.md

```markdown
## Contributing

[fork]: /fork
[pr]: /compare
[code-of-conduct]: CODE_OF_CONDUCT.md

Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.

Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms.

## Issues and PRs

If you have suggestions for how this project could be improved, or want to report a bug, open an issue! We'd love all and any contributions. If you have questions, too, we'd love to hear them.

We'd also love PRs. If you're thinking of a large PR, we advise opening up an issue first to talk about it, though! Look at the links below if you're not sure how to open a PR.

## Submitting a pull request

1. [Fork][fork] and clone the repository.
1. Configure and install the dependencies: `npm install`.
1. Make sure the tests pass on your machine: `npm test`, note: these tests also apply the linter, so there's no need to lint separately.
1. Create a new branch: `git checkout -b my-branch-name`.
1. Make your change, add tests, and make sure the tests still pass.
1. Push to your fork and [submit a pull request][pr].
1. Pat your self on the back and wait for your pull request to be reviewed and merged.

Here are a few things you can do that will increase the likelihood of your pull request being accepted:

- Write and update tests.
- Keep your changes as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).

Work in Progress pull requests are also welcome to get feedback early on, or if there is something blocked you.

## Resources

- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [GitHub Help](https://help.github.com)

```

### backend/CODE_OF_CONDUCT.md

```markdown
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

- The use of sexualized language or imagery and unwelcome sexual attention or
  advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
  address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at . All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of 
[truncated — 208 more characters]
```

### package.json

```
{
  "name": "treehacks",
  "private": true,
  "workspaces": ["backend", "frontend"],
  "scripts": {
    "dev": "concurrently -n backend,frontend \"npm run start -w backend\" \"npm run dev -w frontend\"",
    "build": "npm run build -w backend",
    "start": "npm run start -w backend"
  },
  "devDependencies": {
    "concurrently": "^9.0.0"
  }
}

```

### backend/Dockerfile

```
FROM node:20-slim
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm ci --production
RUN npm cache clean --force
ENV NODE_ENV="production"
COPY . .
CMD [ "npm", "start" ]

```

### frontend/package.json

```
{
  "name": "arena-frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev -p 3001",
    "build": "next build",
    "start": "next start -p 3001",
    "lint": "eslint"
  },
  "dependencies": {
    "lucide-react": "^0.564.0",
    "next": "16.1.6",
    "react": "19.2.3",
    "react-dom": "19.2.3"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### backend/package.json

```
{
  "name": "treehacks",
  "version": "1.0.0",
  "private": true,
  "description": "A Probot app",
  "author": "Tom, Ritvik, Gavin",
  "license": "ISC",
  "homepage": "https://github.com//",
  "keywords": [
    "probot",
    "github",
    "probot-app"
  ],
  "scripts": {
    "build": "tsc",
    "start": "probot run ./lib/index.js",
    "dev": "concurrently -n probot,frontend \"npm run start\" \"npm run dev:frontend\"",
    "dev:frontend": "cd ../frontend && npm run dev",
    "test": "vitest"
  },
  "dependencies": {
    "@octokit/auth-app": "^6.1.4",
    "@octokit/rest": "^22.0.1",
    "@vercel/sdk": "^1.19.1",
    "oz-agent-sdk": "^1.0.0-alpha.8",
    "probot": "^13.4.5"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "concurrently": "^9.0.0",
    "nock": "^14.0.5",
    "smee-client": "^2.0.0",
    "typescript": "^5.8.3",
    "vitest": "^2.0.0"
  },
  "overrides": {
    "@types/pg": "8.15.1"
  },
  "engines": {
    "node": ">= 18"
  },
  "type": "module"
}

```

### frontend/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Arena - Mission Control for AI Agents",
  description: "Arena: Visual verification first. Compare Warp agent candidates and select the winner.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased bg-slate-950 text-slate-200`}
      >
        {children}
      </body>
    </html>
  );
}

```

### backend/src/index.ts

```typescript
import { Probot, Context } from "probot";
import type { ApplicationFunctionOptions } from "probot";
import { createJobAndSpawnAgents, mountArenaRoutes } from "./arena.js";

export default (app: Probot, options: ApplicationFunctionOptions) => {
  if (options.getRouter) {
    const router = options.getRouter("/api");
    mountArenaRoutes(router);
  }

  const ARENA_TRIGGER = /arena\b/i;

  async function spawnAndReply(
    context: Context<"issue_comment.created">,
    params: { issueId: number; repoName: string; issueTitle: string; issueDescription: string; githubRepoOwner: string; githubRepoName: string }
  ) {
    try {
      const { arenaUrl, sessionLinks } = await createJobAndSpawnAgents(params);
      const watchSection =
        sessionLinks.filter(Boolean).length > 0
          ? `\n\n**Watch agents live:**\n${sessionLinks.filter(Boolean).map((link, i) => `- [Agent ${i + 1}](${link})`).join("\n")}`
          : "";
      await context.octokit.issues.createComment(
        context.issue({
          body: `🚀 **Arena is on it!** ${sessionLinks.length} Warp agent(s) are working on this.\n\n**[View progress →](${arenaUrl})**${watchSection}`,
        })
      );
    } catch (err) {
      console.error("Failed to create arena job:", err);
      await context.octokit.issues.createComment(
        context.issue({
          body: `⚠️ Arena could not start agents. Please check WARP_API_KEY is set.\n\nError: ${err instanceof Error ? err.message : String(err)}`,
        })
      );
    }
  }

  app.on("issue_comment.created", async (context) => {
    const { comment, issue, repository } = context.payload;
    if (issue.pull_request) return; // only issues, not PRs
    if (!ARENA_TRIGGER.test(comment.body)) return;

    const [owner, repo] = repository.full_name.split("/");
    const issueDescription = `${issue.body ?? ""}\n\n---\nRequested changes (from comment):\n${comment.body}`.trim();

    await spawnAndReply(context, {
      issueId: issue.number,
      repoName: repository.full_name,
      issueTitle: issue.title,
      issueDescription,
      githubRepoOwner: owner,
      githubRepoName: repo,
    });
  });
};

```

### frontend/app/page.tsx

```typescript
'use client';

import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { Github, Loader2, Rocket, CheckCircle, Clock, Plus, Trash2 } from 'lucide-react';
import { fetchJobs, createJob } from '@/lib/api';
import type { Job } from '@/lib/types';
import type { AgentSlot } from '@/lib/api';

const MODEL_OPTIONS: { id: string; label: string }[] = [
  { id: 'claude-4-sonnet', label: 'Claude 4 Sonnet' },
  { id: 'claude-opus-4-6', label: 'Claude Opus 4.6' },
  { id: 'claude-opus-4-5', label: 'Claude Opus 4.5' },
  { id: 'claude-sonnet-4-5', label: 'Claude Sonnet 4.5' },
  { id: 'claude-opus-4-1', label: 'Claude Opus 4.1' },
  { id: 'claude-haiku-4-5', label: 'Claude Haiku 4.5' },
  { id: 'claude-sonnet-4', label: 'Claude Sonnet 4' },
];

const REPO_OPTIONS = [
  'gsonntag/treehacks-testing-repo',
  'TomBinford/treehacks-testing-repo',
] as const;

function statusToLabel(status: Job['status']): string {
  switch (status) {
    case 'processing':
      return 'Coding (Warp)';
    case 'review_needed':
      return 'Ready for Review';
    case 'completed':
      return 'Completed';
    default:
      return status;
  }
}

function StatusBadge({ status }: { status: Job['status'] }) {
  const isProcessing = status === 'processing';
  const isReady = status === 'review_needed';
  const isCompleted = status === 'completed';

  return (
    <span
      className={`inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm font-medium border ${isProcessing
        ? 'bg-amber-900/30 text-amber-400 border-amber-800'
        : isReady
          ? 'bg-emerald-900/30 text-emerald-400 border-emerald-800'
          : isCompleted
            ? 'bg-slate-800 text-slate-400 border-slate-700'
            : 'bg-slate-800 text-slate-400 border-slate-700'
        }`}
    >
      {isProcessing && <Loader2 className="w-4 h-4 animate-spin" />}
      {isReady && <CheckCircle className="w-4 h-4" />}
      {isCompleted && <CheckCircle className="w-4 h-4" />}
      {status === 'processing' && <Clock className="w-4 h-4" />}
      {statusToLabel(status)}
    </span>
  );
}

export default function LobbyPage() {
  const router = useRouter();
  const [jobs, setJobs] = useState<Job[]>([]);
  const [loading, setLoading] = useState(true);
  const [showForm, setShowForm] = useState(false);
  const [creating, setCreating] = useState(false);
  const [formError, setFormError] = useState<string | null>(null);
  const [form, setForm] = useState({ repoName: '', issueTitle: '', issueDescription: '' });
  const [agentSlots, setAgentSlots] = useState<AgentSlot[]>([{ count: 1, modelId: 'claude-4-sonnet' }]);

  const totalAgents = agentSlots.reduce((sum, s) => sum + s.count, 0);
  const canAddSlot = agentSlots.length < 10 && totalAgents < 10;

  const handleCreateJob = async (e: React.FormEvent) => {
    e.preventDefault();
    setFormError(null);
    if (totalAgents < 1 || totalAgents > 10) {
      setFormError('Total agents must be between 1 and 10');
      return;
    }
    setCreating(true);
    try {
      const { arenaUrl, jobId } = await createJob({
        repoName: form.repoName.trim(),
        issueTitle: form.issueTitle.trim(),
        issueDescription: form.issueDescription.trim(),
        agentConfigs: agentSlots,
      });
      setShowForm(false);
      setForm({ repoName: '', issueTitle: '', issueDescription: '' });
      setAgentSlots([{ count: 1, modelId: 'claude-4-sonnet' }]);
      router.push(`/jobs/${jobId}`);
    } catch (err) {
      setFormError(err instanceof Error ? err.message : 'Failed to create job');
    } finally {
      setCreating(false);
    }
  };

  useEffect(() => {
    let cancelled = false;
    async function load() {
      try {
        const data = await fetchJobs();
        if (!cancelled) setJobs(data);
      } finally {
        if (!cancelled) setLoading(false);
      }
    }
    load();
    const interval = setInterval(load, 5000);
    return () => {
      cancelled = true;
      clearInterval(interval);
    };
  }, []);

  return (
    <div className="min-h-screen bg-slate-950 text-slate-200">
      {/* Header */}
      <header className="border-b border-slate-800 px-6 py-6">
        <div className="max-w-6xl mx-auto flex items-center justify-between">
          <h1 className="text-2xl font-bold text-white flex items-center gap-3">
            <div className="w-10 h-10 rounded-lg bg-gradient-to-br from-violet-500 to-fuchsia-600 flex items-center justify-center">
              <Github className="w-5 h-5 text-white" />
            </div>
            Arena
          </h1>
          <p className="text-slate-400 text-sm">
            Mission Control for AI Agents
          </p>
        </div>
      </header>

      <main className="max-w-6xl mx-auto px-6 py-8">
        <div className="mb-8 flex items-center justify-between">
          <div>
            <h2 className="text-xl font-semibold text-white mb-1">
              Active Jobs
            </h2>
            <p className="text-slate-500 text-sm">
              Monitor and start groups of agents here
            </p>
          </div>
          <button
            onClick={() => setShowForm(!showForm)}
            className="flex items-center gap-2 px-4 py-2 rounded-lg bg-violet-600 hover:bg-violet-500 text-white font-medium transition-colors"
          >
            <Plus className="w-4 h-4" />
            New job
          </button>
        </div>

        {showForm && (
          <form
            onSubmit={handleCreateJob}
            className="mb-8 rounded-xl border border-slate-800 bg-slate-900/50 p-6 space-y-4"
          >
            <h3 className="font-semibold text-white">Create a new job</h3>
            <div>
              <label className="block text-sm text-slate-400 mb-1">Repo (owner/name)</label>
              <select
                value={form.repoName}
                onChange={(e) => setForm((f) => ({ ...f, repoName: e.target.value }))}
                cl
[truncated — 7753 more characters]
```

### frontend/app/jobs/[id]/layout.tsx

```typescript
export default function JobLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return children;
}

```

[15 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]