# Project export: Repo Surgeon

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: OpenAI Build Week
- Tagline: Point Repo Surgeon at any GitHub repo. It upgrades dependencies, patches CVEs, and proves its own fixes work with mutation testing, then opens small, risk graded pull requests, unattended.
- Devpost: https://devpost.com/software/repo-surgeon
- GitHub: https://github.com/mayanks0ni/reposurgeon
- Video: https://www.youtube.com/embed/qQ647BKtcvg?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — mayanks0ni (1 commits)

## Devpost submission (written by the team)

### Inspiration

Every repo we've worked on has thirty outdated dependencies, a few unpatched CVEs, and a test suite nobody trusts enough to run before merging a fix. We wanted to see if an agent could own that whole loop, not just suggest a diff, so we built Repo Surgeon on Codex.

### What it does

Point Repo Surgeon at a GitHub repo and a five-stage pipeline runs unattended inside an isolated Docker sandbox: Scout clones the repo, detects the stack, and records a pass/fail baseline plus a security scan (OSV-Scanner, pip-audit, npm audit). Researcher uses GPT-5.6 with web search to pull real changelogs and migration guides for each outdated or vulnerable dependency. Surgeon dispatches upgrades to Codex running headless, editing code and re-running affected tests after each change. Verifier re-runs the full suite against baseline, loops failures back to the Surgeon, then runs mutation testing on any new tests to prove they'd actually catch a regression. Reviewer splits the work into small, risk-ordered PRs with evidence and a confidence grade, then watches CI and pushes fix commits on failure.

### How we built it

Orchestrator: Python/FastAPI state machine, plus GPT-5.6 for planning, risk ordering, and PR writeups. Agent runtime: Codex CLI headless (codex exec), with subagents for parallel upgrades and a post-edit hook that re-runs affected tests. Sandbox: one Docker container per job, network locked to allow-listed registries. Verification: full suite re-run plus mutmut (Python) and Stryker (JS/TS) for mutation testing. GitHub layer: GitPython + GitHub REST API for branches, risk-graded PRs, and CI polling. Dashboard: Next.js + Tailwind with a live SSE feed. One owner per layer, nightly 20-minute syncs.

### Challenges we ran into

Token blowup on research calls. Input tokens dominated output roughly 20:1 with web search on. Capping output tokens broke generation, since reasoning tokens share that budget. Fixed by batching 3 packages per call plus a pacing gate. Four people, one sequential pipeline. Solved by defining shared Pydantic contracts and Protocol interfaces up front, so real implementations swapped in for mocks without touching the orchestrator or dashboard. One-shot Codex edits were unreliable. Built a bounded retry loop (edit, re-test, diff against baseline, retry with failure context) capped at 5 attempts, then flagged needs_human instead of force-merging. Generated tests can pass trivially. Added mutation testing scored against mutation score, coverage, and stability, so green means the tests would actually catch a regression. Sandboxing untrusted code. Docker with resource/capability/mount limits plus phase-based network policy (network during install, none during execution). Python 3.9 compatibility gap surfaced mid-integration and was fixed during the Researcher/Reviewer/CI-watcher pass. Multi-job dashboard state. Fixed cross-job SSE leakage and a stale-job-ID crash after backend restart, both found through manual end-to-end testing. Real mode is opt-in. Mock mode is default everywhere; live GitHub/OpenAI calls need explicit opt-in and credentials. CI repair is capped at 2 fix commits per PR. At submission: 58 passing backend tests, all four production stages enabled in real mode, dashboard verified end-to-end, and a live Codex smoke test on a real bump (requests==2.31.0 to 2.32.3).

### What we learned

Token cost is an input problem once web search is involved, not an output one. Profile where tokens actually go before optimizing. Never trust a single LLM edit. A verify-and-retry loop with a hard cap and an honest needs_human state beats one-shot generate-and-merge. Passing tests isn't evidence of correctness. Mutation testing is the cheapest way to check if generated tests would catch a real regression. Contracts before implementations was the biggest unlock for four people building in parallel against a sequential pipeline. Default to the safe mode and make risk explicit. Sandboxing untrusted code has to be designed alongside the pipeline, not bolted on. Cap everything that could loop, or a stubborn failure becomes an unbounded cost sink instead of a clean signal. Manual end-to-end testing surfaces bugs unit tests don't; some things only show up when you use the product like a user would.

### What's next

Scoped from what's explicitly still open at submission time, not aspirational ideas. Immediate: pick and authorize 2 to 3 demo-fork repos (enabled but not yet exercised), build the Docker sandbox images (docker/python/Dockerfile, docker/node/Dockerfile exist but weren't built as of 2026-07-20), and get the real demo fork URL into the video walkthrough. Near-term hardening: wider language/stack detection beyond Python and JS/TS, hostname-level network policy (currently phase-based), broader mutation testing coverage, and guaranteeing scanner tools are present rather than silently degrading. Product direction: multi-repo batch mode, a review UI for needs_human items, per-job cost/token visibility on the dashboard, and graduating Planner.from_openai() to the default (behind the same real-mode gate). If this became a real product: persistent job storage (currently in-memory), auth/multi-tenant support, and rate-limit-aware scheduling extended across the whole pipeline.

