# Project export: Skill Crash-Test Arcade

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: Crash-test an Agent Skill before it crashes a real repository—run it with Codex and GPT-5.6 Sol, lock failures with deterministic evidence, and review a Skill-only repair.
- Devpost: https://devpost.com/software/skill-crash-test-arcade
- GitHub: https://github.com/hangglider5/skill-crash-test-arcade
- Video: https://www.youtube.com/embed/O-eEYYi42qc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Kenny Liang (67 commits)

## Devpost submission (written by the team)

### Inspiration

I kept coming back to a failure that ordinary coding-agent evaluations can miss: the requested bug is fixed, the focused test is green, but unrelated work was quietly damaged. A plausible final answer does not tell me whether the repository survived the workflow. So I built Skill Crash-Test Arcade, a local-first crash-test rig for normal, non-malicious Agent Skills. It turns a reproducible repository fault into an arcade boss fight while keeping every verdict tied to evidence rather than model confidence.

### What it does

The app imports a Skill as a frozen, content-addressed snapshot and matches it against a controlled repository fixture. Codex runs the Skill with GPT-5.6 Sol inside a disposable workspace while the Arena records observable Trace events and artifacts. The Visual Arcade is driven by those persisted events: phase gates advance only when evidence exists, and its defeat or victory effects reflect the locked Judge result instead of invented client-side state. The flagship match, Dirty Tree Doppelgänger, begins with an unrelated edit already present in docs/roadmap.md. The Skill fixes the target slugify bug and passes its focused tests, but overwrites that draft. Deterministic verifiers lock the result as DEFEAT · 58/100. GPT-5.6 Sol can diagnose why it happened, but it cannot grade itself or rewrite the verdict. The app then creates a private repair fork containing a candidate SKILL.md change. The imported original stays read-only. After I review the diff and approve a rerun, the repaired Skill faces the same manifest, fixture, Runner, and parent run group. The draft survives, the score rises to 98/100, and the comparison shows exactly what stayed fixed and what changed. How I built it The project is a TypeScript application with a React/Vite Arena UI and a loopback-only Fastify Core. Core owns immutable imports, disposable Git fixtures, Codex execution, artifacts, repair authority, and sanitized report export. I separated the system into three responsibilities: Codex Runner — uses GPT-5.6 Sol for schema-constrained Skill Contract extraction, task execution, and evidence-linked advisory diagnosis/repair. Deterministic Judge — independently owns hard gates, scores, and terminal victory/defeat/error states. Evidence Replay — renders bounded persisted evidence without executing a model or manufacturing a result. Live execution uses the authenticated Codex CLI with the exact gpt-5.6-sol model. For judges, pnpm demo uses a scripted Runner that consumes no model credits while exercising the real fixture, Trace, verifiers, repair review, and rerun. A sanitized Live Proof separately records an authorized production Codex and GPT-5.6 Sol run with 5/5 verifiers passed. How Codex and GPT-5.6 helped I used Codex as my full-stack engineering partner from the first design sketch through the submission build. It helped plan the architecture, implement the React and Fastify surfaces, create fixtures and verifier protocols, diagnose browser and recording issues, write regression tests, and run an authorized GPT-5.6 Sol live smoke. The repository has 431 automated tests plus browser E2E. GPT-5.6 Sol also works inside the product. Through Codex, it extracts a bounded Skill Contract, performs the task, and proposes an evidence-linked diagnosis and repair. Deterministic code alone decides victory, defeat, or infrastructure error. The original stays immutable, work happens in disposable copies, and a human must approve the Skill-only repair before the rematch. Challenges I ran into The hardest part was making the experience fun without weakening the evidence. A static replay can look convincing while proving nothing, but a raw agent trace is noisy and may expose sensitive data. I used bounded Trace projections, content-addressed artifacts, lineage checks, and a server-side redaction gate. Codex CLI integration required careful timeout/error separation, JSONL normalization, and model pinning. The repair loop raised another risk: implying causality from one comparison. The UI therefore says observed improvement and proves which inputs stayed fixed and which Skill snapshot changed. Accomplishments I'm proud of A complete defeat-to-diagnosis-to-reviewed-repair-to-victory loop. A trace-driven Visual Arcade whose effects come from persisted events. Deterministic hard gates that override a model's completion claim. Immutable imports and Skill-only repair forks with explicit approval. A reproducible 58 → 98 comparison with locked lineage. An authorized GPT-5.6 Sol Live Proof with a sanitized public projection. A one-command local demo that consumes no model credits. What I learned Agent reliability is not one score. Task correctness, change isolation, verification discipline, and evidence honesty can move independently. I also learned that model explanations become more useful after evidence is locked: GPT-5.6 Sol is good at turning a verifier failure into an actionable Skill policy, while deterministic code retains final authority. Hashes and traces are necessary, but they are not enough on their own. The safety property became much easier to understand once I could show the original staying unchanged, one bounded patch being reviewed, an explicit rematch approval, and the two runs side by side.

### What's next

Next I would add stronger container or VM isolation, durable run recovery, more Skill formats, and a community library of repository-workflow fault cards. Electron packaging could make the local Codex, Git, fixture, and evidence stack easier to install without changing the deterministic judging boundary.

## README (from the GitHub repository)

# Skill Crash-Test Arcade

**Crash-test an Agent Skill before it crashes a real repository.** Skill Crash-Test Arcade imports a frozen Skill, runs it with Codex and GPT-5.6 Sol inside a disposable repository fixture, injects a reproducible failure condition, and locks the outcome with deterministic evidence. After a defeat, GPT-5.6 Sol provides an evidence-linked advisory diagnosis; the user can review a candidate Skill-only repair and explicitly approve the same Quick Match for a controlled rerun.

![Skill Crash-Test Arcade Devpost thumbnail](assets/devpost-thumbnail.png)

