# Project export: AlphaResearch

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: UC Berkeley AI Hackathon 2026
- Tagline: Recursive Sandboxed Agents for Autonomous Research at Scale
- Devpost: https://devpost.com/software/alpharesearch-9fwk4m
- GitHub: https://github.com/JasonLai150/AlphaResearch
- Demo: https://demo-eosin-seven-69.vercel.app/
- Video: https://www.youtube.com/embed/LcH4rdKbF6Y?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Best Use of Cognition)
- Team: 7 GitHub contributor(s) — Claude Opus 4.8 (73 commits), Aditya (55 commits), Sam Chen (55 commits), JasonLai150 (22 commits), Aditya Bilawar (9 commits), ehulewicz (3 commits), copilot-swe-agent[bot] (1 commits)

## Devpost submission (written by the team)

### Inspiration

In 2026, the AI community has gone all in on two ideas at once: agents that can act on their own, and continual learning that lets models keep getting better. The most ambitious version of the field sits right where those two meet. Automating research itself, and specifically AI research, so that a system can start to improve itself. The rough shape of this has felt possible for a while. When AutoGPT showed up in 2023 it proved you could chain a model into something that plans and acts, even if it mostly spun in circles. The tooling has caught up a lot since then. Andrej Karpathy called natural language the hottest new programming language, and leaders at frontier labs now talk openly about a large and growing share of their code being written by models instead of by people. We have spent real time doing research in reinforcement learning, interpretability, and world models, so we know the work from the inside. Research is slow, technical, and unforgiving. Most of the hours do not go into having ideas, they go into setting up experiments, babysitting training runs, reading results, and deciding what to try next. That loop is the actual bottleneck, and it is a loop. So we asked the obvious question. What if we could automate it?

### What it does

AlphaResearch is an autonomous researcher. We use agent harnesses and sandbox environments (w/ root level access) to run research experiments in parallel in a systematic way. You start by chatting with a lead agent that behaves like a principal investigator. It pins down the research goal with you, looks at existing literature, and turns a vague direction into a concrete, testable plan. From there it dispatches a fleet of researcher agents that go off and do the actual work. Each researcher runs its own experiments in its own sandbox, tests its assigned idea, iterates, and reports back with strong, opinionated, evidence-based claims rather than vibes. The PI(Principal Investigator) agent then reconciles what came back and decides what is real. There are two ways to run it. The first is interactive, where you stay in the loop as the PI and steer. The second is fully autonomous. You seed an idea and the platform runs the research loop on its own, round after round, until the PI agent decides it has findings worth keeping. That loop can run for minutes or for days. Every researcher works against a fixed budget and a bounded depth, so the system cannot quietly spawn ten thousand jobs or burn an unbounded bill. Results are not just text either. Researchers produce real training curves, metrics, and experiment dashboards that flow back to the PI as artifacts, so a claim arrives with the evidence attached. Right now we focus on reinforcement learning and machine learning problems, where we can actually train, simulate, and learn from the results inside the system.

### How we built it

We built both the PI agent and the researcher agents on a Claude Code agent harness, using skills, subagents, tools, and hooks. The PI agent first researches and reads, then proposes research directions and hands them to the researcher agents as concrete assignments. The piece we are proudest of is not any single agent, it is the seam underneath them. Early on we locked a hard contract between the infrastructure and the agent logic: the schemas for a research plan, the dispatch path that spawns work, and the shared store. Once that seam was frozen, the agent side and the infra side could move independently without breaking each other. This is the only reason a mid-build architecture change did not sink us. For compute, we use cloud sandboxes from Modal, which are virtual machines with root access. This gives each agent room to create files, delete files, and read the context we embed, while keeping it contained away from sensitive data and destructive actions. It also gives us real compute, so a sandbox can run a full training loop, a simulation, or an evaluation as part of the job rather than faking it. Modal is where all of our researcher agents live. For the control plane we use Google Cloud and Redis. Cloud Run hosts our API and the runner loops that drive the main agent, and the main agent itself runs as a job. Redis does a surprising amount of the heavy lifting here. It is our state store, our job queue, our event bus, our budget ledger, and the transcript stream all at once. The frontend subscribes to that event bus over server sent events, so you watch the job tree and the agent feed update live, and you can disconnect and resume a session exactly where you left off. Artifacts that researchers produce, like plots and metrics, get pushed to Google Cloud Storage and handed back to the PI. We persisted all of our cross session agent memory using Redis Agent Memory. As a mobile platform we use Poke to send text messages directly to the main agent for research on the go. Researchers run real experiments inside their sandboxes and log everything to Weights and Biases, and we use Browserbase to capture the live dashboard back into the run, so the PI gets the same visual evidence a human would look at instead of a paragraph of self-reported success. The dispatch path itself is guarded. Before any sandbox spins up, a plan is validated against budget, depth, and safety bounds, so an autonomous system cannot wander off, spawn unbounded work, or blow the whole budget on a single bad idea. We wired observability through Sentry and OpenTelemetry across both the control plane and the sandboxes, so a failing researcher leaves a trace instead of disappearing into a black box. The autonomous loop runs on this exact same infrastructure. It is the chat-agent system with a policy on top that decides, each round, whether to keep going or to stop and report.

### Challenges we ran into

The biggest one was observability, and specifically the black box around the sandboxes. Sentry gave us great traces, but we learned quickly that tool calls and reasoning steps are not enough on their own. For research we needed to reason about why an agent took a step and why it failed at a task, not just what it called. Researchers also failed in ways that looked random at first, and our early theories for why were naive. Over time we found the real causes and fixed them one by one. We attacked these with adversarial reviewers that try to refute a finding before we trust it, with deeper tracing into each step, and by hardening the dispatch and ops path so that a bad credential or a stale deploy could not silently take down a run. Then there was distributed reality, which is where a lot of agent demos quietly die. There is no shared filesystem between the control plane and the sandboxes. There are cold starts. There are concurrency races when several researchers run at once. We even had to keep secrets in sync across two separate stores, one for the control plane and one for the sandboxes, and a single mismatch would take the whole run down. Keeping one source of truth across all of these moving parts was a constant fight.

