# Review exports

> Open the API-hosted review UI or a local archive, inspect accepted tasks, drop tasks, and download a cleaned tar.gz.

- Repository: mupt-ai/self-bench
- GitHub: https://github.com/mupt-ai/self-bench
- Human docs: https://grok-wiki.com/public/docs/mupt-ai-self-bench-62fff2be9bcc
- Complete Markdown: https://grok-wiki.com/public/docs/mupt-ai-self-bench-62fff2be9bcc/llms-full.txt

## Source Files

- `review/src/App.tsx`
- `review/src/export-loader.ts`
- `review/src/types.ts`
- `review/src/export-loader.test.ts`
- `src/api.ts`

---

---
title: "Review exports"
description: "Open the API-hosted review UI or a local archive, inspect accepted tasks, drop tasks, and download a cleaned tar.gz."
---

The review UI is a React SPA built from `review/` and served by the self-bench API at `GET /`. It lists Temporal workflows, downloads `GET /v1/runs/:runId/export` when an export exists, or opens a local `.tar.gz` / `.tgz`, then lets you mark accepted Harbor tasks for removal and download `selfbench-{runId}-cleaned.tar.gz` in the browser. Drops are session state only: the API has no review-write or cleaned-export upload route.

<Warning>
Exports include repository snapshots, held-out tests, and reference solutions. They are unencrypted. Keep the original and cleaned archives private.
</Warning>

This page is the operator review surface. Independent model coupling review during generation is a different gate; it already finished before a task appears in the export.

## Prerequisites

- A running API (`self-bench up`, Compose `api`, or `node dist/api-main.js`) if you want the hosted UI and run list.
- `SELFBENCH_API_TOKEN` when the API is configured with a token. Compose sets `SELFBENCH_API_HOST=0.0.0.0` and defaults the token to `selfbench-local` unless you export another value.
- A completed run (`phase` `complete` with an `export` artifact) or a previously downloaded export archive.
- A browser. There is no `self-bench review` CLI command.

<Note>
`GET /` and `GET /assets/*` are served before Bearer auth. Listing runs and downloading an export still require `Authorization: Bearer …` when `SELFBENCH_API_TOKEN` is set. Paste that token into **API token (session only)**; the field is React state and is not persisted.
</Note>

## Open the review UI

<Tabs>
<Tab title="API-hosted (local stack)">
<Steps>
<Step title="Start the stack">

```bash
self-bench up --backend modal
export SELFBENCH_API_URL=http://127.0.0.1:8080
```

Compose publishes the API on `127.0.0.1:8080`. The image serves the Vite build from `dist/review` next to `dist/api.js`.

</Step>
<Step title="Open the SPA">

Browse to `http://127.0.0.1:8080/`. The page title is **SelfBench Review**. On load it calls `GET /v1/runs`.

</Step>
<Step title="Authenticate data routes">

If list/export calls return `401`, paste the same value as `SELFBENCH_API_TOKEN` into the password field, then click **Refresh runs**.

</Step>
</Steps>
</Tab>
<Tab title="Local archive only">

Use **Open gzip** on a file with extension `.gz` or `.tgz` (accept `application/gzip`). The UI does not call `/v1/runs/:runId/export` for that file. The run list still queries the API when the page can reach it.

```bash
self-bench download RUN_ID ./self-bench-evals.tar.gz
```

Then open that file in the UI. CLI download verifies `x-content-sha256`; the file picker does not.

</Tab>
<Tab title="Checkout (Vite)">

```bash
bun run dev:api
bun run dev:worker
bun run dev:review
```

`review/vite.config.ts` proxies `/v1` to `http://127.0.0.1:8080`. Use this pair for source UI work: `tsx src/api-main.ts` resolves review assets relative to `src/`, and `src/review` is not the built SPA. Production and the Compose image serve `dist/review` from the compiled API.

</Tab>
</Tabs>

## Two intake paths