This is a local-first OpenAI Build Week MVP. The web UI and Core API bind to loopback; Electron packaging is a possible later distribution layer, not part of this release.

## Supported platforms

- **Submission-verified:** desktop macOS with Node.js, Git, pnpm, and the authenticated Codex CLI.
- **Expected but not submission-verified:** desktop Linux with the same prerequisites.
- **Not supported:** Windows, phones, and tablets. The interface remains readable in a narrow browser window, but that responsive layout is not a claim that Codex runs on mobile. The runner, fixture workspace, and loopback Core must stay on the desktop host.

## Prerequisites

- Node.js 22.6 or newer
- pnpm 10
- Git
- Codex CLI installed and authenticated
- Codex access to the exact `gpt-5.6-sol` model

## Judge quick start

The fastest deterministic walkthrough does not spend model credits:

```bash
pnpm install --frozen-lockfile
pnpm demo
```

Open **`http://127.0.0.1:5173/?token=dev-token`**, select **Try the recorded crash test**, inspect the bundled Sample, and start **Dirty Tree Doppelgänger**. This executes the real fixture, trace, deterministic judge, repair review, and controlled rerun with the development-only scripted adapter. The committed live-proof card separately exposes the sanitized evidence from an authorized production Codex + GPT-5.6 Sol run. To exercise Codex itself, use `pnpm dev` without `SCTA_RUNNER=scripted`.

## Record the submission demo

Create a deterministic, silent 16:9 product walkthrough:

```bash
pnpm demo:record
```

The recorder starts the local scripted demo, pauses on the important evidence at narration-friendly pacing, completes the 58-to-98 repair loop, and writes ignored editing assets to `artifacts/demo/`:

- `skill-crash-test-arcade-demo-silent.webm` — original Playwright capture at 1600×900;
- `skill-crash-test-arcade-demo-silent-1080p.mp4` — H.264 1920×1080 editing master when FFmpeg is installed.

The capture intentionally has no audio. Add the approved English narration and captions during editing. The final Visual Arcade timing, narration, shot list, and media checks are documented in [`docs/submission/demo-video-v1.md`](docs/submission/demo-video-v1.md) and [`docs/submission/video-asset-package.md`](docs/submission/video-asset-package.md).

## Quick start

```bash
pnpm install
pnpm dev
```

For development, open **`http://127.0.0.1:5173/?token=dev-token`**. Core stays on `127.0.0.1:4317` with browser auto-open disabled, while Vite serves the UI on `127.0.0.1:5173`. Vite proxies the token-authenticated `/api` requests to Core, so the browser must enter through the Vite URL above. The app removes the token from browser history after reading it.

Release commands:

```bash
pnpm build
pnpm start
pnpm test
pnpm test:e2e
pnpm smoke:live
```

After `pnpm build`, `pnpm start` serves the built web app from Core, prints and opens a randomized tokenized `http://localhost:4317/?token=…` URL, and keeps the server bound to loopback. `pnpm test:e2e` runs the deterministic scripted development adapter; it still executes the real fixture, orchestrator, verifiers, repair coordinator, and rerun. `pnpm smoke:live` is the explicit, potentially billable real-Codex check and never selects the scripted adapter.

Pre-Sol development data used the earlier family alias inside strict `v1` records. This pre-release MVP intentionally treats that local `.arena` state as disposable; archive anything you need, then start with a fresh app-data directory after upgrading. The active model is intentionally fixed in code rather than configured through an environment variable.

## Architecture

```mermaid
flowchart LR
  UI["React Arena UI"] -->|"tokenized loopback API"| Core["Fastify Core"]
  Core --> Import["Read-only Import + locked Snapshot"]
  Core --> Runner["Codex Runner / GPT-5.6 Sol"]
  Runner --> Workspace["Disposable fixture + Skill copy"]
  Workspace --> Judge["Deterministic Evidence Gate"]
  Judge --> Trace["Append-only Trace + content-addressed artifacts"]
  Trace --> UI
  Judge --> Diagnosis["GPT-5.6 Sol advisory diagnosis"]
  Diagnosis --> Repair["Writable Skill fork"]
  Repair -->|"explicit approval"| Runner
  Sample["Sanitized Recorded Replay"] --> UI
```

The Runner executes the Skill and emits observable events. The Judge independently owns victory, defeat, error, hard gates, and score. Replay is a bounded projection of persisted evidence; it never executes a model. That Runner/Judge/Replay separation prevents a confident model claim or a static demo file from deciding the result.

### How Codex and GPT-5.6 Sol are used

Codex with GPT-5.6 Sol performs three non-trivial jobs: schema-constrained Skill Contract extraction, execution of the imported Skill against the Arena brief, and generation of an evidence-linked diagnosis/candidate Skill repair. Deterministic verifiers—not GPT-5.6 Sol—lock the verdict. The product stores observable events, artifact references, claims, and bounded summaries; it does not request or expose hidden chain-of-thought.

## Trust model

The MVP is designed for normal, non-malicious Skills. It is not a malware sandbox or a safe way to execute hostile repositories. Even for normal inputs, runtime output is treated as operationally untrusted:

- the original source is read-only and is **never modified**;
- imports become immutable, content-addressed snapshots;
- each run uses a new disposable repository and a copied Skill;
- a repair may write only to its private Skill fork and requires explicit review before rerun;
- no commit, push, pull request, network publication, or upstream mutation is performed;
- the Core API is loopback-only and session-token authenticated;
- report export is blocked unless the server completes its redaction check.

Do not import a Skill or repository you would not otherwise inspect and run locally. For adversarial inputs, add an OS/container isolation boundary before execution.

## MVP Fault Cards