### Accomplishments we're proud of

We shipped a fully working end-to-end product in 24 hours. A real conversation turns into a plan, the plan fans out into researcher agents running in separate sandboxes, those agents run actual training runs, and real metrics and dashboards come back to the PI for synthesis. We got recursive sub-agents orchestrated in sandboxes working reliably, with budgets and depth limits so the system is autonomous without being reckless. We survived a significant architecture change mid-build because we had locked the seam first, which felt like the thing was actually paying off in real time. A lot of us went deep on documentation for GCP, Modal, Sentry, Redis, and the Anthropic stack, learned a ton, and came out genuinely excited to show this off after the hackathon.

### What we learned

Research is search, and the substrate matters more than the prompt. The hard and valuable part is not the agent's wording. It is the recursive, sandboxed, budget governed machine that lets a swarm of agents run validly in parallel. Build the seam before the intelligence. Locking the contract between infra and agent logic is the only reason a mid-build architecture change did not kill us. Keep a human at depth zero and a verifier in the loop. Autonomy is a slider, not a switch. You earn each notch by making the layer beneath it trustworthy. Budgets are a first-class feature, not an afterthought. An autonomous system without a hard ceiling is just a fast way to spend money and compute. Demos lie, nines don't. Distributed reality, with no shared filesystem, cold starts, and concurrency races, is where autonomous-agent dreams actually live or die.

### What's next

Launch an open source version of the product first, and offer a hosted version to enterprises later. Move to Daytona sandboxes with higher performance GPUs so we can run much heavier simulations and more demanding training, well beyond the small RL problems we run today. Spin up thousands of sandboxes with Kubernetes and SkyPilot so a single user can run many more hypotheses and experiments at once, all still directed from one conversation. Broaden past reinforcement learning into other research domains, and give the system memory across runs so it starts to learn what works and stops repeating dead ends.

## README (from the GitHub repository)

# AlphaResearch

Recursive automated-research platform: a single Claude Agent SDK orchestrator (the
"main agent") that the user talks to, which recursively dispatches research
sub-agents — each in its own Modal sandbox — to explore strategies in parallel and
report summaries back up. State, the event bus, the job queue, and the compute
budget all live in Redis; artifacts live in GCS.

Plan & tasks: `meta-planning/docs/plan.md`, `meta-planning/docs/tasks.md`; design
rationale: `meta-planning/docs/design.md`.

**Status:** cloud backplane (Redis Cloud + GCS + Modal) wired and verified end-to-end
via `scripts/smoke_modal.py`. Next: full agent loop over Modal, then the live web demo.

## Layout

```
agent/         self-similar run_agent + Claude Agent SDK tools (research-loop layer)
infra/         the LOCKED seam: store (Redis/GCS), schemas, dispatch, registry, modal_app
orchestrator/  FastAPI API (sessions + SSE), depth-0 worker, Modal runner
web/           Next.js chat + live agent-tree dashboard
scripts/       dev harnesses
deploy/        docker-compose.dev.yml, Cloud Run configs
```

The architecture is split by a stable seam (`infra/store.py` + `infra/dispatch.py`
+ `infra/schemas.py`): the research-loop layer (`agent/`) only ever calls that seam,
never Redis/GCS/Modal directly — so the loop can be rewritten without touching infra.

## Quickstart (local, no cloud)

```bash
# 1. deps (Python pinned to 3.12 via uv)
uv sync --extra dev

# 2. local Redis Stack (RedisJSON + Streams)
docker compose -f deploy/docker-compose.dev.yml up -d

# 3. configure
cp .env.example .env        # set ANTHROPIC_API_KEY; ALPHA_DISPATCH_BACKEND=local

# 4a. infra-only smoke (no API key needed) — exercises store + dispatch + experiment stub
uv run python scripts/smoke_infra.py

# 4b. full depth-0 agent loop (needs ANTHROPIC_API_KEY)
uv run python scripts/run_depth0.py "Improve PPO sample efficiency on MiniGrid-DoorKey-8x8"

# 5. API + SSE
uv run uvicorn orchestrator.api:app --reload --port 8080
#   POST /sessions {"goal": "..."}  ->  GET /sessions/{id}/stream  (SSE)
```

`DISPATCH_BACKEND=local` runs the whole tree in-process with synthetic experiment
stubs — fast iteration with only Redis up. Switch to `modal` once the Modal image is
built and credentials are set.

## Configuration: `.env` vs Modal secrets

Two runtimes, two config sources — they are **separate**:

- **`.env`** configures processes on your machine (orchestrator/API/worker, and
  `DISPATCH_BACKEND=local`). Loaded by `infra/config.py`.
- **The `alpha-secrets` Modal secret** is injected as env vars into the cloud Modal
  containers (depth ≥ 1 sub-agents / experiments). They never see your local `.env`.

A few values must exist in **both** places because each runtime reads its own source:

| value | local `.env` | Modal `alpha-secrets` |
|---|---|---|
| Redis URL | ✅ | ✅ |
| `ANTHROPIC_API_KEY` | ✅ | ✅ |
| GCS bucket / creds | ✅ | ✅ |
| Modal token | — (in `~/.modal.toml`) | — (ambient in Modal) |