## README (from the GitHub repository)

# Repo Surgeon

Repo Surgeon is an autonomous codebase-modernization pipeline built for OpenAI Build Week 2026. Point it at a repo and it establishes a test baseline, researches real breaking changes, executes dependency upgrades and security fixes inside a sandbox, proves its own generated tests actually catch bugs, and opens small, risk-graded pull requests — unattended.

## Status

| Component | Owner | Status |
| --- | --- | --- |
| Orchestrator, job state machine, Surgeon self-correction loop, Codex runner, FastAPI/SSE endpoints | Vasu | Done |
| Sandbox, Scout (stack detection, baseline, coverage), security scanners, Verifier (baseline diff, affected tests, mutation testing) | Faiz | Done |
| Dashboard (Next.js) | Anubhav | Done |
| Evidence-backed Researcher, GitHub Reviewer/PR creation, CI watcher + bounded repair loop | Mayank | Done — enabled in real mode; demo forks need selected targets |

The pipeline runs end to end in mock mode by default. Real mode enables each production stage when its required credentials and local tools are available.

## What it does once every piece is real

1. **Submit.** A user pastes a repo URL into the dashboard.
2. **Scout** (Faiz, real) clones the repo into a Docker sandbox, detects the stack, runs the existing test suite/build for a baseline, and scans dependencies with OSV-Scanner/pip-audit/npm audit.
3. **Researcher** (Mayank) asks GPT-5.6 with web search to fetch primary changelog, migration-guide, and issue-tracker evidence for every outdated or vulnerable dependency. It validates the returned JSON against the detected dependencies and records source URLs in the breaking-change map.
4. **Planner** (Vasu, real) turns the profile and breaking-change map into a risk-ordered upgrade plan: security fixes first, then patch, minor, major.
5. **Surgeon** (Vasu + Faiz, real) runs Codex headless per item with the breaking-change context injected, then Faiz's Verifier re-runs affected and full tests, diffs against baseline, and feeds failures back to Codex (capped at 5 attempts before flagging `needs_human`). It also mutation-tests any new/changed tests to score how many injected bugs they actually catch.
6. **Reviewer** (Mayank) splits green items into small, risk-graded PRs. Each PR contains its evidence link, verification record, confidence grade, and rollback note.
7. **CI watcher** (Mayank) polls GitHub check runs; on failure it extracts the failing check output, asks Codex for a focused repair on the PR branch, pushes a fix commit, and rechecks (capped at two repairs).
8. **Dashboard** (Anubhav, real) shows all of this live: a pipeline stepper, the scout report, the upgrade plan, per-item attempt/score cards with a diff viewer, and the resulting PR links.

Mock and real implementations share the same contracts, so the dashboard and orchestrator do not change when switching modes.

## LLM Architecture: Codex & GPT-5.6

Repo Surgeon relies on a specialized, dual-model architecture rather than a single monolithic LLM. We explicitly chose to split responsibilities to maximize reliability and minimize token waste:

1. **GPT-5.6 (Researcher & Planner):** We use GPT-5.6 for the high-level orchestration tasks. Extracting actionable migration paths from unstructured markdown changelogs requires deep semantic reasoning and the ability to parse dense, technical prose. GPT-5.6's superior structured JSON adherence ensures that the Researcher strictly returns valid schemas instead of hallucinating dependencies, while the Planner can reliably rank upgrades by accurately analyzing security-risk vectors.
2. **Codex (The Surgeon):** Code modification is handled exclusively by Codex. While general-purpose models tend to rewrite entire files or inject unnecessary formatting (often breaking brittle legacy code), Codex is uniquely tuned for surgical, unified diff generation. It acts like a true developer—applying focused, isolated patches to exactly the lines that need changing. This precise scope drastically reduces unexpected side effects during the Verifier's regression tests.

## Pipeline

```text
QUEUED -> SCOUTING -> RESEARCHING -> PLANNING -> OPERATING
      -> REVIEWING -> WATCHING_CI -> DONE

Terminal states: NEEDS_HUMAN, FAILED
```

For each upgrade item, the Surgeon follows this loop:

```text
Codex edit -> verify -> green
                    \-> pass failure logs back to Codex -> retry (max 5)
```

An item that is still failing after five attempts becomes `needs_human`; the pipeline never forces a broken upgrade.

## Repository layout

```text
repo_surgeon/
  contracts.py      Shared Pydantic schemas; source of truth for integrations
  interfaces.py     Protocols for all teammate boundaries
  orchestrator.py   Pipeline state machine
  planner.py        Mock fallback and OpenAI Responses planner
  surgeon.py        Codex/verify self-correction loop
  codex_runner.py   Real and mock Codex runners
  events.py         Async event bus used by SSE
  jobstore.py       In-memory job registry
  app.py            FastAPI application
  researcher.py     GPT-5.6 web-search research with source validation
  github_layer.py   Git branch/worktree management and GitHub PR creation
  ci.py             Check-run watcher and bounded Codex repair loop
  mocks/            Mock services used by the safe default mode
  sandbox/          Docker sandbox manager, command runner, network policy
  scout/            Stack detection, baseline runner, coverage, dependency collection
  security/         OSV-Scanner / pip-audit / npm audit parsing and normalization
  verifier/         Regression-aware verification, affected tests, mutation testing, quality score
dashboard/          Next.js dashboard (submit a repo, watch the live pipeline, view diffs/PRs/scores)
docs/               Implementation plans
tests/              Backend test suite (pytest)
```