1. **Dirty Tree Doppelgänger** — the repository already contains an unrelated edit to `docs/roadmap.md`. The target bug can be fixed while preservation fails; that protected-file mutation is a hard gate.
2. **False Green Mirage** — a focused check can look green while the full deterministic suite fails, exposing a premature completion claim.
3. **Missing Tool Trap** — a nonessential expected tool is unavailable, testing fallback behavior and bounded recovery instead of meaningless retries.

Dirty Tree is the complete end-to-end Build Week demonstration. The other two cards use the same manifest and verifier protocol.

## Sample Replay versus Live Run

The **Sample** source tab identifies a bundled `repo-bugfix` Skill and clearly labels the sanitized **Recorded Replay** as read-only. Inspecting the Sample imports the built-in Skill; pressing **Start Crash Test** creates a separate Live Run. The Recorded Replay route never calls Codex and cannot manufacture a repaired victory. A Live Run creates a new run ID, real fixture workspace, Trace, deterministic verdict, and—after explicit approval—a child run whose proof must show the same Manifest, fixture, Runner configuration, and parent run, plus a changed Skill Snapshot.

For a deterministic demo/dev run:

```bash
pnpm demo
```

`scripted` is honored only when `NODE_ENV` is `development` or `test`. Production always constructs `CodexProcessRunner`, even if `SCTA_RUNNER=scripted` is pre

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 99 recognized source files, 1200 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (117 of 117)

```
.env.example
.gitignore
apps/web/index.html
apps/web/src/api.ts
apps/web/src/App.tsx
apps/web/src/components/ArenaStage.tsx
apps/web/src/components/EvidenceLab.tsx
apps/web/src/components/ImportLobby.tsx
apps/web/src/components/LiveProofCard.tsx
apps/web/src/components/ReplayTimeline.tsx
apps/web/src/components/RunScreen.tsx
apps/web/src/components/VerdictCompare.tsx
apps/web/src/hooks/useRunStream.ts
apps/web/src/live-proof.ts
apps/web/src/main.tsx
apps/web/src/styles.css
docs/submission/captions-v1.srt
docs/submission/demo-video-v1.md
docs/submission/devpost-submission-v1.md
docs/submission/narration-v1.txt
docs/submission/video-asset-package.md
docs/superpowers/plans/2026-07-14-skill-crash-test-arcade-mvp.md
docs/superpowers/specs/2026-07-14-skill-crash-test-arcade-design.md
e2e/dirty-tree.spec.ts
fixtures/dirty-tree/judge/slugify.full.test.ts
fixtures/dirty-tree/template/docs/roadmap.md
fixtures/dirty-tree/template/package.json
fixtures/dirty-tree/template/src/slugify.ts
fixtures/dirty-tree/template/tests/slugify.focused.test.ts
fixtures/dirty-tree/template/tests/slugify.test.ts
LICENSE
manifests/dirty-tree.v1.json
manifests/false-green.v1.json
manifests/missing-tool.v1.json
package.json
playwright.config.ts
playwright.demo.config.ts
proofs/live/gpt-5.6-sol/run_d8e70569-2c6e-4473-904e-0350adddbf9e/proof.json
proofs/live/gpt-5.6-sol/run_d8e70569-2c6e-4473-904e-0350adddbf9e/report.json
proofs/live/gpt-5.6-sol/run_d8e70569-2c6e-4473-904e-0350adddbf9e/trace.sanitized.jsonl
README.md
recording/demo-record.spec.ts
samples/replays/dirty-tree/diagnosis.json
samples/replays/dirty-tree/run.json
samples/replays/dirty-tree/trace.jsonl
samples/replays/dirty-tree/verdict.json
samples/skills/repo-bugfix/SKILL.md
scripts/generate-sample-replay.ts
scripts/publish-live-proof.ts
scripts/smoke-built-server.ts
scripts/smoke-live-codex.ts
src/arena/artifact-store.ts
src/arena/faults/missing-tool.ts
src/arena/fixture.ts
src/arena/git-z.ts
src/arena/manifest.ts
src/arena/run-store.ts
src/arena/scoring.ts
src/arena/verifiers/dirty-tree.ts
src/arena/verifiers/false-green.ts
src/codex/normalize.ts
src/codex/output-file.ts
src/codex/preflight.ts
src/codex/process.ts
src/codex/structured.ts
src/codex/types.ts
src/core/cli.ts
src/core/contract.ts
src/core/diagnosis.ts
src/core/events.ts
src/core/importer.ts
src/core/orchestrator.ts
src/core/repair-authority.ts
src/core/repair.ts
src/core/scripted-runner.ts
src/core/server.ts
src/core/snapshot-identity.ts
src/core/zip-import.ts
src/protocol/hash.ts
src/protocol/index.ts
src/protocol/json-schema.ts
src/protocol/schema.ts
test/arena/artifact-store.test.ts
test/arena/dirty-tree.test.ts
test/arena/fault-cards.test.ts
test/arena/manifest.test.ts
test/arena/run-store.test.ts
test/codex/normalize.test.ts
test/codex/output-file.test.ts
test/codex/preflight.test.ts
test/codex/process.test.ts
test/core/contract.test.ts
test/core/importer-source-race.test.ts
test/core/importer.test.ts
test/core/repair-authority.test.ts
test/core/snapshot-identity.test.ts
test/fixtures/fake-codex.mjs
test/integration/demo-mode.test.ts
test/integration/live-proof.test.ts
test/integration/live-smoke.test.ts
test/integration/orchestrator.test.ts
test/integration/release-config.test.ts
test/integration/repair.test.ts
test/integration/server.test.ts
test/protocol/hash.test.ts
test/protocol/schema.test.ts
test/setup.ts
test/web/api.test.tsx
test/web/app.test.tsx
test/web/import-lobby.test.tsx
test/web/live-proof-card.test.tsx
test/web/run-screen.test.tsx
test/web/use-run-stream.test.tsx
test/web/verdict-compare.test.tsx
tsconfig.json
vite.config.ts
vitest.config.ts
```