> When `DISPATCH_BACKEND=modal`, set the Redis URL to your **Redis Cloud** endpoint
> (`rediss://…`) in **both** places — never `localhost`. The local orchestrator and the
> cloud sandboxes must share the same Redis. For pure local dev (`DISPATCH_BACKEND=local`)
> you don't need the Modal secret at all and `.env` keeps `localhost`.

## Modal setup (only for `DISPATCH_BACKEND=modal`)

```bash
modal token new                       # authenticate (writes ~/.modal.toml)

modal secret create alpha-secrets \
  ALPHA_REDIS_URL="rediss://default:<password>@<host>:<port>" \
  ANTHROPIC_API_KEY="sk-ant-..." \
  ALPHA_GCS_BUCKET="alpha-artifacts" \
  ALPHA_DISPATCH_BACKEND="modal"

modal deploy infra/modal_app.py       # makes run_job addressable (incl. nested spawn)
```

The secret name must be `alpha-secrets` (referenced in `infra/modal_app.py`). GCS uploads
need a service-account credential — use the Modal dashboard's Google Cloud secret template,
or leave `ALPHA_GCS_BUCKET` unset to fall back to local-disk artifacts while validating the
Modal compute path first.

## Frontend (`web/`)

The Next.js app talks to the FastAPI API over REST + SSE. For local development you
can run the API in **local-sim** mode (a scripted research run per session, no cloud
credentials) and point the web app at it:

```bash
# Terminal 1 — API in local-sim mode (no Cloud Run/Modal, no ANTHROPIC_API_KEY)
ALPHA_RUNNER_ENABLED=false ALPHA_LOCAL_SIM=true \
  uv run uvicorn orchestrator.api:app --port 8080

# Terminal 2 — Next.js dev server (http://localhost:3000)
cd web && npm run dev
```

Copy `web/.env.example` to `web/.env.local` first. With Clerk left unset the web app
runs keyless as a fixed "demo" user (see `web/lib/auth-config.ts`); the API similarly
stays in open dev mode while `ALPHA_CLERK_JWKS_URL` is unset.

## Production deploy

Deploy the API (Cloud Run / your host of choice), then configure the three boundaries —
the web → API URL, Clerk auth on both sides, and CORS:

1. **Point the web app at the deployed API.** Set `NEXT_PUBLIC_API_URL` in the web
   environment to the deployed API origin (e.g. `https://api.example.com`). It is
   inlined at build time (`web/lib/types.ts` reads `process.env.NEXT_PUBLIC_API_URL`),
   so set it before building the frontend.

2. **Enable Clerk.** Without these the app runs as the open "demo" user, so set them
   in production:
   - Web: `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` + `CLERK_SECRET_KEY` (from the Clerk
     dashboard). Setting the publishable key flips `CLERK_ENABLED` on, requiring
     sign-in and sending a verified token to the API.
   - Backend: `ALPHA_CLERK_JWKS_URL` (e.g.
     `https://<subdomain>.clerk.accounts.dev/.well-known/jwks.json`) and
     `ALPHA_CLERK_ISSUER` (**required** whenever the JWKS URL is set — the API fails
     fast otherwise). `ALPHA_CLERK_AUDIENCE` is optional. Run `uv sync --extra auth`
     so the JWT-verification deps are present.

3. **Set CORS to the deployed web origin.** The API's allowed browser origins are
   env-driven via `ALPHA_CORS_ALLOW_ORIGINS` (backs `settings.cors_allow_origins` in
   `infra/config.py`; defaults to `["http://localhost:3000"]`). It is parsed as a JSON
   list, so set it to your deployed web origin(s):

   ```bash
   ALPHA_CORS_ALLOW_ORIGINS='["https://app.example.com"]'
   ```


## Detected evidence (automated analysis)

Indexed codebase: 265 recognized source files, 1391 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- PyTorch (technology) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Anthropic (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 287)