| Intake | How | Status header | Task list source |
| --- | --- | --- | --- |
| Completed run | Click a row. Parallel `GET /v1/runs/:runId` and `GET /v1/runs/:runId/export`. | API `phase` (for example `complete`) | Inner `tasks/*.tar.gz` from the export |
| Local gzip | **Open gzip** | Synthetic phase `local export` | Same archive layout |

Opening a run that has no `export` yet fails with **Export is not ready (409)** (`{"error":"run export is not ready"}`). The left list still renders Temporal `status.name` values such as `RUNNING` or `COMPLETED`; that is workflow status, not the SelfBench `phase` field.

The UI does not render per-candidate rejection reasons from `GET /v1/runs/:runId`. Use `self-bench status RUN_ID` or that JSON status payload for `rejected` / `infrastructure_failed` rows.

```mermaid
flowchart LR
  subgraph Browser["Review SPA"]
    App["App"]
    Loader["loadExport / buildCleanExport"]
  end
  subgraph API["self-bench API"]
    Assets["GET / and GET /assets/*"]
    List["GET /v1/runs"]
    Status["GET /v1/runs/:runId"]
    ExportRoute["GET /v1/runs/:runId/export"]
  end
  subgraph Store["Artifact store"]
    Original["selfbench-{runId}.tar.gz"]
  end
  LocalFile["Local .tar.gz / .tgz"]
  Cleaned["selfbench-{runId}-cleaned.tar.gz"]

  Assets --> App
  App -->|"Bearer when token set"| List
  App --> Status
  App --> ExportRoute
  ExportRoute --> Original
  Original --> Loader
  LocalFile --> Loader
  Loader --> Cleaned
```

## Inspect an accepted task

After an export loads, the summary shows `manifest.repository.url`, the run id, `phase`, `manifest.acceptedCount`, and the first 12 characters of `manifest.repository.commit`.

The task list is every inner archive named `tasks/{taskId}.tar.gz` that `manifest.json` lists. Each row is **keep** or **marked for removal**. Selecting a row opens a panel that decodes text files after stripping the `harbor-task/` prefix:

| Panel block | File | Fallback |
| --- | --- | --- |
| Header (`difficulty` · `repo`) | `definition.json` fields | `unknown` and empty repo |
| Prompt | `definition.json` `prompt` | `instruction.md`, else `No prompt` |
| Task definition | `definition.json` | `Missing definition.json` |
| Gold patch | `solution/gold.patch` | `Missing patch` |
| Held-out test patch | `tests/test.patch` | `Missing patch` |

The authoring sandbox tars only `harbor-task/` after compiling `instruction.md`, `task.toml`, environment/verifier Dockerfiles, patches, and `.selfbench-manifest.json`. `definition.json` is stored as a separate authoring artifact and is often absent from the compiled task archive. When it is missing, read the prompt from `instruction.md` (the compiler writes `definition.prompt` there) and treat the definition block as absent rather than as a corrupt export.

The loader also decodes `task.toml`, `.selfbench-manifest.json`, `tests/dependency-setup.patch`, and any other `*.json` path, but the panel does not render them. Environment snapshots such as `environment/repo.tar.gz` stay binary.

Patches render with `@pierre/diffs` `FileDiff`: split view above 900px, unified below, light/dark from `prefers-color-scheme`. If the renderer throws, the raw patch text is shown.

<ParamField body="schemaVersion" type="number">
Copied from `manifest.json`. Generated exports use `1`.
</ParamField>

<ParamField body="runId" type="string">
Copied from `manifest.json`. Becomes the cleaned download basename.
</ParamField>

<ParamField body="acceptedCount" type="number">
Number of accepted tasks in the loaded archive. The cleaned download rewrites this to the kept count.
</ParamField>

<ParamField body="tasks[].taskId" type="string">
Inner archive name without `.tar.gz`.
</ParamField>

<ParamField body="tasks[].sha256" type="string">
SHA-256 of that inner `tasks/{taskId}.tar.gz`. Cleaning keeps the original digest for every kept task.
</ParamField>