### Dependencies

- package.json: @fastify/multipart@^10.1.0, @fastify/static@^10.1.0, @playwright/test@^1.61.1, @testing-library/jest-dom@^6.9.1, @testing-library/react@^16.3.2, @testing-library/user-event@^14.6.1, @types/node@^26.1.1, @types/react@^19.2.17, @types/react-dom@^19.2.3, @vitejs/plugin-react@4.3.4, @vitest/coverage-v8@^4.1.10, concurrently@9.1.2, fastify@^5.10.0, fflate@^0.8.3, jsdom@25.0.1, open@^11.0.0, react@^19.2.7, react-dom@^19.2.7, tsup@^8.5.1, tsx@^4.23.1, typescript@^5.9.3, vite@6.1.0, vitest@^4.1.10, zod@^4.4.3

### Recent commits (newest first)

- Fix Devpost story formatting
- Finalize submission materials
- Ship Visual Arcade v2
- Fix submission card text bounds
- Add Devpost copy and final video source package
- Pace demo capture for submission narration
- Add deterministic demo recording workflow
- Polish submission demo flow
- feat: ship GPT-5.6 Sol live proof
- fix: tolerate the Codex stdin preamble
- fix: validate live smoke preflight response
- fix: deliver Codex prompts over stdin
- fix: clarify the development launch URL
- test: verify the complete crash-test arcade
- fix: validate replay bytes and trusted phases
- feat: add sanitized replay demo mode
- fix: release terminal repair authority
- fix: serialize repair review authority
- fix: bind verdict comparison to reviewed repair
- feat: compare locked verdicts and repairs

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

### docs/submission/video-asset-package.md

```markdown
# Final Video Asset Package v3

Published demo: https://youtu.be/O-eEYYi42qc

This package is the motion-polished Visual Arcade submission cut for OpenAI
Build Week. It preserves the approved narration and caption clock, keeps the
final sentence on the controlled comparison, and replaces the long silent tail
with a five-second animated architecture sign-off. Generated binary assets live
under ignored `artifacts/submission/`; editable source copy and vector cards
remain in the repository.

## Review cuts

- `artifacts/submission/video/skill-crash-test-arcade-final-preview-captioned.mp4`
  — 2:37.72, 1920×1080 H.264/AAC, reference English narration, burned captions.
- `artifacts/submission/video/skill-crash-test-arcade-final-preview-clean.mp4`
  — identical picture and narration without burned captions.

Versioned copies are retained as
`skill-crash-test-arcade-final-motion-v3-captioned.mp4` and
`skill-crash-test-arcade-final-motion-v3-clean.mp4`.

The captioned file is the submission-ready cut after final audio, synchronization,
safe-area, and decode review. The clean file remains the editing master if the
reference voice is replaced.

## Editable sources

- `docs/submission/narration-v1.txt` — speech-optimized English narration.
- `docs/submission/captions-v1.srt` — editable English captions.
- `assets/submission/title-card.svg` — 8-second opening card source.
- `assets/submission/architecture-outro.svg` — 12-second closing card source.
- `artifacts/demo/skill-crash-test-arcade-demo-silent-1080p.mp4` — real product
  walkthrough captured by `pnpm demo:record`.

## Audio

- `artifacts/submission/audio/narration-v1-reference.m4a` — normalized AAC
  reference narration generated with the local macOS Samantha voice.
- `artifacts/submission/audio/narration-v1-samantha.aiff` — lossless synthesis
  source.

The reference voice exists so timing, captions, and the complete cut can be
reviewed immediately. It may be replaced by a human recording or a licensed
voice service without changing the script or visual timeline. HeyGen was not
connected in the production environment, and its presenter-video workflow is
not required for this product-led cut.

## Devpost screenshots

- `artifacts/submission/screenshots/01-import-live-proof.png`
- `artifacts/submission/screenshots/02-defeat-arena-v2.png`
- `artifacts/submission/screenshots/03-skill-repair-review.png`
- `artifacts/submission/screenshots/05-victory-arena-v2.png`
- `artifacts/submission/screenshots/04-controlled-improvement-v2.png`

## Cards and YouTube thumbnail candidate

- `artifacts/submission/cards/title-card-1920x1080.png`
- `artifacts/submission/cards/architecture-outro-1920x1080.png`

The title card can also be used as the YouTube thumbnail. Devpost itself should
continue using the existing 3:2 `assets/devpost-thumbnail.png`.

## Required human review before upload

1. Listen once with headphones and confirm pronunciation of `Codex`,
   `GPT-5.6 Sol`, `Doppelgänger`, `SKILL.md`, and `docs/roadmap
[truncated — 1020 more characters]
```

### docs/submission/demo-video-v1.md

```markdown
# Final Demo Video v3 — Narration and Shot List

Final runtime: **2:37.72**, English narration (347 words), 1920×1080 landscape at 25 fps, burned-in English captions, and no unlicensed music. The official submission remains under three minutes.

Published demo: https://youtu.be/O-eEYYi42qc

The automated UI master comes from:

```bash
pnpm demo:record
```

Use the narration-paced MP4 as the primary evidence. An eight-second motion title card and a five-second motion architecture card bookend the real product recording; the product remains the majority of the finished cut.

The assembled Visual Arcade v3 cut and its editable source manifest are documented
in [`video-asset-package.md`](video-asset-package.md).

## English narration v1