```
.agents/skills/arize-admin/references/ax-profiles.md
.agents/skills/arize-admin/references/ax-setup.md
.agents/skills/arize-admin/references/REFERENCE.md
.agents/skills/arize-admin/SKILL.md
.agents/skills/arize-ai-provider-integration/references/ax-profiles.md
.agents/skills/arize-ai-provider-integration/references/ax-setup.md
.agents/skills/arize-ai-provider-integration/SKILL.md
.agents/skills/arize-annotation/references/ax-profiles.md
.agents/skills/arize-annotation/references/ax-setup.md
.agents/skills/arize-annotation/SKILL.md
.agents/skills/arize-compliance-audit/references/compliance-checklist-template.md
.agents/skills/arize-compliance-audit/references/eu-ai-act-gpai.md
.agents/skills/arize-compliance-audit/references/iso-42001.md
.agents/skills/arize-compliance-audit/references/us-ai-compliance.md
.agents/skills/arize-compliance-audit/SKILL.md
.agents/skills/arize-dataset/references/ax-profiles.md
.agents/skills/arize-dataset/references/ax-setup.md
.agents/skills/arize-dataset/SKILL.md
.agents/skills/arize-evaluator/references/ax-profiles.md
.agents/skills/arize-evaluator/references/ax-setup.md
.agents/skills/arize-evaluator/SKILL.md
.agents/skills/arize-experiment/references/ax-profiles.md
.agents/skills/arize-experiment/references/ax-setup.md
.agents/skills/arize-experiment/SKILL.md
.agents/skills/arize-instrumentation/references/ax-profiles.md
.agents/skills/arize-instrumentation/references/integration-routing.md
.agents/skills/arize-instrumentation/references/manual-spans.md
.agents/skills/arize-instrumentation/references/tracing-assistant-mcp.md
.agents/skills/arize-instrumentation/SKILL.md
.agents/skills/arize-link/references/EXAMPLES.md
.agents/skills/arize-link/SKILL.md
.agents/skills/arize-prompt-optimization/references/ax-profiles.md
.agents/skills/arize-prompt-optimization/references/ax-setup.md
.agents/skills/arize-prompt-optimization/SKILL.md
.agents/skills/arize-prompts/references/ax-profiles.md
.agents/skills/arize-prompts/references/ax-setup.md
.agents/skills/arize-prompts/references/cli-prompts.md
.agents/skills/arize-prompts/SKILL.md
.agents/skills/arize-trace/references/ax-profiles.md
.agents/skills/arize-trace/references/ax-setup.md
.agents/skills/arize-trace/SKILL.md
.claire/worktrees/agent-graph-overlay/web/components/__tests__/graph-node.test.tsx
.dockerignore
.env.example
.gitignore
.python-version
agent/__init__.py
agent/main-agent/.claude/hooks/_push.py
agent/main-agent/.claude/hooks/cap_bash_output.py
agent/main-agent/.claude/hooks/cap_web_fetch.py
agent/main-agent/.claude/hooks/finalize.py
agent/main-agent/.claude/hooks/log_transcript.py
agent/main-agent/.claude/hooks/precompact_snapshot.py
agent/main-agent/.claude/hooks/stream_event.py
agent/main-agent/.claude/hooks/validate_dispatch.py
agent/main-agent/.claude/hooks/validate_graph_spec.py
agent/main-agent/.claude/settings.json
agent/main-agent/CLAUDE.md
agent/main-agent/console_relay.py
agent/main-agent/launch.py
agent/main-agent/scripts/check_children.py
agent/main-agent/scripts/dispatch_subagent.py
agent/main-agent/scripts/read_artifacts.py
agent/main-agent/scripts/render_graph.py
agent/main-agent/scripts/report_round.py
agent/main-agent/scripts/schemas.py
agent/main-agent/scripts/wait_for_children.py
agent/main-agent/skills/dispatch-subagents/SKILL.md
agent/main-agent/skills/plotly-graphs/SKILL.md
agent/main-agent/skills/research/SKILL.md
agent/main-agent/stream_relay.py
agent/sub-agent/.claude/hooks/_push.py
agent/sub-agent/.claude/hooks/finalize.py
agent/sub-agent/.claude/hooks/stream_event.py
agent/sub-agent/.claude/settings.json
agent/sub-agent/CLAUDE.md
agent/sub-agent/console_relay.py
agent/sub-agent/launch.py
agent/sub-agent/reference/cleanrl/ppo_atari_envpool.py
agent/sub-agent/reference/cleanrl/ppo_continuous_action.py
agent/sub-agent/reference/cleanrl/ppo_minigrid_envpool.py
agent/sub-agent/reference/cleanrl/ppo.py
agent/sub-agent/reference/cleanrl/README.md
agent/sub-agent/scripts/train_ppo.py
agent/sub-agent/skills/validated-findings/SKILL.md
CLAUDE.md
deploy/api.Dockerfile
deploy/api.Dockerfile.dockerignore
deploy/docker-compose.dev.yml
deploy/main-agent.Dockerfile
deploy/sub-agent.Dockerfile
DESIGN.md
docs/backend-mvp-notes.md
docs/backend-mvp-overview.html
docs/cleanup-plan.md
docs/deploy-verification.md
docs/e2e-live-sse-verification.md
docs/gcp-setup.md
docs/ops-runbook.md
docs/superpowers/plans/2026-06-21-agent-graph-overlay.md
docs/superpowers/plans/2026-06-21-agent-token-streaming.md
docs/superpowers/specs/2026-06-21-agent-graph-overlay-design.md
docs/superpowers/specs/2026-06-21-agent-token-streaming-design.md
docs/superpowers/specs/2026-06-21-landing-page-design.md
docs/superpowers/specs/2026-06-21-web-pages-design.md
infra/__init__.py
infra/config.py
infra/dispatch.py
infra/loop_policy.py
infra/modal_app.py
infra/redis_keys.md
infra/registry/__init__.py
infra/registry/experiment.py
infra/schemas.py
infra/store.py
meta-planning/docs/autonomous-loops.md
meta-planning/docs/competence-plan.md
meta-planning/docs/demo-prep-handoff.md
meta-planning/docs/design.md
meta-planning/docs/memory-chatbot.md
[167 more files omitted for size]
```

### Dependencies

- pyproject.toml: claude-agent-sdk@>=0.1, fakeredis[json,lua]@>=2.26, fastapi@>=0.115, google-cloud-logging@>=3.11, google-cloud-run@>=0.10, google-cloud-storage@>=2.18, gymnasium@>=0.29, httpx@>=0.27, httpx@>=0.27, matplotlib@>=3.8, matplotlib@>=3.8, minigrid@>=2.3, modal@>=0.64, numpy@>=1.26, numpy@>=1.26, orjson@>=3.10, plotly@>=5.24, pydantic@>=2.9, pydantic-settings@>=2.6, pyjwt[crypto]@>=2.9, pytest@>=8.3, pytest-asyncio@>=0.24, redis@>=5.2, ruff@>=0.7, sse-starlette@>=2.1, tenacity@>=9.0, torch@>=2.2, uvicorn[standard]@>=0.32
- web/package.json: @clerk/nextjs@^7.5.7, @eslint/js@^10.0.1, @radix-ui/react-avatar@^1.1.2, @radix-ui/react-dialog@^1.1.17, @radix-ui/react-scroll-area@^1.2.2, @radix-ui/react-separator@^1.1.1, @radix-ui/react-slot@^1.1.1, @radix-ui/react-tooltip@^1.1.6, @tailwindcss/postcss@^4.0.0, @testing-library/jest-dom@^6.9.1, @testing-library/react@^16.3.2, @testing-library/user-event@^14.6.1, @types/d3-force@^3.0.10, @types/node@^22.0.0, @types/react@^19.0.0, @types/react-dom@^19.0.0, @vitejs/plugin-react@^6.0.2, class-variance-authority@^0.7.1, clsx@^2.1.1, d3-force@^3.0.0, eslint@^10.5.0, eslint-config-next@^16.2.9, eslint-config-prettier@^10.1.8, geist@^1.3.1, globals@^17.6.0, jsdom@^29.1.1, lucide-react@^0.469.0, next@^15.1.0, prettier@^3.8.4, react@^19.0.0, react-dom@^19.0.0, recharts@^2.15.0, sonner@^2.0.7, tailwind-merge@^2.6.0, tailwindcss@^4.0.0, typescript@^5.6.0, typescript-eslint@^8.61.1, vitest@^4.1.9

