# Project export: Groundline

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: A causal laboratory that computes a company’s operational ground truth, lets GPT-5.6 employees report through a hierarchy, and measures how incentives and attention change what reaches the executives.
- Devpost: https://devpost.com/software/groundline
- GitHub: https://github.com/RoyCoding8/Groundline
- Video: https://player.vimeo.com/video/1211847558?byline=0&portrait=0&title=0#t=
- Team: 1 GitHub contributor(s) — Shashwata Roy (2 commits)

## Devpost submission (written by the team)

### Overview

Project description Organizations often fail after a problem has already been seen. The problem changes as it moves upward: a production incident becomes a delay, the delay becomes a risk, and the risk becomes a reassuring status update. By the time a decision-maker sees it, the report may sound coherent while no longer matching reality. Many language-agent simulations combine the state generator and the reporter, leaving no independent truth. A transcript can be persuasive while still lacking evidence of distortion. Groundline gives the operational world its own authority. The world engine computes the state; language carries reports about local evidence. The system runs a small simulated company with persistent employees arranged in a configurable reporting tree. The included scenario has Product, Engineering, and QA departments. The configuration also supports deeper, unbalanced trees and gives each employee a role, department, skills, honesty, blame sensitivity, and utility weights that remain stable throughout the run. The deterministic world engine owns the business state. It computes task progress, dependencies, quality, defects, deadlines, reliability, and incident severity from a seed and a scenario. A real shock is injected at a known point in the run. No model response can directly set those values. Default demo The default demo is deliberately small enough to inspect: Each run contains a request, event ledger, decision ledger, distortion metrics, and hash-verifiable manifest. Reporting path Employees receive scoped evidence. Hidden world state stays outside the policy context. A policy may choose a report and a permitted action, while the world engine resolves the action and records its real consequence. Reports travel upward one level at a time, so a manager sees subordinate reports and local evidence, never a privileged copy of the simulator’s truth. The executive dashboard therefore represents a belief assembled through the hierarchy while the simulator keeps an authoritative record beside it. Groundline then measures the difference. Distortion is calculated numerically between reported state and world truth, broken down by time, department, and hierarchy depth. The operator can change incentive pressure and manager attention, hold the seed fixed, rerun the same company, and compare the curves. A no-pressure ablation checks whether distortion shrinks when the proposed mechanism is removed. The evidence is the metric’s response to the intervention; transcripts provide context for the measured path. Measurement For employee $i$ at tick $t$, the report and world state each contain four normalized health dimensions: progress, quality, schedule, and reliability. Groundline computes a weighted vector loss: $$ L_{i,t} = 0.20\lvert p^{r}{i,t}-p^{w}{t}\rvert + 0.30\lvert q^{r}{i,t}-q^{w}{t}\rvert + 0.20\lvert s^{r}{i,t}-s^{w}{t}\rvert + 0.30\lvert r^{r}{i,t}-r^{w}{t}\rvert $$ The optimism bias used for upward amplification is the difference between the report’s aggregate health score and the world score: $$ B_{i,t}=H^{r}{i,t}-H^{w}{t} $$ The configured upward-amplification outcome compares adverse reports from the root executive with adverse contributor reports: $$ A_{\mathrm{exec},\mathrm{contrib}} =\operatorname{wmean}(B_{\mathrm{exec},t}) -\operatorname{wmean}(B_{\mathrm{contrib},t}) $$ The weighted means use each report’s scoped business value, and only rows below the configured adverse-health threshold enter the outcome. A positive $A_{\mathrm{exec},\mathrm{contrib}}$ means the executive report is more optimistic relative to truth. The experiment uses paired seeds across a 2×2 design: incentive pressure crossed with manager attention. The same underlying random world is reused in every treatment cell, making the seed the unit of comparison. Groundline reports seed-level contrasts and uncertainty because messages from one run are correlated. Its statistical layer includes paired randomization tests, bootstrap intervals, multiplicity correction, sensitivity checks, and factorial main-effect and interaction estimates. Fixture-policy results are engineering verification for the instrument; their estimates stop at the configured simulation. Intervention matrix The same seed (s) appears in all four cells. A paired contrast therefore compares organizational conditions while holding the simulated world’s random tape fixed: $$ \Delta_{a\rightarrow b}=\frac{1}{\lvert S\rvert}\sum_{s\in S}\left(Y_{b,s}-Y_{a,s}\right) $$ where $Y_{a,s}$ is the seed-level outcome for treatment $a$. The implementation is Python-first with a React/TypeScript control room. The core modules have deliberately narrow seams: a world engine for truth, an organization model for topology, an observation layer for local evidence, a policy adapter for structured decisions, a simulation runner for the event order, an immutable event ledger, a replay engine, a paired experiment runner, and an analysis layer. The web interface makes the central contrast legible quickly: WORLD TRUTH beside EXECUTIVE BELIEF, a distortion-by-level view, treatment controls, and an evidence inspector that traces a displayed event back to its causal parents. Data path Runtime modes Hosted decisions are treated as a reproducibility problem. In record mode, a language-model response is bound to the complete authorized context and written to a local decision cache. In locked mode, the system refuses an unseen context before making a network call. A finalized run can therefore be replayed without provider credentials. Fresh hosted output remains explicitly nondeterministic until it is recorded. GPT-5.6 and Codex Codex was the primary development environment. It carried the project from research framing and architecture through the Python simulation, statistical design, React interface, regression tests, browser workflows, documentation, and final integration. The important design decisions were made explicit in code and tests: deterministic truth must remain outside the policy, the model must not be prompted to lie, and causal comparisons must reuse paired seeds. During a bounded compatibility and hardening phase immediately after the GPT-5.6 rollout, early Codex client issues temporarily interrupted the workflow. I used GPT-5.6 through Claude Code and CLIProxyAPI for that portion of the debugging work, then returned to Codex for the final integration and validation pass. That history is intentional and transparent; Claude Code and CLIProxyAPI are not runtime dependencies of Groundline. Groundline also supports GPT-5.6 as the live employee-policy model through its OpenAI-compatible structured-output adapter. The adapter sends only the employee’s authorized context and never permits model output to become authoritative business state. The current live-provider regression harness measures the provider-neutral structured-decision and record/locked seams; a GPT-5.6 behavioral benchmark requires a separately labeled runtime configuration. Why this is useful Groundline gives builders and researchers a sharper question about agent organizations: under a controlled intervention, how much information changed before it reached the person making the decision? That distinction matters for multi-agent work systems, where a fluent summary can hide a broken chain of evidence. Groundline makes that chain inspectable and makes the mechanism testable. Scope and limitations This is a focused instrument for one simulated operational setting. Its causal estimates apply to that configured world; extrapolation to human organizations requires separate validation. Future work can add matrix reporting, committees, cross-functional decision rights, dynamic reorganization, and calibration against human organizational studies while preserving the same separation between computed truth and language.