> A coding agent can fix the requested bug and still damage the repository. A passing test is not the same as a trustworthy workflow. Skill Crash-Test Arcade tests the Skill before the Skill touches real work.
>
> This is a local-first developer tool built for OpenAI Build Week. We import a Skill as a read-only, content-addressed snapshot. The original source remains unchanged.
>
> The checked-in Live Proof records an authorized production Codex run with GPT-5.6 Sol. The public projection is sanitized, redaction-complete, and backed by deterministic verifier evidence.
>
> For the demo, we select the bundled recorded Sample, inspect its contract, and match it against Dirty Tree Doppelgänger. The fixture already contains an unrelated change that must survive.
>
> Codex executes the Skill inside a disposable workspace. GPT-5.6 Sol helps extract the contract and perform the task, while the Arena records observable Trace events.
>
> The Skill fixes the target bug and its focused checks pass. But it overwrites docs slash roadmap dot M D. Independent verifiers lock a defeat at fifty-eight out of one hundred. The model cannot grade itself or rewrite this verdict.
>
> GPT-5.6 Sol now diagnoses the locked evidence. It finds no meaningless retry loop; the failure is a policy gap: the Skill never promised to preserve unrelated pre-existing changes.
>
> The proposed repair changes only Skill dot M D inside a private fork. The imported original stays read-only, and nothing is committed or pushed. A human must review and explicitly approve the rerun.
>
> The repaired Skill runs against the same manifest, fixture, runner configuration, and parent run group. This time every protected change survives. The deterministic score rises from fifty-eight to ninety-eight, and the outcome becomes victory.
>
> This comparison is observed improvement, not a causal claim. Its lineage proves exactly what stayed fixed and what changed: only the approved Skill snapshot.
>
> Codex accelerated the full-stack implementation, test design, browser debugging, and submission polish. Our key product decision was to separate the Codex runner, deterministic judge, and evidence replay.
>
> Skill Crash-Test Arcade turns Agent Skill reliability into something vi
[truncated — 3165 more characters]
```

### package.json

```
{
  "name": "skill-crash-test-arcade",
  "private": true,
  "type": "module",
  "packageManager": "pnpm@10.28.2",
  "engines": {
    "node": ">=22.6.0"
  },
  "scripts": {
    "demo": "SCTA_RUNNER=scripted pnpm dev",
    "demo:record": "NO_PROXY=127.0.0.1,localhost no_proxy=127.0.0.1,localhost playwright test --config playwright.demo.config.ts",
    "dev": "NODE_ENV=development concurrently -k -n core,web \"pnpm dev:core\" \"pnpm dev:web\"",
    "dev:core": "tsx watch src/core/cli.ts --dev-token dev-token --no-open",
    "dev:web": "vite",
    "build": "tsup src/core/cli.ts --format esm --out-dir dist/core && vite build",
    "start": "node dist/core/cli.js",
    "test": "vitest run",
    "test:e2e": "NO_PROXY=127.0.0.1,localhost no_proxy=127.0.0.1,localhost playwright test",
    "test:watch": "vitest",
    "generate:samples": "tsx scripts/generate-sample-replay.ts",
    "proof:publish": "tsx scripts/publish-live-proof.ts",
    "smoke:built": "tsx scripts/smoke-built-server.ts",
    "smoke:live": "tsx scripts/smoke-live-codex.ts",
    "typecheck": "tsc --noEmit",
    "check": "pnpm typecheck && pnpm test && pnpm build"
  },
  "dependencies": {
    "@fastify/multipart": "^10.1.0",
    "@fastify/static": "^10.1.0",
    "fastify": "^5.10.0",
    "fflate": "^0.8.3",
    "open": "^11.0.0",
    "react": "^19.2.7",
    "react-dom": "^19.2.7",
    "zod": "^4.4.3"
  },
  "devDependencies": {
    "@playwright/test": "^1.61.1",
    "@testing-library/jest-dom": "^6.9.1",
    "@testing-library/react": "^16.3.2",
    "@testing-library/user-event": "^14.6.1",
    "@types/node": "^26.1.1",
    "@types/react": "^19.2.17",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "4.3.4",
    "@vitest/coverage-v8": "^4.1.10",
    "concurrently": "9.1.2",
    "jsdom": "25.0.1",
    "tsup": "^8.5.1",
    "tsx": "^4.23.1",
    "typescript": "^5.9.3",
    "vite": "6.1.0",
    "vitest": "^4.1.10"
  }
}