### Recent commits (newest first)

- Merge pull request #33 from JasonLai150/chore/cut-sponsor-fat
- chore: remove standalone demo/ mock app
- Merge origin/main into chore/cut-sponsor-fat
- docs: mark cut-plan steps done + note scrub deviation
- chore: cut Sentry + OpenTelemetry observability
- chore: cut Browserbase + wandb sponsor integrations
- Merge pull request #32 from JasonLai150/demo/live-claude
- feat(demo): live Claude streaming for the research chat
- Merge pull request #31 from JasonLai150/worktree-subagent-speed-and-wait
- feat(infra): configurable sub-agent warm pool + cold-start probe
- fix(agent): treat cancelled sub-agents as terminal in wait_for_children
- Merge pull request #30 from JasonLai150/feat/agent-chat-runner
- feat(runner): real conversational chat turns (replace local-sim mock)
- Merge pull request #29 from JasonLai150/streaming
- feat(console): stream raw agent stdout/stderr over SSE + per-subagent console
- Merge pull request #28 from JasonLai150/worktree-demo-mock
- fix(demo): adversarial-review fidelity + UX fixes (P6 part 2)
- test(demo): green suite + determinism/no-Math.random guard (P6 part 1)
- feat(demo): /autonomous loops section (P4)
- feat(demo): /view researcher inspector + in-chat long-term graphs (P3)

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

### CLAUDE.md

```markdown
This project is for autonomous research using claude code agent harnesses, research loops, and sandboxes in order to develop meaningful and opioniated research directions that subagents in docker + VMs can work on.  The goal is to make an easy application that humans can use for autonomous research. We're starting simple and trying to ensure simple RL research tasks can be completed meaningfully and quickly. 

Coding Practices 

1. Keep code modular. Keep code less than 800 lines per file. There should be single source of truth and config files.
2. Create unit tests and e2e tests when necessary. Be systematic in thinking and approaches.
3. Always challenge assumptions. You should verify and spawn adversarial agents to review your work. You should note any important assumptions the human might be making or you might be making.
4. Always ensure you are on a clean branch, and never push on main. 

Directory

meta-planning/docs/plan.md and meta-planning/docs/tasks.md contains important information about the high-level direction of the project. You should be concise when updating these docs. Keep your update either to a checkmark for tasks.md, or 1-3 lines for plan.md. 



```

### DESIGN.md

```markdown
---
version: alpha
name: xAI-design-analysis
description: An inspired interpretation of xAI's design language — Elon Musk's frontier-AI company whose web surface is a strict near-black canvas broken only by white pill outlines, occasional warm sunset / dusk gradient accents, a custom geometric sans (Universal Sans) for display, and an uppercase tracked monospace caption face; the whole system reads as engineered-cosmic, unmarketed.

colors:
  primary: "#ffffff"
  on-primary: "#0a0a0a"
  ink: "#ffffff"
  ink-hover: "#fafaf7"
  body: "#dadbdf"
  body-mid: "#7d8187"
  mute: "#7d8187"
  hairline: "#212327"
  canvas: "#0a0a0a"
  canvas-soft: "#1a1c20"
  canvas-card: "#191919"
  canvas-mid: "#363a3f"
  accent-sunset: "#ff7a17"
  accent-sunset-soft: "#ffc285"
  accent-dusk: "#7c3aed"
  accent-twilight: "#c4b5fd"
  accent-breeze: "#a0c3ec"
  accent-midnight: "#0d1726"

typography:
  display-xl:
    fontFamily: universalSans, Inter, system-ui, -apple-system, sans-serif
    fontSize: 96px
    fontWeight: 400
    lineHeight: 96px
    letterSpacing: -2.4px
  display-lg:
    fontFamily: universalSans, Inter, system-ui, sans-serif
    fontSize: 72px
    fontWeight: 400
    lineHeight: 72px
    letterSpacing: -1.8px
  display-md:
    fontFamily: universalSans, Inter, system-ui, sans-serif
    fontSize: 48px
    fontWeight: 400
    lineHeight: 48px
    letterSpacing: -1.2px
  display-sm:
    fontFamily: universalSans, Inter, system-ui, sans-serif
    fontSize: 32px
    fontWeight: 400
    lineHeight: 36px
    letterSpacing: -0.6px
  display-xs:
    fontFamily: universalSans, Inter, system-ui, sans-serif
    fontSize: 20px
    fontWeight: 400
    lineHeight: 28px
  body-lg:
    fontFamily: universalSans, Inter, system-ui, sans-serif
    fontSize: 18px
    fontWeight: 400
    lineHeight: 28px
  body-md:
    fontFamily: universalSans, Inter, system-ui, sans-serif
    fontSize: 16px
    fontWeight: 400
    lineHeight: 24px
  body-sm:
    fontFamily: universalSans, Inter, system-ui, sans-serif
    fontSize: 14px
    fontWeight: 400
    lineHeight: 20px
  caption-mono:
    fontFamily: GeistMono, ui-monospace, SFMono-Regular, Menlo, Monaco, monospace
    fontSize: 14px
    fontWeight: 400
    lineHeight: 20px
    letterSpacing: 1.4px
  caption-mono-sm:
    fontFamily: GeistMono, ui-monospace, SFMono-Regular, Menlo, monospace
    fontSize: 12px
    fontWeight: 400
    lineHeight: 16px
    letterSpacing: 1.2px
  button-md:
    fontFamily: universalSans, Inter, system-ui, sans-serif
    fontSize: 14px
    fontWeight: 400
    lineHeight: 20px

rounded:
  none: 0px
  sm: 8px
  pill: 9999px
  full: 9999px

spacing:
  xxs: 2px
  xs: 4px
  sm: 8px
  md: 12px
  lg: 16px
  xl: 24px
  2xl: 32px
  3xl: 48px
  4xl: 64px

components:
  nav-bar:
    backgroundColor: "{colors.canvas}"
    textColor: "{colors.ink}"
    typography: "{typography.body-sm}"
    padding: "{spacing.md} {spacing.xl}"
  nav-link:
    textColor: "{colors.ink}"
    typography: "{typography.body-sm}"
  button-pri
[truncated — 18597 more characters]
```