## Drop tasks and download a cleaned archive

<Steps>
<Step title="Mark tasks">

**Mark for removal** adds the `taskId` to an in-memory `Set`. **Keep task** removes it. Changing runs or opening another gzip clears the set. Nothing is written to Temporal or the artifact store.

</Step>
<Step title="Download">

The primary button is **Download export copy** when the set is empty, or **Download cleaned export (N)** when N tasks remain. Both paths call `buildCleanExport` and trigger a browser download named `selfbench-{runId}-cleaned.tar.gz`.

</Step>
<Step title="Verify the cleaned tree">

```bash
mkdir -p ./cleaned && tar -tzf ./selfbench-RUN_ID-cleaned.tar.gz
jq '{runId, acceptedCount, tasks: [.tasks[].taskId]}' \
  <(tar -xOf ./selfbench-RUN_ID-cleaned.tar.gz manifest.json)
```

Expect `manifest.json` plus one `tasks/{taskId}.tar.gz` per kept id. `acceptedCount` equals the kept length. `candidateCounts`, `repository`, and `version` are unchanged copies of the source manifest.

</Step>
</Steps>

What cleaning changes:

| Field / entry | Behavior |
| --- | --- |
| `acceptedCount` | Set to the number of kept tasks (may be `0`) |
| `manifest.tasks` | Filtered to kept ids; original `sha256` values retained |
| `tasks/{taskId}.tar.gz` | Original gzip bytes copied; not recompressed |
| Removed task archives | Omitted |
| `candidateCounts` | Not rewritten |
| Server artifact | Unchanged |

The cleaned file is a new gzip (new tar mtimes). It does not carry `x-content-sha256`. `self-bench-eval --export` accepts it because the outer layout is the same as a generated export.

<Info>
Self-bench has no remote deletion route. Cleaning is a local republish step. To discard a whole run's artifacts, delete the local artifact volume or the GCS run prefix with operator tooling.
</Info>

## API surface the UI uses

:::endpoint GET / serve the review SPA
Unauthenticated. Maps `/` to `dist/review/index.html` (`cache-control: no-cache`). Paths under `/assets/` map into the same directory with `public, max-age=31536000, immutable`. Paths that escape that root return `400` `invalid asset path`. Missing files return `404` `asset not found`. Content types: `.html` → `text/html; charset=utf-8`, `.css` → `text/css; charset=utf-8`, `.js` → `text/javascript; charset=utf-8`.
:::

:::endpoint GET /v1/runs list workflows
Requires Bearer auth when `SELFBENCH_API_TOKEN` is set. Returns Temporal executions with `WorkflowType = 'selfBenchRunWorkflow'`.

<ResponseExample>

```json
[
  {
    "runId": "sb-20260814120000-abcd1234",
    "status": "COMPLETED",
    "startedAt": "2026-08-14T12:00:00.000Z",
    "closedAt": "2026-08-14T14:10:00.000Z"
  }
]
```

</ResponseExample>
:::

:::endpoint GET /v1/runs/:runId read run status
`:runId` must match `^[a-z0-9][a-z0-9-]{2,62}$`. Used with the export GET when you click a run.

<ResponseField name="phase" type="string">
`queued` · `discovering` · `authoring` · `exporting` · `complete` · `blocked` · `failed` · `cancelled`
</ResponseField>

<ResponseField name="export" type="object">
Present only after `buildExport` stored the archive. Fields: `uri`, `sha256`, `sizeBytes`, `contentType`.
</ResponseField>
:::

:::endpoint GET /v1/runs/:runId/export download the generated archive
`409` when `export` is missing. On success:

| Header | Value |
| --- | --- |
| `content-type` | Artifact `contentType` (`application/gzip`) |
| `content-length` | `sizeBytes` |
| `content-disposition` | `attachment; filename="selfbench-{runId}.tar.gz"` |
| `x-content-sha256` | Artifact SHA-256 |