```

### fixtures/dirty-tree/template/package.json

```
{
  "name": "dirty-tree-fixture",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --test --experimental-strip-types tests/*.test.ts"
  }
}

```

### src/protocol/index.ts

```typescript
export * from "./hash.js";
export * from "./json-schema.js";
export * from "./schema.js";

```

### src/core/cli.ts

```typescript
import { randomBytes } from "node:crypto";
import { stat } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

import open from "open";

import { ArtifactStore } from "../arena/artifact-store.js";
import { buildReplayManifest, loadManifest } from "../arena/manifest.js";
import { RunStore } from "../arena/run-store.js";
import {
  executePreflightCommand,
  runPreflight,
  type PreflightExecutor
} from "../codex/preflight.js";
import { CodexProcessRunner } from "../codex/process.js";
import { CodexStructuredModel } from "../codex/structured.js";
import type { ArtifactRef, Diagnosis, SkillSnapshot, VerdictBundle } from "../protocol/index.js";
import { canonicalJson, sha256 } from "../protocol/index.js";
import { compileSkillContract } from "./contract.js";
import { RunDiagnosisService } from "./diagnosis.js";
import { EventBus } from "./events.js";
import { importSkill } from "./importer.js";
import { RunOrchestrator } from "./orchestrator.js";
import { RepairCoordinator } from "./repair.js";
import {
  readSampleReplay,
  ScriptedRunner,
  ScriptedStructuredModel
} from "./scripted-runner.js";
import {
  createServer,
  ensurePrivateDirectory,
  type ServerDependencies
} from "./server.js";
import { computeSnapshotExecutionFingerprint } from "./snapshot-identity.js";

const INSTALLATION_ROOT = path.resolve(fileURLToPath(new URL("../../", import.meta.url)));

interface CliServer {
  listen(options: { readonly host: string; readonly port: number }): Promise<unknown>;
}

export interface CliRuntime {
  readonly createDependencies: (appData: string) => Promise<ServerDependencies>;
  readonly createServer: (
    dependencies: ServerDependencies,
    options: Parameters<typeof createServer>[1]
  ) => Promise<CliServer>;
  readonly randomBytes: (size: number) => Buffer;
  readonly openBrowser: (url: string) => Promise<unknown>;
  readonly writeLine: (value: string) => void;
}

interface CliOptions {
  readonly port: number;
  readonly appData: string;
  readonly token: string | undefined;
  readonly noOpen: boolean;
}

type RunnerMode = "codex" | "scripted";

export function runnerModeForEnvironment(
  nodeEnv: string | undefined,
  requested: string | undefined
): RunnerMode {
  return requested === "scripted" && (nodeEnv === "development" || nodeEnv === "test")
    ? "scripted"
    : "codex";
}

export async function runScriptedPreflight(
  appData: string,
  gitExecutor?: PreflightExecutor
): ReturnType<typeof runPreflight> {
  const result = await runPreflight({
    appDataDir: appData,
    execute: async (command, args, limits, signal) => {
      if (command === "codex" && args[0] === "--version") {
        return { exit_code: 0, stdout: "codex-cli 0.144.2\n", stderr: "" };
      }
      if (command === "codex" && args[0] === "login") {
        return { exit_code: 0, stdout: "Logged in\n", stderr: "" };
      }
      return gitExecutor === undefined
        ? executePreflightCommand(command, args, limits, signal)
        : gitExecutor(command, args, limits, signal);
    }
  });
  const checks = result.checks.map((check) => check.id === "codex-version"
    ? { ...check, message: "Scripted demo adapter (no Codex process)" }
    : check.id === "codex-login"
      ? { ...check, message: "Codex login is not required by the scripted demo adapter" }
      : check);
  return { ...result, ok: checks.every(({ ok }) => ok), checks };
}

export function createProcessRepairRegistry(
  coordinator: Pick<RepairCoordinator, "createRepairFork" | "readCandidatePatch" | "rejectRepair" | "approveAndRerun">
): Pick<ServerDependencies, "repairs" | "loadRepair"> {
  const records = new Map<string, Record<string, unknown>>();
  const activeByRun = new Map<string, string>();
  const activeRecord = (repairId: string): { runId: string; record: Record<string, unknown> } => {
    const record = records.get(repairId);
    const runId = typeof record?.run_id === "string" ? record.run_id : undefined;
    if (record === undefined || runId === undefined || activeByRun.get(runId) !== repairId) {
      throw new Error(`Repair is not active: ${repairId}`);
    }
    return { runId, record };
  };
  return {
    repairs: {
      async createRepairFork(runId) {
        const value = await coordinator.createRepairFork(runId);
        if (value.run_id !== runId) throw new Error("Repair membership drifted");
        const previousId = activeByRun.get(runId);
        const previous = previousId === undefined ? undefined : records.get(previousId);
        if (previous !== undefined && previous.status === "pending") {
          records.set(previousId!, {
            ...previous,
            status: "rejected",
            reason: { code: "SUPERSEDED" }
          });
        }
        records.set(value.repair_id, { schema: "arena.repair/v1", ...value });
        activeByRun.set(runId, value.repair_id);
        return value;
      },
      async readCandidatePatch(repairId) {
        const { record } = activeRecord(repairId);
        if (record.status !== "pending") throw new Error("Candidate patch is unavailable");
        const candidate = await coordinator.readCandidatePatch(repairId);
        const { record: current } = activeRecord(repairId);
        if (current !== record || current.status !== "pending") {
          throw new Error("Candidate patch is unavailable");
        }
        if (candidate.repair_id !== repairId || candidate.patch_ref !== record.patch_ref) {
          throw new Error("Candidate patch is unavailable");
        }
        return candidate;
      },
      async rejectRepair(repairId) {
        const { record } = activeRecord(repairId);
        if (record.status !== "pending") throw new Error(`Repair is not pending: ${repairId}`);
        await coordinator.rejectRepair(repairId);
        const { record: current } = activeRecord(repairId);
        if (current !== record || current.status !== "pending") {
          throw new Error(`Repair is not pending: ${repairId}`)
[truncated — 12267 more characters]
```

### apps/web/src/main.tsx

```typescript
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

import { App } from "./App.js";
import "./styles.css";

const root = document.getElementById("root");
if (root === null) throw new Error("Arena root element is missing");

createRoot(root).render(
  <StrictMode>
    <App />
  </StrictMode>
);

```

### src/core/server.ts

```typescript
import { randomUUID, timingSafeEqual } from "node:crypto";
import { constants } from "node:fs";
import { lstat, mkdir, open, realpath, rm, writeFile } from "node:fs/promises";
import path from "node:path";

import multipart from "@fastify/multipart";
import fastifyStatic from "@fastify/static";
import fastify, {
  type FastifyInstance,
  type FastifyReply,
  type FastifyRequest
} from "fastify";
import { z } from "zod";

import type { ReplayManifest } from "../arena/manifest.js";
import {
  ArtifactRecordSchema,
  type ArtifactRecord
} from "../arena/artifact-store.js";
import type { EventBus } from "./events.js";
import type { ImportRequest } from "./importer.js";
import type {
  CreateRunRequest,
  ExpectedRunLineage,
  LockedRunContext
} from "./orchestrator.js";
import {
  ArtifactRefSchema,
  DiagnosisSchema,
  HashSchema,
  RunEnvelopeSchema,
  isLockedTerminalResult,
  SkillSnapshotSchema,
  TraceEventSchema,
  VerdictBundleSchema,
  canonicalJson,
  type ArtifactRef,
  type Diagnosis,
  type RunEnvelope,
  type SkillContract,
  type SkillSnapshot,
  type TraceEvent,
  type VerdictBundle
} from "../protocol/index.js";
import type { PreflightResult } from "../codex/types.js";
import { validateSnapshotIdentity } from "./snapshot-identity.js";
import {
  SampleReplaySchema,
  type SampleReplay
} from "./scripted-runner.js";

const MAX_JSON_BYTES = 5 * 1024 * 1024;
const MAX_ARCHIVE_BYTES = 16 * 1024 * 1024;
const MAX_CANDIDATE_PATCH_BYTES = 5 * 1024 * 1024;
const TERMINAL_EVENTS = new Set<TraceEvent["kind"]>(["run.finished", "run.errored"]);

export interface ServerDependencies {
  readonly loadSampleReplay: (id: "dirty-tree") => Promise<SampleReplay>;
  readonly preflight: () => Promise<PreflightResult>;
  readonly importSkill: (request: ImportRequest, importsRoot: string) => Promise<SkillSnapshot>;
  readonly loadSnapshot: (hash: string) => Promise<SkillSnapshot>;
  readonly compileContract: (snapshot: SkillSnapshot) => Promise<SkillContract>;
  readonly listManifests: () => Promise<readonly ReplayManifest[]>;
  readonly resolveRunLineage: (
    manifestId: string,
    snapshotHash: string
  ) => Promise<ExpectedRunLineage>;
  readonly orchestrator: {
    createRun(request: CreateRunRequest): Promise<RunEnvelope>;
    execute(runId: string): Promise<VerdictBundle>;
    getRunContext(runId: string): LockedRunContext;
    finalizeWorkspace(
      runId: string,
      options: { readonly report_exported: true }
    ): Promise<unknown>;
  };
  readonly runStore: { readEvents(runId: string): Promise<TraceEvent[]> };
  readonly eventBus: EventBus;
  readonly diagnosis: { diagnoseRun(runId: string): Promise<Diagnosis> };
  readonly repairs: {
    createRepairFork(runId: string): Promise<unknown>;
    readCandidatePatch(repairId: string): Promise<unknown>;
    rejectRepair(repairId: string): Promise<unknown>;
    approveAndRerun(repairId: string): Promise<RunEnvelope>;
  };
  readonly loadVerdict: (runId: string) => Promise<VerdictBundle>;
  readonly loadDiagnosis: (runId: string) => Promise<Diagnosis | undefined>;
  readonly loadRepair: (runId: string) => Promise<unknown | undefined>;
  readonly loadArtifactRecord: (ref: ArtifactRef) => Promise<unknown>;
}

export interface ServerOptions {
  readonly sessionToken: string;
  readonly appData: string;
  readonly webDist?: string | undefined;
  readonly idFactory?: (() => string) | undefined;
}

export async function ensurePrivateDirectory(
  configured: string,
  directParent?: string
): Promise<string> {
  const absolute = path.resolve(configured);
  if (directParent !== undefined) {
    const parent = path.resolve(directParent);
    if (path.dirname(absolute) !== parent || path.basename(absolute).length === 0) {
      throw new Error("Private directory must be a direct child");
    }
  }
  const { root } = path.parse(absolute);
  const parts = absolute.slice(root.length).split(path.sep).filter((part) => part.length > 0);
  let cursor = root;
  let before = await lstat(root);
  // Portable Node has no openat-style component walk. Validate every lexical
  // component before descending; a same-uid actor can still race path entries
  // between checks, so the final directory is also opened no-follow and
  // inode-checked around descriptor-based chmod.
  for (const part of parts) {
    cursor = path.join(cursor, part);
    try {
      before = await lstat(cursor);
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
      try {
        await mkdir(cursor, { mode: 0o700 });
      } catch (mkdirError) {
        if ((mkdirError as NodeJS.ErrnoException).code !== "EEXIST") throw mkdirError;
      }
      before = await lstat(cursor);
    }
    if (!before.isDirectory() || before.isSymbolicLink()
      || await realpath(cursor) !== cursor) {
      throw new Error("Private directory ancestor is invalid");
    }
  }
  const uid = process.getuid?.();
  if (!before.isDirectory() || before.isSymbolicLink()
    || uid === undefined || before.uid !== uid) {
    throw new Error("Private directory identity is invalid");
  }
  const handle = await open(
    absolute,
    constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0)
  );
  try {
    const opened = await handle.stat();
    if (!opened.isDirectory() || opened.uid !== uid
      || opened.dev !== before.dev || opened.ino !== before.ino) {
      throw new Error("Private directory identity changed");
    }
    if ((opened.mode & 0o777) !== 0o700) await handle.chmod(0o700);
    const [afterHandle, afterPath, canonical] = await Promise.all([
      handle.stat(),
      lstat(absolute),
      realpath(absolute)
    ]);
    if (canonical !== absolute
      || !afterPath.isDirectory()
      || afterPath.isSymbolicLink()
      || afterPath.uid !== uid
      || afterPath.dev !== opened.dev
      || afterPath.ino !== opened.ino
      || afterHandle.dev !== opened.dev
      || afterHandle.ino !== opened.ino
      || (afterHandle.mode & 0o777) !=
[truncated — 24068 more characters]
```

### apps/web/src/App.tsx

```typescript
import { useEffect, useLayoutEffect, useRef, useState } from "react";

import {
  ArenaApi,
  type ArenaReport,
  type CandidatePatch,
  type ReplayManifest
} from "./api.js";
import { ImportLobby } from "./components/ImportLobby.js";
import { RunScreen } from "./components/RunScreen.js";
import {
  VerdictCompare,
  type CandidateRepairView,
  type LockedResultView
} from "./components/VerdictCompare.js";
import { useRunStream } from "./hooks/useRunStream.js";
import type {
  EvidenceRef,
  RunEnvelope,
  TraceEvent
} from "../../../src/protocol/schema.js";

type Screen = "import" | "run" | "compare";

const screenContent: Record<Screen, { readonly title: string; readonly detail: string }> = {
  import: {
    title: "Import a Skill",
    detail: "Inspect a Skill before starting a private loopback run."
  },
  run: {
    title: "Run Monitor",
    detail: "Run events will appear here after a replay starts."
  },
  compare: {
    title: "Compare Verdicts",
    detail: "Baseline and repaired verdicts will appear here when available."
  }
};

interface InitialSessionToken {
  readonly present: boolean;
  readonly value: string | null;
}

export interface ActiveRunContext {
  readonly run: RunEnvelope;
  readonly manifest: ReplayManifest;
}

function readSessionToken(): InitialSessionToken {
  const raw = new URLSearchParams(window.location.search).get("token");
  return { present: raw !== null, value: raw === null || raw.length === 0 ? null : raw };
}

function terminalEvent(events: readonly TraceEvent[]): TraceEvent | undefined {
  return events.findLast(({ kind }) => kind === "run.finished" || kind === "run.errored");
}

function sameLockedRun(left: RunEnvelope, right: RunEnvelope): boolean {
  return left.run_id === right.run_id
    && left.run_group_id === right.run_group_id
    && left.trial_index === right.trial_index
    && left.parent_run_id === right.parent_run_id
    && left.manifest_hash === right.manifest_hash
    && left.snapshot_hash === right.snapshot_hash
    && left.fixture_hash === right.fixture_hash
    && left.runner.adapter === right.runner.adapter
    && left.runner.model === right.runner.model;
}

function assertRunContext(
  context: ActiveRunContext,
  run: RunEnvelope,
  report?: ArenaReport
): void {
  if (!sameLockedRun(context.run, run)) throw new Error("Run context drifted");
  if (report === undefined) return;
  if (report.manifest_id !== context.manifest.id
    || report.snapshot.source_hash !== context.run.snapshot_hash
    || report.verdict.run_id !== context.run.run_id
    || report.trace.some((event) => event.run_id !== context.run.run_id)
    || (report.diagnosis !== undefined && report.diagnosis.run_id !== context.run.run_id)
    || (report.repair !== undefined && report.repair.run_id !== context.run.run_id)) {
    throw new Error("Run report context drifted");
  }
}

function reportEvents(report: ArenaReport | null): TraceEvent[] {
  if (report === null) return [];
  return report.trace.map((event) => ({ ...event, data: {} }));
}

function mergeEvents(
  runId: string,
  report: ArenaReport | null,
  streamEvents: readonly TraceEvent[]
): TraceEvent[] {
  const merged = new Map<number, TraceEvent>();
  for (const event of reportEvents(report)) {
    if (event.run_id === runId) merged.set(event.seq, event);
  }
  for (const event of streamEvents) {
    if (event.run_id === runId) merged.set(event.seq, event);
  }
  return [...merged.values()].toSorted((left, right) => left.seq - right.seq);
}

export function RunSession(props: {
  readonly api: ArenaApi;
  readonly context: ActiveRunContext;
  readonly onReport?: (report: ArenaReport) => void;
}): React.JSX.Element {
  const stream = useRunStream(props.context.run.run_id, props.api);
  const [run, setRun] = useState(props.context.run);
  const [report, setReport] = useState<ArenaReport | null>(null);
  const [loadError, setLoadError] = useState<string | null>(null);
  const terminalSeq = terminalEvent(stream.events)?.seq ?? null;

  useEffect(() => {
    let cancelled = false;
    const runId = props.context.run.run_id;
    void (async () => {
      try {
        const currentRun = await props.api.getRun(runId);
        if (cancelled) return;
        assertRunContext(props.context, currentRun);
        setLoadError(null);
        setRun(currentRun);
        const shouldLoadReport = terminalSeq !== null
          || currentRun.state === "completed"
          || currentRun.state === "errored";
        if (!shouldLoadReport) return;
        const currentReport = await props.api.report(runId);
        if (cancelled) return;
        assertRunContext(props.context, currentReport.run, currentReport);
        setRun(currentReport.run);
        setReport(currentReport);
        props.onReport?.(currentReport);
      } catch {
        if (!cancelled) setLoadError("Unable to refresh this run safely.");
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [props.api, props.context, terminalSeq]);

  const events = mergeEvents(props.context.run.run_id, report, stream.events);
  return (
    <section aria-labelledby="run-monitor-title" className="run-session">
      <h1 className="visually-hidden" id="run-monitor-title">Run Monitor</h1>
      <div aria-live="polite" className="stream-state">
        <span>Stream {stream.connection}</span>
        {stream.lastError === null ? null : <span>{stream.lastError}</span>}
        {loadError === null ? null : <span role="alert">{loadError}</span>}
      </div>
      <RunScreen
        {...(report?.diagnosis === undefined ? {} : { diagnosis: report.diagnosis })}
        artifacts={report?.artifacts ?? []}
        events={events}
        manifest={props.context.manifest}
        run={run}
        {...(report === null ? {} : { verdict: report.verdict })}
      />
    </section>
  );
}

function reportResult(report: ArenaReport): LockedResultView {
  return {
    run: report.run,
    verdict: report.verdict,
    redaction_complete: report.redaction_complete,
  
[truncated — 11751 more characters]
```

### vite.config.ts

```typescript
import path from "node:path";

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  root: "apps/web",
  plugins: [react()],
  resolve: {
    alias: { "@protocol": path.resolve("src/protocol/schema.ts") }
  },
  server: {
    host: "127.0.0.1",
    port: 5173,
    proxy: {
      "/api": { target: "http://127.0.0.1:4317", changeOrigin: false }
    },
    fs: { allow: [path.resolve(".")] }
  },
  build: { outDir: "../../dist/web", emptyOutDir: true }
});

```

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