### pyproject.toml

```
[project]
name = "alpharesearch"
version = "0.0.1"
description = "Recursive automated-research platform: a Claude Agent SDK orchestrator dispatching research sub-agents into Modal sandboxes."
readme = "README.md"
requires-python = ">=3.12,<3.13"
dependencies = [
    "claude-agent-sdk>=0.1",
    "redis>=5.2",
    "modal>=0.64",
    "fastapi>=0.115",
    "uvicorn[standard]>=0.32",
    "sse-starlette>=2.1",
    "pydantic>=2.9",
    "pydantic-settings>=2.6",
    "httpx>=0.27",
    "tenacity>=9.0",
    "google-cloud-storage>=2.18",
    "google-cloud-run>=0.10",
    "google-cloud-logging>=3.11",
    "orjson>=3.10",
]

[project.optional-dependencies]
# Light deps for local dev: lets DISPATCH_BACKEND=local run the whole tree
# (stub experiments + synthetic plots) without the heavy ML stack.
dev = [
    "pytest>=8.3",
    "pytest-asyncio>=0.24",
    "ruff>=0.7",
    "matplotlib>=3.8",
    "numpy>=1.26",
    "fakeredis[json,lua]>=2.26",
    "httpx>=0.27",
    "plotly>=5.24",   # main-agent renders synthesis graphs via scripts/render_graph.py
]
# Optional: verify Clerk session JWTs on the public API (set ALPHA_CLERK_JWKS_URL).
# Imported lazily, so the dev/open path doesn't need it. Install: uv sync --extra auth
auth = [
    "pyjwt[crypto]>=2.9",
]
# The real prebaked trainer stack lives in the Modal image (infra/modal_app.py),
# NOT installed locally (envpool has no macOS wheels). Listed here for reference.
experiment = [
    "torch>=2.2",
    "numpy>=1.26",
    "gymnasium>=0.29",
    "minigrid>=2.3",
    "matplotlib>=3.8",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["agent", "infra", "orchestrator", "runner"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]

[tool.ruff.lint.flake8-bugbear]
# FastAPI's dependency-injection markers are designed to be call-in-default.
extend-immutable-calls = [
    "fastapi.Depends", "fastapi.Header", "fastapi.Query", "fastapi.Path",
    "fastapi.Body", "fastapi.Cookie", "fastapi.File", "fastapi.Form",
]

```

### web/package.json

```
{
  "name": "alpharesearch-web",
  "private": true,
  "version": "0.0.1",
  "scripts": {
    "dev": "next dev -p 3000",
    "build": "next build",
    "start": "next start -p 3000",
    "lint": "eslint .",
    "format": "prettier --write .",
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "test:watch": "vitest"
  },
  "dependencies": {
    "@clerk/nextjs": "^7.5.7",
    "@radix-ui/react-avatar": "^1.1.2",
    "@radix-ui/react-dialog": "^1.1.17",
    "@radix-ui/react-scroll-area": "^1.2.2",
    "@radix-ui/react-separator": "^1.1.1",
    "@radix-ui/react-slot": "^1.1.1",
    "@radix-ui/react-tooltip": "^1.1.6",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "d3-force": "^3.0.0",
    "geist": "^1.3.1",
    "lucide-react": "^0.469.0",
    "next": "^15.1.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "recharts": "^2.15.0",
    "sonner": "^2.0.7",
    "tailwind-merge": "^2.6.0"
  },
  "devDependencies": {
    "@eslint/js": "^10.0.1",
    "@tailwindcss/postcss": "^4.0.0",
    "@testing-library/jest-dom": "^6.9.1",
    "@testing-library/react": "^16.3.2",
    "@testing-library/user-event": "^14.6.1",
    "@types/d3-force": "^3.0.10",
    "@types/node": "^22.0.0",
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0",
    "@vitejs/plugin-react": "^6.0.2",
    "eslint": "^10.5.0",
    "eslint-config-next": "^16.2.9",
    "eslint-config-prettier": "^10.1.8",
    "globals": "^17.6.0",
    "jsdom": "^29.1.1",
    "prettier": "^3.8.4",
    "tailwindcss": "^4.0.0",
    "typescript": "^5.6.0",
    "typescript-eslint": "^8.61.1",
    "vitest": "^4.1.9"
  }
}

```