The UI feeds the response blob into `loadExport`. The CLI `self-bench download` also consumes this route and refuses the file if the body hash does not match the header.
:::

## Loader constraints

`loadExport` gunzips the outer archive, requires a top-level `manifest.json`, then for every `manifest.tasks[]` entry requires `tasks/{taskId}.tar.gz`, gunzips that archive, and indexes files after removing a leading `harbor-task/` prefix. Extra members not listed in the manifest are ignored.

```text
manifest.json
tasks/
├── task-one.tar.gz
└── task-two.tar.gz
```

<RequestExample>

```json
{
  "schemaVersion": 1,
  "runId": "sb-20260814120000-abcd1234",
  "candidateCounts": { "easy": 10, "medium": 10, "hard": 10 },
  "repository": {
    "url": "https://github.com/example/repo",
    "commit": "0123456789abcdef0123456789abcdef01234567"
  },
  "version": {
    "selfbenchCommit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "executionBackend": "modal",
    "sandboxImage": "node:22-bookworm",
    "schema": 1
  },
  "acceptedCount": 2,
  "tasks": [
    { "taskId": "task-one", "sha256": "aa…64 hex…" },
    { "taskId": "task-two", "sha256": "bb…64 hex…" }
  ]
}
```

</RequestExample>

Hard failures (shown in the red banner):

| Message | Cause |
| --- | --- |
| `Unable to list runs ({status})` | `GET /v1/runs` not OK (often `401`) |
| `Unable to load run ({status})` | Status GET failed |
| `Export is not ready ({status})` | Export GET failed; `409` means no artifact yet |
| `export is missing manifest.json` | Not a self-bench outer archive |
| `export is missing tasks/{taskId}.tar.gz` | Manifest/archive mismatch |

## After you publish a cleaned export

<CardGroup>
<Card title="Export format" href="/export-format">
Outer tar.gz layout, manifest fields, and what the archive excludes.
</Card>
<Card title="Evaluate with Harbor" href="/evaluate-with-harbor">
Point `self-bench-eval --export` at the cleaned file or extract one `harbor-task`.
</Card>
</CardGroup>

## Troubleshooting

| Symptom | What to check |
| --- | --- |
| **No runs found** | API unreachable, wrong token, or no `selfBenchRunWorkflow` executions. Confirm `GET http://127.0.0.1:8080/healthz` returns `{"ok":true}`. |
| **401 on refresh** | Paste `SELFBENCH_API_TOKEN`. Compose default is `selfbench-local` if you did not set one. |
| **Export is not ready (409)** | Wait until `self-bench status RUN_ID` shows `phase: "complete"` and an `export` object. Blocked or failed runs never grow an export. |
| **Missing definition.json** | Expected for compiler-produced Harbor tasks. Use **Prompt** from `instruction.md`. |
| **Missing patch** | Inner archive lacks `solution/gold.patch` or `tests/test.patch`. |
| **Pierre renderer falls back to text** | Invalid or unsupported diff; the raw patch is still shown. |
| **Cleaned file rejected by a hash check** | Only the API original has `x-content-sha256`. Re-hash the cleaned bytes yourself if you need a digest. |
| **Need the original, not a recopy** | `self-bench download RUN_ID OUTPUT.tar.gz` writes create-only and verifies SHA-256. |

## Related pages

<CardGroup>
<Card title="Manage runs" href="/manage-runs">
`list`, `status`, `cancel`, and SHA-256 `download` against `SELFBENCH_API_URL`.
</Card>
<Card title="HTTP API" href="/http-api">
Bearer auth, status codes, export headers, and review-asset serving.
</Card>
<Card title="Start the local stack" href="/start-local-stack">
`self-bench up` / `down`, Compose services, and the `127.0.0.1:8080` publish.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
`409` export-not-ready, SHA-256 mismatch, and leftover Docker resources.
</Card>
<Card title="Contributing" href="/contributing">
`bun run dev:review`, `bun test tests review/src`, and `build:review`.
</Card>
</CardGroup>