## README (from the GitHub repository)

# Groundline

A causal evaluation environment for hierarchical language-agent organizations. A deterministic operational world computes business truth. Persistent employees receive scoped evidence, take permitted actions, and decide what to report upward. The repository and Python package retain the working name `groundline`.

The narrow research claim is not "LLMs can simulate a company." It is that hierarchical reporting can be treated as a causal, replayable measurement problem: keep world randomness fixed, intervene on organizational conditions, and estimate how information changes as it climbs.

## Built with GPT-5.6 and Codex

Groundline was developed primarily with [Codex](https://developers.openai.com/codex/), from the initial research and architecture through the deterministic Python engine, statistical design, React control room, regression tests, browser validation, and final integration. [GPT-5.6](https://developers.openai.com/api/docs/guides/latest-model?model=gpt-5.6) was also used directly as the coding and reasoning model during a bounded compatibility and hardening phase.

There is one important implementation-history detail. Immediately after the GPT-5.6 rollout, early issues in the Codex client temporarily interrupted this project by consuming excessive amount of credits. For part of that hardening work, GPT-5.6 was accessed through Claude Code using CLIProxyAPI. Development then returned to Codex, where the final fixes, aggregate verification, documentation, and submission pass were completed. Claude Code and CLIProxyAPI are not runtime dependencies of Groundline.

GPT-5.6 is also supported at runtime as the live employee policy. The adapter sends only an employee's authorized context to an OpenAI-compatible chat-completions endpoint and requires a structured JSON decision. It never sends hidden world truth and never allows model output to become authoritative state. Live decisions can be recorded once, replayed without credentials, and rerun in network-disabled locked mode.

The key engineering decisions made with Codex were:

- operational truth belongs exclusively to the deterministic world engine;
- agents are never instructed to lie, conceal, or exaggerate;
- causal comparisons pair every treatment on the same seeds;
- fresh hosted output becomes reproducible only after it is recorded in the decision ledger;
- organizations are validated rooted trees, not a hardcoded pyramid;
- fixture-policy results verify the instrument but are not presented as evidence about GPT-5.6 behavior.

## Run the complete offline experiment

Prerequisites: Python 3.13, `uv`, Node 24, and npm.

```powershell
uv sync
npm --prefix frontend install
npm --prefix frontend run build
uv run groundline experiment --config configs/demo.yaml --artifacts artifacts
uv run groundline serve --artifacts artifacts
```

Open `http://127.0.0.1:8000`. The backend serves the artifact API and compiled interface. The operator controls launch fresh 2×2 paired intervention matrices; SQLite-backed job progress and expired-lease recovery are exposed through the same API.

A normal wheel build runs the frontend build and packages the compiled same-origin application:

```powershell
uv build --wheel
uv pip install dist/groundline-*.whl
```

The demo experiment is a general rooted tree with 13 persistent employees: nine contributors, three department directors, and one executive across Product, Engineering, and QA. The topology engine also accepts deeper, unbalanced trees.

## Run one company trajectory

```powershell
uv run groundline run --config configs/demo.yaml --seed 7 --policy fixture
```

Use GPT-5.6, or another language model exposed through an OpenAI-compatible chat-completions endpoint, as the employee policy. Configure the bare model name, base URL, and API key in a `.env` file (see `.env.example`) or export them as environment variables — real environment variables take precedence over the file:

```powershell
# .env
GROUNDLINE_MODEL=gpt-5.6
GROUNDLINE_API_BASE=https://api.openai.com/v1
GROUNDLINE_API_KEY=your-key

# Record mode captures unseen decisions from the provider:
uv run groundline run --config configs/demo.yaml --seed 7 --policy record --artifacts artifacts

# Any OpenAI-compatible endpoint works the same way — set GROUNDLINE_API_BASE
# to its URL and GROUNDLINE_MODEL to a bare model name. Providers without a
# native OpenAI-compatible surface (e.g. AWS Bedrock) must be reached through
# an OpenAI-compatible proxy.
```

Live decisions request JSON-object structured output, receive no hidden world state, and are cached by the hash of the complete authorized context. `record` captures unseen decisions. `locked` permits only already-captured decisions and fails before making a network call on a cache miss. Every finalized run can be reconstructed exactly without a network call:

```powershell
# After a record run, replay works with zero provider credentials:
uv run groundline replay artifacts/<run-id>

# Or re-run with locked mode — no network call, fails on unseen context:
uv run groundline run --config configs/demo.yaml --seed 7 --policy locked --model gpt-5.6 --artifacts artifacts
```

## Artifact contract

Each run contains:

- `request.json`: complete seed, organization, scenario, and treatment
- `events.jsonl`: truth, observations, verifications, reports, decisions, metrics, and consequences
- `decisions.jsonl`: context hashes and structured policy outputs
- `metrics.json`: tick-level distortion by agent, department, and hierarchy depth
- `manifest.json`: schema, policy, and engine fingerprints plus request, event, decision, and metrics hashes and counts

The manifest is published last. Shared verification rejects malformed, incomplete, tampered, identity-mismatched, or replay-inconsistent artifacts before replay, analysis, resume, or API reads.

Each experiment adds an atomic execution state, paired run index, typed JSON analysis, CSV and Parquet seed-level outcomes, and a Markdown report. Inference includes exact or seeded Monte Carlo sign-flip tests, deterministic BCa intervals with explicit percentile fallback, Holm adjustment within declared confirmatory families, preregistered sensitivities, within-seed 2×2 factorial contrasts, design-resolution diagnostics, and a report-level mixed-effects model with seed and reporting-agent random intercepts. Fixture-policy results are engineering verification, not evidence about language-agent behavior.

## Verification

```powershell
uv run pytest -q
uv run ruff check .
uv run ruff format --check .
uv run mypy
npm --prefix frontend run test
npm --prefix frontend run build
npm --prefix frontend run test:e2e
```

## Architecture and evidence

| Module | Responsibility |
|---|---|
| [`world/engine.py`](src/groundline/world/engine.py) | Deterministic authoritative business state |
| [`organization/models.py`](src/groundline/organization/models.py) | Arbitrary reporting-tree validation and derived topology |
| [`observation/engine.py`](src/groundline/observation/engine.py) | Scoped local evidence and manager verification |
| [`openai_compat_policy.py`](src/groundline/policy/openai_compat_policy.py) | GPT-5.6/OpenAI-compatible structured decisions, retries, and record/locked behavior |
| [`simulation/runner.py`](src/groundline/simulation/runner.py) | Tick, report, action, consequence, and metric orchestration |
| [`events/store.py`](src/groundline/events/store.py) | Canonical event ledger and finalized artifact manifest |
| [`replay/engine.py`](src/groundline/replay/engine.py) | Zero-network reconstruction and equivalence checks |
| [`experiments/runner.py`](src/groundline/experiments/runner.py) | Paired intervention execution, resume, recovery, and exports |
| [`statistics/inference.py`](src/groundline/statistics/inference.py) | Seed-level causal inference and sensitivity analysis |
| [`api/app.py`](src/groundline/api/app.py) | Artifact queries, experiment jobs, and compiled control-room hosting |

## LICENSE 
A

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 91 recognized source files, 646 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (102 of 102)

```
.env.example
.gitignore
configs/demo.yaml
configs/e2e.yaml
frontend/e2e/app.spec.ts
frontend/e2e/builder.spec.ts
frontend/e2e/full-stack.spec.ts
frontend/index.html
frontend/package.json
frontend/playwright.config.ts
frontend/src/api.test.ts
frontend/src/api.ts
frontend/src/App.test.tsx
frontend/src/App.tsx
frontend/src/builder/builder.test.tsx
frontend/src/builder/ExperimentBuilder.tsx
frontend/src/charts.tsx
frontend/src/components.test.tsx
frontend/src/components.tsx
frontend/src/dashboard/DashboardView.tsx
frontend/src/main.tsx
frontend/src/material-elements.d.ts
frontend/src/md.ts
frontend/src/shell/NavigationRail.tsx
frontend/src/styles.css
frontend/src/test-setup.ts
frontend/src/theme.css
frontend/src/types.ts
frontend/src/views/ExperimentsView.tsx
frontend/src/views/RunView.tsx
frontend/tsconfig.app.json
frontend/tsconfig.json
frontend/tsconfig.node.json
frontend/vite.config.ts
frontend/vitest.config.ts
hatch_build.py
LICENSE
pyproject.toml
README.md
run.bat
run.sh
scripts/serve_e2e.py
src/groundline/__init__.py
src/groundline/api/__init__.py
src/groundline/api/app.py
src/groundline/api/jobs.py
src/groundline/api/models.py
src/groundline/cli.py
src/groundline/config.py
src/groundline/domain/__init__.py
src/groundline/domain/reports.py
src/groundline/events/__init__.py
src/groundline/events/artifacts.py
src/groundline/events/models.py
src/groundline/events/store.py
src/groundline/experiments/__init__.py
src/groundline/experiments/analysis.py
src/groundline/experiments/runner.py
src/groundline/metrics/__init__.py
src/groundline/metrics/engine.py
src/groundline/metrics/outcomes.py
src/groundline/observation/__init__.py
src/groundline/observation/engine.py
src/groundline/organization/__init__.py
src/groundline/organization/models.py
src/groundline/organization/topology.py
src/groundline/policy/__init__.py
src/groundline/policy/cache.py
src/groundline/policy/factory.py
src/groundline/policy/fixture.py
src/groundline/policy/models.py
src/groundline/policy/openai_compat_policy.py
src/groundline/policy/oracle.py
src/groundline/py.typed
src/groundline/replay/__init__.py
src/groundline/replay/engine.py
src/groundline/simulation/__init__.py
src/groundline/simulation/runner.py
src/groundline/statistics/__init__.py
src/groundline/statistics/inference.py
src/groundline/world/__init__.py
src/groundline/world/engine.py
src/groundline/world/models.py
tests/api/test_app.py
tests/events/__init__.py
tests/events/test_canonical.py
tests/experiments/test_analysis.py
tests/experiments/test_experiment.py
tests/metrics/test_epistemic.py
tests/metrics/test_outcomes.py
tests/observation/test_observations.py
tests/organization/test_topology.py
tests/policy/test_factory.py
tests/policy/test_openai_compat_policy.py
tests/simulation/test_run_and_replay.py
tests/statistics/test_inference.py
tests/test_cli.py
tests/test_config.py
tests/test_tui_env.py
tests/world/test_world_engine.py
tui.py
uv.lock
```

### Dependencies

- frontend/package.json: @material/web@^2.3.0, @phosphor-icons/react@^2.1.10, @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/react@^19.1.10, @types/react-dom@^19.1.7, @vitejs/plugin-react@^5.0.2, jsdom@^29.1.1, react@^19.1.1, react-dom@^19.1.1, typescript@^5.9.2, vite@^7.1.5, vitest@^4.1.10
- pyproject.toml: fastapi@>=0.115,<1, httpx@>=0.28,<1, numpy@>=2.2,<3, pandas@>=2.2,<4, pyarrow@>=19,<25, pydantic@>=2.10,<3, python-dotenv@>=1.0,<2, pyyaml@>=6,<7, rich@>=14,<15, scipy@>=1.15,<2, statsmodels@>=0.14,<1, typer@>=0.16,<1, uvicorn@>=0.34,<1

### Recent commits (newest first)

- Beautify ugly frontend
- Add more configs
- Major Rename
- Initial Commit

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

### pyproject.toml

```
[project]
name = "groundline"
dynamic = ["version"]
description = "Causal evaluation environment for hierarchical language-agent organizations"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
  "fastapi>=0.115,<1",
  "httpx>=0.28,<1",
  "numpy>=2.2,<3",
  "pandas>=2.2,<4",
  "pyarrow>=19,<25",
  "pydantic>=2.10,<3",
  "python-dotenv>=1.0,<2",
  "pyyaml>=6,<7",
  "scipy>=1.15,<2",
  "statsmodels>=0.14,<1",
  "rich>=14,<15",
  "typer>=0.16,<1",
  "uvicorn>=0.34,<1",
]

[project.scripts]
groundline = "groundline.cli:app"

[dependency-groups]
dev = [
  "hypothesis>=6.130,<7",
  "mypy>=1.15,<2",
  "pytest>=8.3,<10",
  "pytest-asyncio>=0.26,<2",
  "ruff>=0.11,<1",
  "pandas-stubs>=2.2,<4",
  "types-PyYAML>=6,<7",
]

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

[tool.hatch.version]
path = "src/groundline/__init__.py"

[tool.hatch.build.targets.wheel]
packages = ["src/groundline"]

[tool.hatch.build.hooks.custom]

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
markers = ["live: requires external OpenAI-compatible provider API access"]

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

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

[tool.ruff.lint.per-file-ignores]
"src/groundline/cli.py" = ["B008"]

[tool.mypy]
python_version = "3.13"
strict = true
packages = ["groundline"]

```

### frontend/package.json

```
{
  "name": "groundline-ui",
  "private": true,
  "version": "0.2.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:e2e": "npm run build && playwright test"
  },
  "dependencies": {
    "@material/web": "^2.3.0",
    "@phosphor-icons/react": "^2.1.10",
    "react": "^19.1.1",
    "react-dom": "^19.1.1"
  },
  "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/react": "^19.1.10",
    "@types/react-dom": "^19.1.7",
    "@vitejs/plugin-react": "^5.0.2",
    "jsdom": "^29.1.1",
    "typescript": "^5.9.2",
    "vite": "^7.1.5",
    "vitest": "^4.1.10"
  }
}

```

### frontend/src/main.tsx

```typescript
import React from "react";
import ReactDOM from "react-dom/client";
import "./md"; // registers @material/web (Material 3) custom elements
import "./styles.css";
import { App } from "./App";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

```

### src/groundline/cli.py

```python
import asyncio
import json
from pathlib import Path

import typer
import uvicorn

from groundline import __version__
from groundline.config import (
    load_env,
    load_experiment_request,
    load_run_request,
    load_yaml,
)
from groundline.events.store import FileEventStore
from groundline.experiments.runner import ExperimentRequest, ExperimentRunner
from groundline.policy.factory import build_policy
from groundline.policy.models import AgentPolicy
from groundline.replay.engine import ReplayEngine
from groundline.simulation.runner import RunRequest, SimulationRunner

app = typer.Typer(
    help=f"The Groundline {__version__}: computed truth, strategic reports.",
    no_args_is_help=True,
)


@app.command()
def validate(config: Path) -> None:
    """Validate a scenario or experiment configuration."""
    data = load_yaml(config)
    kind = "experiment" if "seeds" in data else "run"
    if kind == "experiment":
        ExperimentRequest.model_validate(data)
    else:
        RunRequest.model_validate(data)
    typer.echo(f"valid {kind}: {config}")


@app.command()
def run(
    config: Path = typer.Option(...),
    seed: int = typer.Option(...),
    policy: str = typer.Option("fixture"),
    artifacts: Path = typer.Option(Path("artifacts")),
    model: str | None = typer.Option(None),
) -> None:
    """Run one seeded company trajectory."""
    load_env()
    request = load_run_request(config, seed=seed)
    selected_policy = _policy(policy, model, artifacts)
    result = asyncio.run(
        SimulationRunner().run(request, selected_policy, FileEventStore(artifacts))
    )
    typer.echo(str(result.run_directory.resolve()))


@app.command()
def replay(run_directory: Path) -> None:
    """Replay a finalized Run without model calls."""
    result = asyncio.run(ReplayEngine().replay(run_directory))
    typer.echo(result.model_dump_json())
    if not result.equivalent:
        raise typer.Exit(1)


@app.command()
def experiment(
    config: Path = typer.Option(...),
    policy: str = typer.Option("fixture"),
    artifacts: Path = typer.Option(Path("artifacts")),
    model: str | None = typer.Option(None),
) -> None:
    """Execute a paired Experiment."""
    load_env()
    request = load_experiment_request(config)
    selected_policy = _policy(policy, model, artifacts)
    result = asyncio.run(
        ExperimentRunner().run(request, selected_policy, FileEventStore(artifacts))
    )
    typer.echo(str(result.analysis_path.resolve()))


@app.command()
def analyze(experiment_directory: Path) -> None:
    """Analyze a completed Experiment."""
    analysis = json.loads((experiment_directory / "analysis.json").read_text(encoding="utf-8"))
    typer.echo(json.dumps(analysis, indent=2, sort_keys=True))


@app.command()
def serve(
    artifacts: Path = typer.Option(Path("artifacts")),
    host: str = typer.Option("127.0.0.1"),
    port: int = typer.Option(8000),
) -> None:
    """Serve the read-only query application."""
    load_env()
    from groundline.api.app import create_app

    uvicorn.run(create_app(artifacts), host=host, port=port)


def _policy(name: str, model: str | None, artifacts: Path) -> AgentPolicy:
    try:
        return build_policy(name, model=model, artifacts=artifacts)
    except ValueError as exc:
        raise typer.BadParameter(str(exc)) from exc

```

### frontend/src/App.tsx

```typescript
import { useCallback, useEffect, useState } from "react";
import { List } from "@phosphor-icons/react";
import {
  checkBackend,
  launchExperiment,
  loadDecisions,
  loadEvidence,
  loadExperiment,
  loadFirstExperiment,
  loadRun,
  waitForJob,
} from "./api";
import { type InterventionValues } from "./components";
import { ExperimentBuilder } from "./builder/ExperimentBuilder";
import { DashboardView } from "./dashboard/DashboardView";
import { NavigationRail, type BackendStatus, type ThemeChoice, type View } from "./shell/NavigationRail";
import { ExperimentsView } from "./views/ExperimentsView";
import { RunView, type ExperimentStatus, type SelectedRunState } from "./views/RunView";
import type { Experiment, JobStatus } from "./types";

const isAbortError = (caught: unknown) => caught instanceof DOMException && caught.name === "AbortError";

function usePrefersDarkScheme(): boolean {
  const [dark, setDark] = useState(() =>
    typeof window !== "undefined" && window.matchMedia
      ? window.matchMedia("(prefers-color-scheme: dark)").matches
      : false,
  );
  useEffect(() => {
    if (typeof window === "undefined" || !window.matchMedia) return;
    const media = window.matchMedia("(prefers-color-scheme: dark)");
    const onChange = () => setDark(media.matches);
    media.addEventListener("change", onChange);
    return () => media.removeEventListener("change", onChange);
  }, []);
  return dark;
}

export function App() {
  const [view, setView] = useState<View>("dashboard");
  const [backendStatus, setBackendStatus] = useState<BackendStatus>("checking");
  const [theme, setTheme] = useState<ThemeChoice>(() => {
    const stored = localStorage.getItem("theme");
    return stored === "light" || stored === "dark" ? stored : "";
  });
  const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
  const [mobileOpen, setMobileOpen] = useState(false);
  const [experiment, setExperiment] = useState<Experiment | null>(null);
  const [experimentStatus, setExperimentStatus] = useState<ExperimentStatus>("loading");
  const [experimentError, setExperimentError] = useState("");
  const [experimentReload, setExperimentReload] = useState(0);
  const [selectedRun, setSelectedRun] = useState("");
  const [selectedRunState, setSelectedRunState] = useState<SelectedRunState>({ status: "idle" });
  const [runReload, setRunReload] = useState(0);
  const [job, setJob] = useState<JobStatus | null>(null);
  const [launchError, setLaunchError] = useState("");
  const [launchPolicy, setLaunchPolicy] = useState<"fixture" | "record" | "locked">("fixture");
  const prefersDark = usePrefersDarkScheme();

  useEffect(() => {
    if (theme === "") {
      delete document.documentElement.dataset.theme;
      localStorage.removeItem("theme");
    } else {
      document.documentElement.dataset.theme = theme;
      localStorage.setItem("theme", theme);
    }
  }, [theme]);

  const resolvedDark = theme === "" ? prefersDark : theme === "dark";
  const cycleTheme = useCallback(() => {
    setTheme((previous) => (previous === "light" ? "dark" : previous === "dark" ? "" : "light"));
  }, []);

  useEffect(() => {
    const controller = new AbortController();
    setBackendStatus("checking");
    checkBackend(controller.signal).then((ok) => {
      if (!controller.signal.aborted) setBackendStatus(ok ? "online" : "offline");
    });
    return () => controller.abort();
  }, []);

  useEffect(() => {
    const controller = new AbortController();
    setExperimentStatus("loading");
    setExperimentError("");
    loadFirstExperiment(controller.signal).then((data) => {
      if (controller.signal.aborted) return;
      if (data === null || !data.runs.length) {
        setExperiment(data);
        setSelectedRun("");
        setSelectedRunState({ status: "idle" });
        setExperimentStatus("empty");
        return;
      }
      setExperiment(data);
      setSelectedRun(data.runs[0].run_id);
      setExperimentStatus("loaded");
    }).catch((caught: unknown) => {
      if (controller.signal.aborted || isAbortError(caught)) return;
      setExperimentStatus("error");
      setExperimentError(caught instanceof Error ? caught.message : "experiment load failed");
    });
    return () => controller.abort();
  }, [experimentReload]);

  useEffect(() => {
    if (!selectedRun) {
      setSelectedRunState({ status: "idle" });
      return;
    }
    const controller = new AbortController();
    const runId = selectedRun;
    setSelectedRunState({ status: "loading", runId });
    Promise.all([
      loadRun(runId, controller.signal),
      loadEvidence(runId, {}, controller.signal),
      loadDecisions(runId, controller.signal),
    ]).then(([[detail, timeline], chain, decisions]) => {
      if (controller.signal.aborted) return;
      if (detail.manifest.run_id !== runId) {
        setSelectedRunState({ status: "error", runId, message: `run identity mismatch: expected ${runId}` });
        return;
      }
      setSelectedRunState({
        status: "loaded",
        runId,
        detail,
        timeline,
        evidence: chain.nodes,
        evidenceKey: "",
        evidenceStatus: "loaded",
        evidenceError: "",
        decisions: decisions.nodes,
        decisionsStatus: "loaded",
        decisionsError: "",
      });
    }).catch((caught: unknown) => {
      if (controller.signal.aborted || isAbortError(caught)) return;
      setSelectedRunState({ status: "error", runId, message: caught instanceof Error ? caught.message : "run load failed" });
    });
    return () => controller.abort();
  }, [runReload, selectedRun]);

  const chooseRun = useCallback((runId: string) => {
    setLaunchError("");
    setSelectedRun(runId);
  }, []);

  const handleNavigate = useCallback((nextView: View) => {
    const changeView = () => setView(nextView);
    const transition = (document as Document & { startViewTransition?: (callback: () => void) => void }).startViewTransition;
    if (transition && !window.matchMedia("(prefe
[truncated — 5695 more characters]
```

### src/groundline/api/app.py

```python
from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any, cast

from fastapi import FastAPI, HTTPException, Query, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pydantic import ValidationError

from groundline import __version__
from groundline.api.jobs import ExperimentJobManager, ExperimentLaunchConflict
from groundline.api.models import (
    ArtifactErrorResponse,
    DecisionResponse,
    EvidenceResponse,
    ExperimentDetailResponse,
    ExperimentSummaryResponse,
    HealthResponse,
    JobStatus,
    LaunchExperiment,
    MessageErrorResponse,
    RunDetailResponse,
    ValidationErrorResponse,
)
from groundline.config import load_env
from groundline.events.artifacts import ArtifactCorruptError, verify_run_artifacts
from groundline.events.models import Event, RunManifest
from groundline.experiments.runner import ExperimentRequest
from groundline.policy.models import PolicyDecision


class ArtifactRepository:
    def __init__(self, root: Path) -> None:
        self.root = root.resolve()

    @staticmethod
    def _json(path: Path) -> Any:
        try:
            return json.loads(path.read_text(encoding="utf-8"))
        except OSError as error:
            raise ArtifactCorruptError(
                "unreadable_artifact", path.name, f"cannot read artifact {path.name}"
            ) from error
        except json.JSONDecodeError as error:
            raise ArtifactCorruptError(
                "invalid_json", path.name, f"artifact {path.name} is not valid JSON"
            ) from error

    @staticmethod
    def _jsonl(path: Path) -> list[dict[str, Any]]:
        try:
            values = [
                json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line
            ]
        except OSError as error:
            raise ArtifactCorruptError(
                "unreadable_artifact", path.name, f"cannot read artifact {path.name}"
            ) from error
        except json.JSONDecodeError as error:
            raise ArtifactCorruptError(
                "invalid_json", path.name, f"artifact {path.name} is not valid JSON"
            ) from error
        if any(not isinstance(value, dict) for value in values):
            raise ArtifactCorruptError(
                "invalid_structure",
                path.name,
                f"artifact {path.name} has an invalid structure",
            )
        return cast(list[dict[str, Any]], values)

    @classmethod
    def _object(cls, path: Path) -> dict[str, Any]:
        value = cls._json(path)
        if not isinstance(value, dict):
            raise ArtifactCorruptError(
                "invalid_structure",
                path.name,
                f"artifact {path.name} has an invalid structure",
            )
        return value

    def run_directories(self) -> dict[str, Path]:
        found: dict[str, Path] = {}
        if not self.root.exists():
            return found
        for manifest_path in self.root.rglob("manifest.json"):
            if "quarantine" in manifest_path.parts:
                continue
            try:
                artifacts = verify_run_artifacts(manifest_path.parent)
            except ArtifactCorruptError:
                # A single corrupt or stale run must not poison the whole index.
                # Skip it so the remaining runs (and the run viewer) stay queryable;
                # the bad run surfaces as a 404 on its own lookup instead of a 500
                # on every lookup.
                continue
            found[artifacts.manifest.run_id] = artifacts.directory
        return found

    def list_runs(self) -> list[dict[str, Any]]:
        manifests = [
            verify_run_artifacts(path).manifest.model_dump(mode="json")
            for path in self.run_directories().values()
        ]
        return sorted(manifests, key=lambda manifest: manifest["run_id"])

    def run(self, run_id: str) -> Path:
        directory = self.run_directories().get(run_id)
        if directory is not None:
            return directory
        # Not in the verified index — it may be corrupt or stale. Resolve by
        # directory name so a direct, targeted lookup still verifies and surfaces
        # a typed 500 rather than degrading to a silent 404.
        for manifest_path in self.root.rglob("manifest.json"):
            if "quarantine" in manifest_path.parts:
                continue
            if manifest_path.parent.name == run_id:
                return manifest_path.parent
        raise KeyError(run_id)

    def experiments(self) -> list[dict[str, Any]]:
        experiments_root = self.root / "experiments"
        if not experiments_root.exists():
            return []
        return [
            {"name": directory.name, "analysis": self._object(directory / "analysis.json")}
            for directory in sorted(experiments_root.iterdir())
            if directory.is_dir() and (directory / "analysis.json").exists()
        ]

    def experiment(self, name: str) -> dict[str, Any]:
        experiments_root = (self.root / "experiments").resolve()
        directory = (experiments_root / name).resolve()
        if directory.parent != experiments_root:
            raise KeyError(name)
        if not directory.is_dir() or not (directory / "analysis.json").exists():
            raise KeyError(name)
        rows = self._jsonl(directory / "run-index.jsonl")
        completed = [row for row in rows if row.get("status", "completed") == "completed"]
        failures = [row for row in rows if row.get("status") == "failed"]
        request_path = directory / "experiment-request.json"
        request = self.experiment_request(name) if request_path.exists() else None
        return {
            "name": name,
            "analysis": self._object(directory / "analysis.json"),
            "request": request.model_dump(mode="json") if request is not None else None,
            "
[truncated — 12266 more characters]
```

### run.sh

```shell
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"
exec uv run python tui.py "$@"

```

### hatch_build.py

```python
from __future__ import annotations

import shutil
import subprocess
from pathlib import Path
from typing import Any

from hatchling.builders.hooks.plugin.interface import BuildHookInterface


class CustomBuildHook(BuildHookInterface):
    def initialize(self, version: str, build_data: dict[str, Any]) -> None:
        if self.target_name != "wheel" or build_data.get("editable_mode"):
            return
        root = Path(self.root)
        frontend = root / "frontend"
        npm = shutil.which("npm") or shutil.which("npm.cmd")
        if npm is None:
            raise RuntimeError("npm is required to build the packaged frontend")
        if not (frontend / "node_modules").is_dir():
            subprocess.run([npm, "ci"], cwd=frontend, check=True)
        subprocess.run([npm, "run", "build"], cwd=frontend, check=True)
        force_include = build_data.setdefault("force_include", {})
        force_include[str(frontend / "dist")] = "groundline/frontend"

```

### tui.py

```python
"""Groundline — lightweight TUI launcher.

Usage:
    python tui.py
    uv run python tui.py
"""

from __future__ import annotations

import concurrent.futures
import contextlib
import os
import signal
import subprocess
import sys
import time
from pathlib import Path

try:
    from rich.console import Console
    from rich.panel import Panel
    from rich.table import Table
    from rich.text import Text
except ImportError:
    print("rich is not installed. Run: uv sync")
    sys.exit(1)

ROOT = Path(__file__).resolve().parent
ENV_FILE = ROOT / ".env"
EXAMPLE_ENV = ROOT / ".env.example"

API_PORT = 8000
FRONTEND_PORT = 5173

console = Console()

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _version() -> str:
    init = ROOT / "src" / "groundline" / "__init__.py"
    for line in init.read_text().splitlines():
        if line.startswith("__version__"):
            return line.split("=")[1].strip().strip("\"'")
    return "?"


def _env_status() -> dict[str, str]:
    """Read .env and report key presence (never print values)."""
    keys = [
        "GROUNDLINE_MODEL",
        "GROUNDLINE_API_BASE",
        "GROUNDLINE_API_KEY",
        "GROUNDLINE_TIMEOUT_SECONDS",
        "GROUNDLINE_MAX_ATTEMPTS",
    ]
    values: dict[str, str] = {}
    if ENV_FILE.exists():
        for line in ENV_FILE.read_text().splitlines():
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            if "=" in line:
                k, v = line.split("=", 1)
                values[k.strip()] = v.strip()
    result: dict[str, str] = {}
    for k in keys:
        v = values.get(k, "")
        if not v:
            result[k] = "[dim]not set[/]"
        elif k == "GROUNDLINE_API_KEY":
            result[k] = "[green]set[/]"
        else:
            result[k] = f"[green]{v}[/]"
    return result


def _run(cmd: list[str], cwd: Path | None = None) -> int:
    """Run a command in a subprocess, forwarding stdio."""
    proc = subprocess.run(cmd, cwd=cwd or ROOT, shell=(sys.platform == "win32"))
    return proc.returncode


def _start_background(cmd: list[str], cwd: Path | None = None) -> subprocess.Popen:
    """Start a process in the background, returning the Popen object"""
    kwargs: dict = {
        "cwd": cwd or ROOT,
        "stdout": subprocess.DEVNULL,
        "stderr": subprocess.DEVNULL,
    }
    if sys.platform == "win32":
        kwargs["shell"] = True
    else:
        kwargs["preexec_fn"] = os.setsid  # new process group for tree kill
    return subprocess.Popen(cmd, **kwargs)


def _kill_tree(proc: subprocess.Popen) -> None:
    """Kill a process and all its children across platforms."""
    if proc.poll() is not None:
        return  # already exited
    if sys.platform == "win32":
        subprocess.run(
            ["taskkill", "/F", "/T", "/PID", str(proc.pid)],
            capture_output=True,
        )
    else:
        try:
            os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
        except (ProcessLookupError, PermissionError):
            with contextlib.suppress(ProcessLookupError):
                proc.terminate()


def _kill_port(port: int) -> None:
    """Kill any process listening on the given port, cross-platform."""
    import socket

    try:
        with socket.create_connection(("127.0.0.1", port), timeout=1):
            pass
    except OSError:
        return  # nothing on that port

    def _kill(pid: str) -> None:
        if sys.platform == "win32":
            subprocess.run(["taskkill", "/F", "/T", "/PID", pid], capture_output=True)
        else:
            subprocess.run(["kill", "-9", pid], capture_output=True)
        console.print(f"  [yellow]Killed process on port {port} (PID {pid})[/]")

    if sys.platform == "win32":
        out = subprocess.run(
            [
                "powershell",
                "-NoProfile",
                "-Command",
                "Get-NetTCPConnection -State Listen -LocalPort "
                f"{port} -ErrorAction SilentlyContinue | "
                "Select-Object -Expand OwningProcess -Unique",
            ],
            capture_output=True,
            text=True,
        )
    else:
        out = subprocess.run(
            ["lsof", "-ti", f":{port}"],
            capture_output=True,
            text=True,
        )
        # fuser fallback where lsof is absent (some minimal Linux images)
        if not out.stdout.strip():
            out = subprocess.run(
                ["fuser", f"{port}/tcp"],
                capture_output=True,
                text=True,
            )

    for pid in out.stdout.replace("\r", "").splitlines():
        pid = pid.strip()
        if pid:
            _kill(pid)



def _wait_for_port(host: str, port: int, timeout: float = 15.0) -> bool:
    """Poll until a port is accepting connections."""
    import socket

    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        try:
            with socket.create_connection((host, port), timeout=1):
                return True
        except OSError:
            time.sleep(0.3)
    return False


# ---------------------------------------------------------------------------
# Menu actions
# ---------------------------------------------------------------------------


def action_launch_web() -> None:
    """Start backend + frontend together, print URLs, wait for Ctrl+C."""
    console.print()
    console.print("[bold]Launching Groundline Engine…[/]\n")

    # Kill anything on our ports first
    _kill_port(API_PORT)
    _kill_port(FRONTEND_PORT)

    # Start backend
    backend = _start_background(
        [sys.executable, "-m", "uvicorn", "groundline.api.app:app", "--port", str(API_PORT)],
    )
    console.print(f"  [cyan]Backend[/] starting on port {API_PORT}…")

    # Start frontend
    fe = ROOT / "frontend"
    if not fe.exists():
        con
[truncated — 7545 more characters]
```

### frontend/vite.config.ts

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

export default defineConfig({
  plugins: [react()],
  server: {
    host: "127.0.0.1",
    proxy: { "/api": "http://127.0.0.1:8000" },
  },
});

```

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