## Setup

Prerequisites:

- Python 3.12+
- Node.js/npm (dashboard, and to install the Codex CLI)
- Git
- Docker (only needed for real-mode sandbox execution)

Install the backend:

```powershell
py -m pip install -e ".[dev]"
```

If `py` is not on PATH, use the bundled Python runtime:

```powershell
$py = "C:\Users\MSI1\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe"
& $py -m pip install -e ".[dev]"
```

Run the backend tests:

```powershell
& $py -m pytest -q
```

## Run the mock demo

Start the API:

```powershell
& $py -m uvicorn repo_surgeon.app:app --host 127.0.0.1 --port 8000
```

In another terminal, create a job:

```powershell
$job = Invoke-RestMethod -Method Post `
  -Uri "http://127.0.0.1:8000/jobs" `
  -ContentType "application/json" `
  -Body '{"repo_url":"https://example.invalid/demo.git"}'

Invoke-RestMethod "http://127.0.0.1:8000/jobs/$($job.job_id)"
```

The default application uses mocks, so this demo does not call OpenAI or modify a repository.

## Dashboard

The dashboard lives in [`dashboard/`](dashboard) (Next.js 16 + Tailwind). It proxies every API call through `/api/backend/*` to the FastAPI backend, so no CORS configuration is needed.

```powershell
cd dashboard
npm install
npm run dev
```

Open `http://localhost:3000` with the backend running on `:8000` (set `BACKEND_URL` in `dashboard/.env.local` to point elsewhere). Submitting a repo URL creates a job and opens its live page: a pipeline stepper, the scout report, the upgrade plan, per-item cards with live test counts and a diff viewer, mutation/test-quality scores (populated in real mode; mock mode shows placeholders), and PR links. See [`docs/DASHBOARD_IMPLEMENTATION_PLAN.md`](docs/DASHBOARD_IMPLEMENTATION_PLAN.md) for the full design.

## API

| Endpoint | Purpose |
| --- | --- |
| `POST /jobs` | Create and asynchronously run a job. Body: `{ "repo_url": "..." }`. |
| `GET /jobs` | List all jobs (id, repo URL, state, error). |
| `GET /jobs/{job_id}` | Read the current state, results, PRs, error, repo profile, and upgrade plan. |
| `GET /jobs/{job_id}/events` | Server-sent event stream for th

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 94 recognized source files, 356 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (107 of 107)

```
.env.example
.gitattributes
.gitignore
dashboard/.claude/launch.json
dashboard/.gitignore
dashboard/AGENTS.md
dashboard/CLAUDE.md
dashboard/DESIGN.md
dashboard/eslint.config.mjs
dashboard/next.config.ts
dashboard/package.json
dashboard/postcss.config.mjs
dashboard/README.md
dashboard/src/app/globals.css
dashboard/src/app/jobs/[id]/page.tsx
dashboard/src/app/layout.tsx
dashboard/src/app/not-found.tsx
dashboard/src/app/page.tsx
dashboard/src/components/CompletionToast.tsx
dashboard/src/components/DiffViewer.tsx
dashboard/src/components/EventLog.tsx
dashboard/src/components/fx/MagneticField.tsx
dashboard/src/components/ItemCard.tsx
dashboard/src/components/LiveLogPanel.tsx
dashboard/src/components/PipelineStepper.tsx
dashboard/src/components/PlanTable.tsx
dashboard/src/components/PRPanel.tsx
dashboard/src/components/ScoutSummary.tsx
dashboard/src/components/StateBadge.tsx
dashboard/src/components/TraceExplorer.tsx
dashboard/src/components/ui/AnimatedNumber.tsx
dashboard/src/components/ui/GlowCard.tsx
dashboard/src/hooks/useJobEvents.ts
dashboard/src/hooks/useJobLogs.ts
dashboard/src/lib/api.ts
dashboard/src/lib/motion.ts
dashboard/src/lib/quality.ts
dashboard/src/lib/types.ts
dashboard/src/styles/theme.css
dashboard/tsconfig.json
docker/node/Dockerfile
docker/python/Dockerfile
docs/DASHBOARD_IMPLEMENTATION_PLAN.md
pyproject.toml
README.md
repo_surgeon/__init__.py
repo_surgeon/app.py
repo_surgeon/ci.py
repo_surgeon/codex_runner.py
repo_surgeon/contracts.py
repo_surgeon/events.py
repo_surgeon/github_layer.py
repo_surgeon/interfaces.py
repo_surgeon/jobstore.py
repo_surgeon/live_logs.py
repo_surgeon/llm.py
repo_surgeon/mocks/__init__.py
repo_surgeon/mocks/mock_sandbox.py
repo_surgeon/mocks/services.py
repo_surgeon/orchestrator.py
repo_surgeon/planner.py
repo_surgeon/pypi_versions.py
repo_surgeon/researcher.py
repo_surgeon/sandbox/__init__.py
repo_surgeon/sandbox/command_runner.py
repo_surgeon/sandbox/errors.py
repo_surgeon/sandbox/manager.py
repo_surgeon/sandbox/models.py
repo_surgeon/sandbox/policy.py
repo_surgeon/scout/__init__.py
repo_surgeon/scout/baseline_runner.py
repo_surgeon/scout/command_detector.py
repo_surgeon/scout/coverage.py
repo_surgeon/scout/dependency_collector.py
repo_surgeon/scout/profile_writer.py
repo_surgeon/scout/service.py
repo_surgeon/scout/stack_detector.py
repo_surgeon/security/__init__.py
repo_surgeon/security/normalizer.py
repo_surgeon/security/npm_audit.py
repo_surgeon/security/osv.py
repo_surgeon/security/pip_audit.py
repo_surgeon/security/service.py
repo_surgeon/surgeon.py
repo_surgeon/trace.py
repo_surgeon/verifier/__init__.py
repo_surgeon/verifier/affected_tests.py
repo_surgeon/verifier/baseline_diff.py
repo_surgeon/verifier/mutation.py
repo_surgeon/verifier/mutmut_runner.py
repo_surgeon/verifier/quality_score.py
repo_surgeon/verifier/service.py
repo_surgeon/verifier/stryker_runner.py
scratch/diff.txt
START_TESTING.md
tests/conftest.py
tests/test_app.py
tests/test_audit.py
tests/test_codex_runner_diff.py
tests/test_faiz_components.py
tests/test_mayank_services.py
tests/test_orchestrator.py
tests/test_planner.py
tests/test_scout_bootstrap.py
tests/test_surgeon.py
tests/test_trace.py
tests/test_verifier_fixes.py
```

### Dependencies

- dashboard/package.json: @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.2.10, next@16.2.10, react@19.2.4, react-dom@19.2.4, tailwindcss@^4, typescript@^5
- pyproject.toml: fastapi@>=0.115, httpx@>=0.27, openai@>=1.0, pydantic@>=2.0, pytest@>=8.0, pytest-asyncio@>=0.24, uvicorn@>=0.30

### Recent commits (newest first)

- Update fixes
- fixes
- Initial commit

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

### START_TESTING.md

```markdown
# Testing Steps

Run from `E:\openai\Repo-Surgeon`.

## 1. Start Docker Desktop, then build images (first time only)

```powershell
docker build -t repo-surgeon-python:local -f docker/python/Dockerfile .
docker build -t repo-surgeon-node:local   -f docker/node/Dockerfile .
```

Tag must be `:local` exactly — the code looks for that tag and won't fall back to `:latest`.

## 2. Check `.env` has both keys set

`OPENAI_API_KEY` and `GITHUB_TOKEN` must both be non-empty.

## 3. Start the backend (terminal 1)

```powershell
Get-Content .env | ForEach-Object { if ($_ -match '^([^#][^=]*)=(.*)$') { Set-Item -Path "env:$($Matches[1].Trim())" -Value $Matches[2].Trim() } }
$env:CODEX_API_KEY = $env:OPENAI_API_KEY
python -m uvicorn repo_surgeon.app:app --host 127.0.0.1 --port 8000
```

## 4. Start the dashboard (terminal 2)

```powershell
cd dashboard
npm run dev
```

## 5. Test

Open http://localhost:3000, paste the demo repo URL, watch it run.

## 6. Read the data flow

Every job writes each stage's exact inputs and outputs to `test_results/<job_id>/`,
numbered in execution order:

```
01_job_input.json                     repo URL, mode, models, which services are live
02_clone_output.json                  workspace path
03/04_scouting_*.json                 full RepoProfile: stack, deps, vulns, baseline
   scout stack/baseline/dependencies/security   sub-traces, written as Scout runs
05_researching_output.json            BreakingChanges, plus which packages were rejected and why
   research_llm_call.json             verbatim prompt, raw response, token usage, duration
06/07_plan_*.json                     what the Planner saw and the plan it returned
09_operating_input.json               plan + migration notes handed to the Surgeon
10+_operate_<pkg>_iter<N>_*.json      per iteration: Codex input, Codex patch, verify result
15+_reviewing_*.json                  PR request and PR result
NN_job_summary.json                   final state, per-stage durations, all results
```

Any stage that throws also writes `<stage>_error.json` with the traceback and, for
model calls, the raw unparsed response.

Turn it off with `REPO_SURGEON_TRACE=0`; relocate it with `REPO_SURGEON_TRACE_DIR`.
For very chatty output, `REPO_SURGEON_DEBUG=1` drops `repo_surgeon.*` to DEBUG.

## Rate limiting

Research is the only stage that can realistically trip a rate limit — web search
bills the pages it reads as input (measured: ~13-27K input tokens per call). Every
upgradable dependency is researched — there is no cap trimming a big repo's
findings — split into small batches that run concurrently through a shared,
staggered gate. Tune with:

| Variable | Default | Effect |
|---|---|---|
| `REPO_SURGEON_MAX_CANDIDATES` | 500 | safety ceiling, not a deliberate trim — real repos won't hit it |
| `REPO_SURGEON_RESEARCH_BATCH_SIZE` | 3 | packages per web-search call |
| `REPO_SURGEON_LLM_CONCURRENCY` | 3 | model calls allowed in flight at once |
| `REPO_SURGEON_LLM_MIN_INTERVAL` | 3.0 | sec
[truncated — 1609 more characters]
```

### dashboard/CLAUDE.md

```markdown
@AGENTS.md

```

### pyproject.toml

```
[project]
name = "repo-surgeon"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["fastapi>=0.115", "pydantic>=2.0", "openai>=1.0", "uvicorn>=0.30"]

[project.optional-dependencies]
dev = ["pytest>=8.0", "pytest-asyncio>=0.24", "httpx>=0.27"]

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

[tool.setuptools]
packages = ["repo_surgeon"]

```

### dashboard/package.json

```
{
  "name": "dashboard",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "next": "16.2.10",
    "react": "19.2.4",
    "react-dom": "19.2.4"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.2.10",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### docker/python/Dockerfile

```
FROM ghcr.io/google/osv-scanner:v2.4.0 AS osv
FROM python:3.10-slim
COPY --from=osv /osv-scanner /usr/local/bin/osv-scanner
RUN apt-get update && apt-get install -y --no-install-recommends git build-essential ca-certificates libxml2-dev libxslt-dev \
 && pip install --no-cache-dir pytest pytest-cov coverage pip-audit mutmut build \
 && rm -rf /var/lib/apt/lists/*
RUN useradd --create-home --uid 10001 surgeon
USER surgeon
WORKDIR /workspace

```

### docker/node/Dockerfile

```
FROM ghcr.io/google/osv-scanner:v2.4.0 AS osv
FROM node:22-bookworm-slim
COPY --from=osv /osv-scanner /usr/local/bin/osv-scanner
RUN apt-get update && apt-get install -y --no-install-recommends git build-essential ca-certificates procps \
 && corepack enable \
 && corepack prepare pnpm@10.14.0 --activate \
 && corepack prepare yarn@1.22.22 --activate \
 && npm install --global @stryker-mutator/core @stryker-mutator/jest-runner @stryker-mutator/vitest-runner \
 && npm cache clean --force \
 && rm -rf /var/lib/apt/lists/*
USER node
WORKDIR /workspace

```

### repo_surgeon/app.py

```python
from __future__ import annotations
import asyncio
import json
import logging
import os
import sys
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from .codex_runner import MockCodexRunner, RealCodexRunner
from .ci import live_ci_watcher
from .events import EventBus
from .jobstore import InMemoryJobStore
from . import live_logs
from .mocks import MockResearcher, MockReviewer, MockSandbox, MockScout, MockVerifier
from .orchestrator import Orchestrator
from .planner import Planner
from .github_layer import GitHubClient, GitHubReviewer
from .researcher import OpenAIResearcher
from .sandbox import AsyncCommandRunner, RealSandbox, SandboxedCommandRunner
from .scout import ProfileRegistry, RealScout
from .surgeon import Surgeon
from .verifier import RealVerifier

# uvicorn only configures its own "uvicorn.*" loggers; without this, every
# logger.info() call in the pipeline (repo_surgeon.*) is silently dropped by
# the root logger's default WARNING level, so the terminal shows request
# lines but nothing about what a job is actually doing.
#
# reconfigure(): the Windows console defaults to cp1252, which mangles every
# non-ASCII character the pipeline logs (em-dashes here, and arbitrary bytes in
# captured test output) into a replacement glyph or a UnicodeEncodeError.
for _stream in (sys.stdout, sys.stderr):
    try:
        _stream.reconfigure(encoding="utf-8", errors="replace")
    except (AttributeError, ValueError):
        pass
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s",
                    datefmt="%H:%M:%S")
logging.getLogger("repo_surgeon").setLevel(
    logging.DEBUG if os.getenv("REPO_SURGEON_DEBUG", "").lower() in {"1", "true", "yes"} else logging.INFO)
# The OpenAI SDK logs full request/response bodies at DEBUG, which buries the
# pipeline's own output. Keep it at WARNING unless explicitly debugging HTTP.
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("openai").setLevel(logging.WARNING)
# Feeds the /debug dashboard's live log console (see live_logs.py) — a
# temporary testing aid, safe to delete along with debug_dashboard.html.
live_logs.install()


def build_orchestrator(mode: str | None = None, store: InMemoryJobStore | None = None,
                       events: EventBus | None = None) -> Orchestrator:
    mode = (mode or os.getenv("REPO_SURGEON_MODE", "mock")).lower()
    store, events = store or InMemoryJobStore(), events or EventBus()
    if mode == "mock":
        sandbox, scout, verifier, codex = MockSandbox(), MockScout(), MockVerifier(), MockCodexRunner()
        researcher, planner, reviewer, ci_watcher = MockResearcher(), Planner(), MockReviewer(), None
    elif mode == "real":
        host_runner, registry = AsyncCommandRunner(), ProfileRegistry()
        sandbox = RealSandbox(runner=host_runner)
        runner = SandboxedCommandRunner(sandbox)
        codex = RealCodexRunner()
        # codex is passed in so Scout can bootstrap a test suite when none is
        # detected, rather than leaving every upgrade on that repo unverifiable.
        scout, verifier = RealScout(runner, registry, codex=codex), RealVerifier(registry, runner)
        token = os.getenv("GITHUB_TOKEN")
        researcher, planner = OpenAIResearcher.from_openai(), Planner.from_openai()
        reviewer, ci_watcher = GitHubReviewer(GitHubClient(token)), live_ci_watcher(token)
    else:
        raise ValueError("REPO_SURGEON_MODE must be 'mock' or 'real'")
    return Orchestrator(store, events, sandbox, scout, researcher, planner,
        Surgeon(codex, verifier, events), reviewer, ci_watcher)


app = FastAPI(title="Repo Surgeon")
store, events = InMemoryJobStore(), EventBus()
orchestrator = build_orchestrator(store=store, events=events)

# asyncio only holds a *weak* reference to a task created via create_task —
# with nothing else referencing it, the task is eligible for garbage
# collection mid-run and the job silently stops advancing (typically noticed
# during the long-running scouting stage, which has the most await points and
# elapsed time for GC to strike). Keeping a strong reference here, and
# dropping it via the done-callback once the job finishes, is the standard
# fix: https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task
_background_jobs: set[asyncio.Task] = set()


class CreateJob(BaseModel): repo_url: str


@app.post("/jobs")
async def create_job(request: CreateJob) -> dict[str, str]:
    job = store.create(request.repo_url)
    task = asyncio.create_task(orchestrator.run(job.id))
    _background_jobs.add(task)
    task.add_done_callback(_background_jobs.discard)
    return {"job_id": job.id}


@app.get("/jobs")
async def list_jobs() -> list[dict]:
    return [{"id": j.id, "repo_url": j.repo_url, "state": j.state, "error": j.error} for j in store.list()]


@app.get("/jobs/{job_id}")
async def get_job(job_id: str) -> dict:
    job = store.get(job_id)
    if not job: raise HTTPException(404, "Job not found")
    return {"id": job.id, "repo_url": job.repo_url, "state": job.state, "results": job.results, "prs": job.prs,
            "error": job.error, "profiles": [p.model_dump(mode="json") for p in job.profiles] if job.profiles else [],
            "plans": [p.model_dump(mode="json") for p in job.plans] if job.plans else []}


@app.get("/jobs/{job_id}/events")
async def get_events(job_id: str) -> StreamingResponse:
    if not store.get(job_id): raise HTTPException(404, "Job not found")
    async def stream():
        async for event in events.subscribe(job_id): yield f"data: {json.dumps(event.model_dump(mode='json'))}\n\n"
    return StreamingResponse(stream(), media_type="text/event-stream")


# --- Live pipeline transparency ------------------------------------------
# Full log lines (not just the coarse stage events already on /jobs/{id}/events)
# and the per-stage data-flow dumps written to test_
[truncated — 1884 more characters]
```

### dashboard/src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Link from "next/link";
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: "Repo Surgeon",
  description: "Autonomous codebase modernization: plan, edit, verify, and open pull requests, unattended.",
  icons: {
    icon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E%F0%9F%A9%BA%3C/text%3E%3C/svg%3E",
  },
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html
      lang="en"
      className={`${geistSans.variable} ${geistMono.variable} h-full antialiased dark`}
    >
      <body className="min-h-full flex flex-col bg-background text-foreground">
        <header className="sticky top-0 z-40 border-b border-[var(--border)] bg-[var(--surface-glass)] backdrop-blur-md">
          <div className="mx-auto flex w-full max-w-5xl items-center justify-between px-6 py-3">
            <Link href="/" className="flex items-center gap-2.5">
              <span aria-hidden="true" className="dot-live h-2 w-2 shrink-0" />
              <span className="font-mono text-sm font-medium tracking-tight text-[var(--text)]">
                REPO SURGEON
              </span>
            </Link>
            <span className="eyebrow hidden sm:inline">Autonomous codebase surgery</span>
          </div>
        </header>
        <div className="rise-in flex flex-1 flex-col">{children}</div>
      </body>
    </html>
  );
}

```

### dashboard/src/app/page.tsx

```typescript
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { createJob, listJobs } from "@/lib/api";
import type { JobSummary } from "@/lib/types";
import { StateBadge } from "@/components/StateBadge";
import { GlowCard } from "@/components/ui/GlowCard";
import { MagneticField } from "@/components/fx/MagneticField";
import { staggerDelay } from "@/lib/motion";

const EXAMPLE_REPOS = [
  "https://github.com/org/legacy-service",
  "https://github.com/acme/payments-api",
  "https://github.com/your-org/your-repo",
];

function useTypewriterPlaceholder(active: boolean): string {
  const [text, setText] = useState("");
  useEffect(() => {
    if (!active) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      // eslint-disable-next-line react-hooks/set-state-in-effect -- one-shot sync with a media query unavailable during SSR
      setText(EXAMPLE_REPOS[0]);
      return;
    }
    let cancelled = false;
    let timer: ReturnType<typeof setTimeout>;

    async function loop() {
      let repoIndex = 0;
      while (!cancelled) {
        const full = EXAMPLE_REPOS[repoIndex % EXAMPLE_REPOS.length];
        for (let i = 0; i <= full.length && !cancelled; i++) {
          setText(full.slice(0, i));
          await new Promise((resolve) => {
            timer = setTimeout(resolve, 28);
          });
        }
        await new Promise((resolve) => {
          timer = setTimeout(resolve, 1400);
        });
        for (let i = full.length; i >= 0 && !cancelled; i--) {
          setText(full.slice(0, i));
          await new Promise((resolve) => {
            timer = setTimeout(resolve, 14);
          });
        }
        await new Promise((resolve) => {
          timer = setTimeout(resolve, 300);
        });
        repoIndex++;
      }
    }
    void loop();
    return () => {
      cancelled = true;
      clearTimeout(timer);
    };
  }, [active]);
  return text;
}

export default function Home() {
  const router = useRouter();
  const [repoUrl, setRepoUrl] = useState("");
  const [focused, setFocused] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [jobs, setJobs] = useState<JobSummary[]>([]);
  const [jobsLoaded, setJobsLoaded] = useState(false);
  const errorFlashRef = useRef<HTMLDivElement>(null);

  const placeholder = useTypewriterPlaceholder(!focused && !repoUrl && !submitting);

  const refresh = useCallback(async () => {
    try {
      const list = await listJobs();
      setJobs([...list].reverse());
    } catch {
      // transient network issue; keep showing the last known list
    } finally {
      setJobsLoaded(true);
    }
  }, []);

  useEffect(() => {
    // eslint-disable-next-line react-hooks/set-state-in-effect -- initial fetch + poll on mount, setState only runs after the await
    void refresh();
    const interval = setInterval(() => {
      if (document.visibilityState === "visible") void refresh();
    }, 5000);
    return () => clearInterval(interval);
  }, [refresh]);

  async function handleSubmit(event: React.FormEvent) {
    event.preventDefault();
    setError(null);
    if (!/^https:\/\//.test(repoUrl.trim())) {
      setError("Enter a valid https:// repository URL.");
      return;
    }
    setSubmitting(true);
    try {
      const { job_id } = await createJob(repoUrl.trim());
      router.push(`/jobs/${job_id}`);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to create job.");
      setSubmitting(false);
    }
  }

  return (
    <main className="flex w-full flex-1 flex-col">
      <section className="relative isolate overflow-hidden border-b border-[var(--border)]">
        <MagneticField className="absolute inset-0 -z-10" />
        <div
          aria-hidden="true"
          className="pointer-events-none absolute inset-0 -z-10"
          style={{
            background:
              "radial-gradient(60% 50% at 50% 40%, transparent 0%, var(--bg) 100%)",
          }}
        />
        <div className="mx-auto flex w-full max-w-3xl flex-col gap-8 px-6 py-20 sm:py-28">
          <div className="rise-in space-y-4 text-center">
            <span className="eyebrow">Autonomous codebase surgery</span>
            <h1 className="text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
              Operates on your repo.
              <br />
              <span className="text-[var(--accent)]">Unattended.</span>
            </h1>
            <p className="mx-auto max-w-xl text-[var(--text-muted)]">
              Baseline tests, researched breaking changes, sandboxed upgrades, verified
              fixes, risk-graded pull requests &mdash; point it at a repo and step back.
            </p>
          </div>

          <form
            onSubmit={handleSubmit}
            className="rise-in"
            style={{ animationDelay: "120ms" }}
          >
            <div
              className="flex flex-col gap-3 rounded-[var(--radius-lg)] border p-2 transition-[border-color,box-shadow] duration-[var(--dur-base)] sm:flex-row"
              style={{
                borderColor: focused ? "var(--accent)" : "var(--border)",
                boxShadow: focused ? "var(--glow-accent)" : "none",
                background: "var(--surface-1)",
              }}
            >
              <input
                id="repo-url"
                type="text"
                aria-label="Repository URL"
                placeholder={placeholder || "https://github.com/org/repo"}
                value={repoUrl}
                onChange={(event) => setRepoUrl(event.target.value)}
                onFocus={() => setFocused(true)}
                onBlur={() => setFocused(false)}
                disabled={submitting}
                className="h-14 flex-1 rounded-[var(--radius-md)] bg-transparent px-4 font-mono text-sm text-[var
[truncated — 2629 more characters]
```

### dashboard/src/app/jobs/[id]/page.tsx

```typescript
"use client";

import { useEffect, useState } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { useJobEvents } from "@/hooks/useJobEvents";
import { StateBadge } from "@/components/StateBadge";
import { PipelineStepper } from "@/components/PipelineStepper";
import { ScoutSummary } from "@/components/ScoutSummary";
import { PlanTable } from "@/components/PlanTable";
import { ItemCard } from "@/components/ItemCard";
import { PRPanel } from "@/components/PRPanel";
import { EventLog } from "@/components/EventLog";
import { LiveLogPanel } from "@/components/LiveLogPanel";
import { TraceExplorer } from "@/components/TraceExplorer";
import { CompletionToast } from "@/components/CompletionToast";
import { staggerDelay } from "@/lib/motion";
import type { IterationPayload } from "@/lib/types";

function useElapsed(startedAt: number | null, stopped: boolean): string {
  const [now, setNow] = useState(() => Date.now());
  useEffect(() => {
    if (stopped || startedAt == null) return;
    const interval = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(interval);
  }, [startedAt, stopped]);
  if (startedAt == null) return "—";
  const seconds = Math.max(0, Math.floor((now - startedAt) / 1000));
  const m = Math.floor(seconds / 60);
  const s = seconds % 60;
  return `${m}:${s.toString().padStart(2, "0")}`;
}

export default function JobPage() {
  const params = useParams<{ id: string }>();
  const jobId = params.id;
  const { job, events, connected, notFound } = useJobEvents(jobId);
  const [copied, setCopied] = useState(false);

  const firstEventTs = events.length > 0 ? new Date(events[0].ts).getTime() : null;
  const isTerminal = job ? job.state === "done" || job.state === "needs_human" || job.state === "failed" : false;
  const elapsed = useElapsed(firstEventTs, isTerminal);

  function copyJobId() {
    if (!job) return;
    void navigator.clipboard.writeText(job.id);
    setCopied(true);
    setTimeout(() => setCopied(false), 1500);
  }

  if (notFound) {
    return (
      <main className="mx-auto flex w-full max-w-2xl flex-1 flex-col items-center justify-center gap-3 px-6 text-center">
        <h1 className="text-2xl font-semibold">Job not found</h1>
        <p className="text-[var(--text-faint)]">
          This job doesn&apos;t exist &mdash; it may have been created before the backend last restarted.
        </p>
        <Link href="/" className="text-sm text-[var(--accent-bright)] hover:underline">
          Back to home
        </Link>
      </main>
    );
  }

  if (!job) {
    return (
      <main className="mx-auto flex w-full max-w-4xl flex-1 flex-col gap-6 px-6 py-10">
        <div className="h-8 w-64 animate-pulse rounded bg-[var(--surface-1)]" />
        <div className="h-24 animate-pulse rounded-[var(--radius-lg)] bg-[var(--surface-1)]" />
        <div className="h-48 animate-pulse rounded-[var(--radius-lg)] bg-[var(--surface-1)]" />
      </main>
    );
  }

  const iterationsByItem = new Map<string, IterationPayload[]>();
  for (const event of events) {
    if (event.type !== "iteration") continue;
    const payload = event.payload as unknown as IterationPayload;
    const list = iterationsByItem.get(payload.item_id) ?? [];
    list.push(payload);
    iterationsByItem.set(payload.item_id, list);
  }
  const resultByItem = new Map(job.results.map((result) => [result.item_id, result]));
  const flaggedItems = job.results.filter((r) => r.status === "needs_human");

  return (
    <main className="mx-auto flex w-full max-w-4xl flex-1 flex-col gap-6 px-6 py-10">
      <header className="sticky top-[57px] z-30 -mx-6 space-y-3 border-b border-[var(--border)] bg-[var(--surface-glass)] px-6 py-4 backdrop-blur-md">
        <div className="flex flex-wrap items-center justify-between gap-3">
          <div className="min-w-0">
            <p className="truncate text-lg font-semibold text-[var(--text)]">{job.repo_url}</p>
            <button
              type="button"
              onClick={copyJobId}
              className="flex items-center gap-1.5 font-mono text-xs text-[var(--text-faint)] hover:text-[var(--text-muted)]"
              title="Click to copy job id"
            >
              {job.id}
              <span className="text-[var(--accent)]">{copied ? "✓ copied" : "⧉"}</span>
            </button>
          </div>
          <div className="flex items-center gap-3">
            <span className="font-mono text-xs text-[var(--text-faint)]" style={{ fontVariantNumeric: "tabular-nums" }}>
              {elapsed}
            </span>
            {!connected && !isTerminal && (
              <span className="rounded-full bg-[var(--surface-2)] px-2.5 py-0.5 text-xs text-[var(--text-muted)]">
                reconnecting…
              </span>
            )}
            <StateBadge state={job.state} />
          </div>
        </div>
        {job.error && (
          <div className="rounded-[var(--radius-md)] border border-[var(--danger)]/40 bg-[var(--danger)]/10 px-4 py-3 text-sm text-[var(--danger)]">
            {job.error}
          </div>
        )}
      </header>

      {job.state === "needs_human" && flaggedItems.length > 0 && (
        <div className="rise-in rounded-[var(--radius-lg)] border border-[var(--warn)]/40 bg-[var(--warn)]/10 px-5 py-4 text-sm">
          <p className="font-medium text-[var(--warn)]">
            ⚑ {flaggedItems.length} {flaggedItems.length === 1 ? "item needs" : "items need"} a human — still failing
            after {flaggedItems[0]?.iterations ?? 5} attempts.
          </p>
          <ul className="mt-2 flex flex-wrap gap-2">
            {flaggedItems.map((r) => (
              <li key={r.item_id}>
                <a href={`#item-${r.item_id}`} className="rounded-full bg-[var(--surface-1)] px-2.5 py-0.5 font-mono text-xs text-[var(--text-muted)] hover:text-[var(--warn)]">
                  {r.item_id}
                </a>
              </li>
            ))}
          </ul
[truncated — 1295 more characters]
```

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