### runner/main.py

```python
"""Boot the runner's asyncio background tasks from FastAPI startup.

One leadership task (SEV-2) elects a single active runner via a Redis lease; the
three work loops only act while this instance holds the lease, so a rolling
deploy never double-spawns.
"""

from __future__ import annotations

import asyncio
import secrets

from runner import loops


def start_runner_tasks() -> list[asyncio.Task]:
    runner_id = secrets.token_hex(8)
    return [
        asyncio.create_task(loops.leadership_loop(runner_id)),
        asyncio.create_task(loops.session_loop()),
        asyncio.create_task(loops.dispatch_loop()),
        asyncio.create_task(loops.chat_loop()),
        asyncio.create_task(loops.reconcile_loop()),
    ]

```

### web/app/page.tsx

```typescript
import type { Metadata } from "next";
import { redirect } from "next/navigation";

import { LandingHero } from "@/components/landing/landing-hero";

export const metadata: Metadata = {
  title:
    "AlphaResearch — Recursive Sandboxed Agents for Autonomous RL Research at Scale",
  description:
    "A lead agent scopes your research goal, recursively spawns sandboxed sub-agents that run RL experiments in parallel, and streams the results back — live.",
};

export default async function Page() {
  // Signed-in users skip the marketing page and land on the console. Guarded on
  // the server secret so keyless dev mode just renders the landing (no Clerk).
  if (process.env.CLERK_SECRET_KEY) {
    const { auth } = await import("@clerk/nextjs/server");
    const { userId } = await auth();
    if (userId) redirect("/app");
  }
  return <LandingHero />;
}

```

### web/app/layout.tsx

```typescript
import "./globals.css";
import type { Metadata } from "next";
import type { ReactNode } from "react";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { ClerkProvider } from "@clerk/nextjs";

import { AppAuthProvider } from "@/components/auth/app-auth";
import { Toaster } from "@/components/ui/sonner";
import { CLERK_ENABLED } from "@/lib/auth-config";

export const metadata: Metadata = {
  title: "Alpha Research — Overview",
  description: "Recursive automated RL research console",
};

// Dark-canvas theming for Clerk's hosted components. Variables set the palette;
// `elements` push the primary CTA and inputs into the design-system pill shape
// (rounded-full white-filled CTA, hairline-bordered card on the canvas).
const clerkAppearance = {
  variables: {
    colorBackground: "#0a0a0a",
    colorInputBackground: "#1a1c20",
    colorText: "#ffffff",
    colorTextSecondary: "#7d8187",
    colorInputText: "#ffffff",
    colorPrimary: "#ffffff",
    colorNeutral: "#ffffff",
    borderRadius: "9999px",
  },
  elements: {
    rootBox: "w-full",
    cardBox:
      "border border-[#212327] bg-[#191919] shadow-none rounded-2xl",
    card: "bg-transparent shadow-none",
    headerTitle: "text-[#ffffff]",
    headerSubtitle: "text-[#7d8187]",
    socialButtonsBlockButton:
      "rounded-full border border-[#212327] bg-transparent text-[#ffffff] hover:bg-[#1a1c20]",
    formFieldInput:
      "rounded-full border border-[#212327] bg-[#1a1c20] text-[#ffffff]",
    formButtonPrimary:
      "rounded-full bg-[#ffffff] text-[#0a0a0a] font-normal normal-case shadow-none hover:bg-[#fafaf7]",
    footerActionLink: "text-[#ffffff] hover:text-[#fafaf7]",
    footer: "bg-transparent",
    dividerLine: "bg-[#212327]",
    dividerText: "text-[#7d8187]",
  },
};

export default function RootLayout({ children }: { children: ReactNode }) {
  const tree = (
    <html lang="en" className={`${GeistSans.variable} ${GeistMono.variable}`}>
      <body>
        <AppAuthProvider>{children}</AppAuthProvider>
        <Toaster />
      </body>
    </html>
  );

  return CLERK_ENABLED ? (
    <ClerkProvider appearance={clerkAppearance} afterSignOutUrl="/">
      {tree}
    </ClerkProvider>
  ) : (
    tree
  );
}

```

### web/app/(shell)/layout.tsx

```typescript
import type { ReactNode } from "react";

import { AppShell } from "@/components/app-shell";

// Route group: the parentheses keep these pages off the URL while sharing one
// layout. Every page under (shell) renders inside the persistent sidebar shell.
export default function ShellLayout({ children }: { children: ReactNode }) {
  return <AppShell>{children}</AppShell>;
}

```

### web/app/app/page.tsx

```typescript
"use client";

import { useCallback, useEffect, useMemo, useState } from "react";
import { ArrowLeft, Menu, Network, Sparkles } from "lucide-react";
import { toast } from "sonner";

import { stopSession } from "@/lib/api";
import { AgentGraph } from "@/components/agent-graph";
import { useAppAuth } from "@/components/auth/app-auth";
import { AppSidebar } from "@/components/app-sidebar";
import { ChatComposer } from "@/components/chat-composer";
import { ChatTranscript } from "@/components/chat-transcript";
import { ConsolePanel } from "@/components/console-panel";
import { ContextBar } from "@/components/context-bar";
import { Eyebrow } from "@/components/eyebrow";
import { ResizeHandle } from "@/components/resize-handle";
import { SessionHeader } from "@/components/session-header";
import { TreePanel } from "@/components/tree-panel";
import { Button } from "@/components/ui/button";
import {
  Sheet,
  SheetContent,
  SheetHeader,
  SheetTitle,
} from "@/components/ui/sheet";
import { TooltipProvider } from "@/components/ui/tooltip";
import { useChatSubmit } from "@/hooks/use-chat-submit";
import { useResizablePane } from "@/hooks/use-resizable-pane";
import { useSession } from "@/hooks/use-session";
import { useSessions } from "@/hooks/use-sessions";
import {
  artifactsOf,
  consoleOf,
  graphOf,
  rootJob,
  subagentsOf,
  treeOf,
} from "@/lib/session-reducer";
import { cn } from "@/lib/utils";
import type { RepoContext, TranscriptItem } from "@/lib/types";

export default function Page() {
  const { userId, getToken } = useAppAuth();
  const [activeId, setActiveId] = useState<string | null>(null);
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [treeOpen, setTreeOpen] = useState(false);
  const [stopping, setStopping] = useState(false);
  const [graphOpen, setGraphOpen] = useState(false);
  // Center pane: chat transcript vs. the lead agent's raw console.
  const [view, setView] = useState<"chat" | "console">("chat");
  // A selected sub-agent opens its own live console in a right drawer.
  const [consoleJobId, setConsoleJobId] = useState<string | null>(null);

  const {
    width: sidebarWidth,
    nudge: nudgeSidebar,
    reset: resetSidebar,
  } = useResizablePane({ key: "ar.sidebarWidth", min: 200, max: 480, initial: 264 });

  const { sessions, loading, refresh } = useSessions(userId, getToken);
  const { state, phase, notFound, reconnect } = useSession(activeId, getToken);

  function select(id: string | null) {
    setActiveId(id);
    setSidebarOpen(false);
    setTreeOpen(false);
    setGraphOpen(false);
    setView("chat");
    setConsoleJobId(null);
    const url = new URL(window.location.href);
    if (id) url.searchParams.set("s", id);
    else url.searchParams.delete("s");
    window.history.replaceState({}, "", url.toString());
  }

  const onStop = useCallback(async () => {
    if (!activeId || stopping) return;
    setStopping(true);
    try {
      await stopSession(activeId, await getToken());
      toast.info("Stop requested — the loop halts at the next round boundary.");
    } catch {
      toast.error("Couldn't stop the loop.");
    } finally {
      setStopping(false);
    }
  }, [activeId, stopping, getToken]);

  const { busy, pending, optimistic, onSubmit, settle, reconcile } =
    useChatSubmit({
      activeId,
      userId,
      getToken,
      onCreate: (sessionId) => {
        select(sessionId);
        // The sessions hook polls every 8s; one immediate refresh surfaces the
        // new chat in the sidebar without waiting for the next tick.
        refresh();
      },
    });

  // Restore the active session from the URL (?s=) on first load.
  useEffect(() => {
    const s = new URLSearchParams(window.location.search).get("s");
    if (s) setActiveId(s);
  }, []);

  // Stop the "working" indicator once the assistant replies (#3).
  const lastRole = state.transcript[state.transcript.length - 1]?.role;
  useEffect(() => {
    if (lastRole === "assistant") settle();
  }, [lastRole, state.transcript.length, settle]);

  // Reconcile the optimistic echo (#8): once the server transcript carries a
  // matching user message, drop the local echo so it never double-renders.
  const serverEchoed = useMemo(() => {
    if (!optimistic) return false;
    return state.transcript.some(
      (t) => t.role === "user" && t.text === optimistic.text
    );
  }, [optimistic, state.transcript]);
  useEffect(() => {
    reconcile(serverEchoed);
  }, [serverEchoed, reconcile]);

  // Merge the optimistic user echo into the rendered transcript until the
  // server confirms it (#8). Never append when the server already has it.
  const items: TranscriptItem[] = useMemo(() => {
    if (optimistic && !serverEchoed) return [...state.transcript, optimistic];
    return state.transcript;
  }, [optimistic, serverEchoed, state.transcript]);

  const root = rootJob(state);
  // Working while the lead agent is active, or a follow-up is awaiting a reply.
  // (A parked, queued sub-agent shouldn't keep the indicator spinning forever.)
  const running =
    (root
      ? root.status === "running" || root.status === "pending"
      : phase === "connecting" || phase === "streaming") || pending;

  // Build the execution-context chips from REAL session fields (#11): backend
  // env, mode, a short session id, and the relative started-time. Chips with no
  // real source are dropped by ContextBar (empty label).
  const ctx: RepoContext = {
    env: state.backend ?? "",
    repo: activeId ? activeId.slice(0, 8) : "",
    branch: state.mode ?? "",
    worktree: state.startedAt ?? "",
  };

  const tree = treeOf(state);
  const graph = graphOf(state);
  const subagents = subagentsOf(state);
  const artifacts = artifactsOf(state);
  const rootConsole = consoleOf(state, root?.id ?? null);
  const selectedAgent = subagents.find((a) => a.id === consoleJobId) ?? null;
  const drawerConsole = consoleOf(state, consoleJobId);

  const sidebar = (
    <AppSidebar
      sessions={sess
[truncated — 7208 more characters]
```

### web/app/sign-in/[[...sign-in]]/page.tsx

```typescript
"use client";

import { SignIn } from "@clerk/nextjs";

import { CLERK_ENABLED } from "@/lib/auth-config";

export default function SignInPage() {
  return (
    <div className="flex min-h-screen flex-col items-center justify-center gap-8 bg-canvas px-6 py-12">
      <header className="text-center">
        <h1 className="text-lg font-medium text-ink">Alpha Research</h1>
        <p className="mt-1 text-sm text-mute">
          Sign in to your research console
        </p>
      </header>
      {CLERK_ENABLED ? (
        <SignIn fallbackRedirectUrl="/app" signUpUrl="/sign-up" />
      ) : (
        <p className="text-sm text-mute">
          Authentication is disabled in this environment.
        </p>
      )}
    </div>
  );
}

```

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