# Project export: Mono

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: Mono gives autonomous coding and research agents persistent memory of every hypothesis, code change, experiment, result, and failure.
- Devpost: https://devpost.com/software/runtrace
- GitHub: https://github.com/vano04/Mono
- Video: https://www.youtube.com/embed/l_Zm_mI1wdE?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Ivan Belenko (53 commits)

## Devpost submission (written by the team)

### Inspiration

Always struggling with sharing my results.tsv file from traditional autoresearch, I thought to use this opportunity to create a central Mono(lith) for autoresearch with AI tooling integration. Note!!! Gallery screenshots and demo video use the old name! Please refer to the GitHub for installing and trying this project!

### What it does

Mono is a self-hosted experiment registry and persistent memory layer for autonomous agents. It keeps the full research loop together: project goals, versioned instructions, exclusions, and tags; experiment proposals with hypotheses, reasoning, implementation plans, and dependencies; atomic worker claims so parallel agents do not unknowingly duplicate work; live run metrics and events streamed into the dashboard; parameters, commands, Git branches and commits, artifacts, outcomes, and conclusions; baselines and best-so-far progress for the project's primary metric; keyword search with optional pgvector semantic retrieval; reusable, project-defined RTVis result visualizations; role-based browser access and scoped, revocable agent tokens. Humans get a coherent web workspace. Agents use the same records through the Python SDK, CLI, HTTP API, or MCP server. How it works Orient: an agent asks Mono for the project context and searches prior evidence. Plan: it proposes an experiment or atomically claims an existing proposal. Execute: one owner creates the run and streams metrics, events, parameters, Git metadata, and artifacts. Conclude: the run is marked kept, discarded, or crashed with a durable conclusion. Reuse: future agents retrieve that evidence, compare against the baseline, and choose the next hypothesis from what is already known. The central design constraint is that every execution attempt has exactly one authoritative run record, even when MCP, the SDK, and the CLI are all available.

### How we built it

Mono is a production-shaped, self-hosted system rather than a single demo page: FastAPI + SQLAlchemy provide the HTTP API and persistence layer. PostgreSQL 17 + pgvector store structured records and optional semantic embeddings. Next.js 16 + React 19 + TypeScript power the authenticated, responsive dashboard. Server-Sent Events update live metrics, events, and lifecycle status. A lightweight Python SDK and Typer CLI support instrumented code and tracked subprocesses. A stdio MCP server and Codex plugin expose 32 project, evidence, experiment, run, baseline, tag, and visualization tools. RTVis renders trusted, theme-aware components and isolates custom JavaScript without network or same-origin access. Docker Compose, Alembic migrations, health checks, demo seeding, and published packages make the project reproducible outside the development machine. How we used Codex and GPT-5.6 Codex with GPT-5.6 was the primary engineering collaborator across the build. It helped turn the initial “persistent memory for agents” idea into a complete product surface: shaping the data model and API contracts, implementing the FastAPI service and Next.js interface, building the SDK/CLI/MCP integration, debugging live-stream behavior, hardening authentication and project-scoped authorization, improving responsive charts, writing tests, and producing deployment and integration documentation. Codex was especially valuable for work that crossed boundaries. A change to run ownership, for example, had to remain consistent across HTTP routes, Python context managers, CLI subprocess tracking, MCP tool contracts, the dashboard, and the documentation. GPT-5.6 could reason across those layers, run the relevant checks, and revise the implementation as one coherent system. Mono also integrates back into Codex as a plugin. That created a useful feedback loop: Codex helped build the memory system, then used Mono's MCP tools to inspect context, track experiments, and generate reusable visualizations.

### Challenges we ran into

Preventing duplicate run ownership The hardest correctness problem was supporting MCP, SDK, and CLI workflows without allowing the same execution to create multiple competing run records. We introduced explicit attachment semantics and a one-owner contract so an SDK process can attach to an MCP-created run rather than silently creating another. Making live evidence reliable Metrics can arrive quickly, browsers reconnect, and runs can finish while a stream is open. SSE resume cursors, polling fallback, terminal refreshes, and buffered SDK writes keep the UI responsive without replaying or losing evidence. Sharing data safely with headless agents Browser users need normal sessions and role-based controls; agents need scoped credentials that can be revoked and expired. Mono hashes tokens at rest, fixes their project grants, and prevents bearer tokens from administering identities or minting more credentials. Keeping custom visualizations portable and safe Agents can author reusable visualizations, but arbitrary dashboard code cannot be trusted. RTVis prefers trusted layout, metric, table, badge, and chart nodes; custom JavaScript is isolated, network-disabled, and receives only the intended data and theme.

### Accomplishments we're proud of

A working end-to-end product spanning web, API, Python, CLI, MCP, and Docker deployment. Durable experiment evidence that captures decisions and failures, not only successful metrics. Cooperative proposal claiming for parallel workers. Live, reusable result displays and project-defined visualizations. Authentication and authorization designed for both humans and autonomous agents. A one-command seeded demo plus clear installation and testing documentation. Codex and Claude plugin bundles that can be installed without cloning the repository. Try it Clone the repository and run: For a local instance populated with demonstration records: Then open http://localhost:3000. The repository includes setup instructions, architecture documentation, feature coverage, integration examples, and verification commands. More concrete instructions can be found on the GitHub.

## README (from the GitHub repository)

# Mono

Mono is a self-hosted experiment registry and persistent memory layer for autonomous research agents. It keeps hypotheses, code metadata, live metrics, artifacts, outcomes, and conclusions together so future runs can build on prior evidence.

The repository contains the maintained FastAPI service, Next.js application, Python SDK/CLI, and MCP server. A new installation starts empty unless the development seed is explicitly enabled.

## Table of contents

- [Product tour](#product-tour)
- [What it provides](#what-it-provides)
- [Architecture](#architecture)
- [How Codex was used](#how-codex-was-used)
- [Quick start](#quick-start)
- [Native development](#native-development)
- [Install the CLI and Python package](#install-the-cli-and-python-package)
- [Codex and Claude Code plugins](#codex-and-claude-code-plugins)
- [Configuration](#configuration)
- [Verification](#verification)
- [Repository layout](#repository-layout)
- [License](#license)

## Product tour

![Mono project dashboard showing best-so-far progress and the shared experiment queue](docs/images/gallery/mono-dashboard-progress.png)

The dashboard keeps the research objective, best-so-far progress, baseline, worker activity, and cooperative experiment queue in one shared view.

![Mono live experiment detail showing streamed metrics in a reusable custom visualization](docs/images/gallery/mono-live-metrics.png)

Every run preserves the hypothesis, reasoning, code metadata, live metrics, outcome, and artifacts as durable evidence.

![Mono evidence search across prior experiments, configurations, outcomes, and conclusions](docs/images/gallery/mono-evidence-search.png)

Agents and humans can retrieve what has already been tried before spending another run on the same idea.

![Mono CLI showing authentication, search, context, tracked execution, and agent integration commands](docs/images/gallery/mono-cli.png)

The CLI gives agents the same memory and experiment-tracking workflow without requiring a browser session.

![Mono built-in documentation for the dashboard, SDK, CLI, HTTP API, and MCP server](docs/images/gallery/mono-built-in-docs.png)

The built-in documentation connects the dashboard to the SDK, CLI, HTTP API, and MCP workflows agents use to produce those records.

## What it provides

- project-scoped experiment proposals and atomic worker claims;
- live run metrics and events over Server-Sent Events;
- parameters, Git metadata, logs, and downloadable artifacts;
- versioned `program.md` instructions and research exclusions;
- completed-run baselines and best-so-far progress charts;
- archive, restore, soft-delete, and internal mutation audit records;
- keyword search, with optional pgvector semantic retrieval;
- browser password authentication for a self-hosted instance;
- revocable, expiring agent tokens for headless clients;
- read-only viewer, project editor, and project owner roles with matching API and web controls;
- project-scoped, MCP-generated RTVis widgets with ShadCN theming, sandboxed JavaScript, and portable JSON import and export;
- HTTP, Python, CLI, and MCP interfaces.

The [complete feature catalog](docs/features.md) lists every current-source web, HTTP, SDK, CLI, MCP, visualization, authentication, and operational capability.

## Architecture

| Component | Location | Technology |
| --- | --- | --- |
| API | `apps/api` | FastAPI, SQLAlchemy, Alembic |
| Web app | `apps/web` | Next.js 16, React 19, TypeScript, Tailwind CSS |
| Python client and CLI | `packages/python_sdk` | HTTPX, Typer |
| MCP server | `apps/mcp` | Python MCP SDK |
| Database | Compose service | PostgreSQL 17 with pgvector |

## How Codex was used

OpenAI Codex with GPT-5.6 was the primary AI engineering collaborator during Mono's development. The human developer supplied the product direction, constraints, acceptance criteria, and review; Codex helped turn those decisions into working code and repeatedly tested the result. This was an iterative engineering process rather than a one-shot code generation pass.

Codex worked across the full repository: it traced behavior through the FastAPI service, database models and migrations, Next.js interface, Python SDK and CLI, MCP server, Docker setup, release workflows, and documentation. It implemented and debugged cross-cutting features such as live metric streaming, run ownership and attachment, authentication and project authorization, custom visualizations, responsive layouts, localization, demo modes, and persistent user settings. Because a change in one interface often affects every other client, Codex was also used to keep HTTP, Python, CLI, MCP, UI, and documentation contracts aligned.

The most notable tools and integrations were:

| Codex capability | How it contributed |
| --- | --- |
| Coding and terminal tools | Inspected the repository, edited source and documentation, managed development services, and ran focused tests plus full Python, web, package, Compose, and release checks. |
| Browser | Exercised the live application with accessible-role interactions and DOM snapshots; changed viewport sizes; inspected console output and layout measurements; and captured before/after screenshots. This exposed issues that static review missed, including mobile record dialogs, fixed-width charts, horizontal overflow, and live-update behavior. |
| Computer Use | Controlled Chrome and macOS applications when page-level browser automation was not enough. It supported window-level visual QA, screen-capture experiments, and preparation of product-tour media. |
| Product Design plugin | Guided a responsive UX audit across desktop, mobile, and portrait-monitor layouts. Its recommendations were implemented and then verified in the browser, including full-screen mobile details, responsive charts, compact metadata, stable close controls, and removal of unintended page overflow. |
| GitHub plugin | Worked with `codex/` branches and pull requests, monitored GitHub Actions, and verified release assets. It was used for production hardening, the 0.1.4 release, and the 0.1.5 public-repository hygiene release across GitHub Releases, PyPI, and GHCR. |
| Mono plugin and MCP tools | Dogfooded Mono from Codex itself: retrieving project context, searching prior evidence, exercising experiment lifecycles, streaming metrics and events, and creating or validating project visualizations. That feedback loop helped refine both the product and its agent workflow. |

Codex also used separate focused tasks for agent-facing QA. Those tasks tested MCP memory retrieval, single-owner experiment lifecycles, CLI and SDK subprocess tracking, saved authentication, run attachment, failure closure, and visualization import/export against live instances. Synthetic fixtures were kept under `MonoDemo`, while reproducible regression coverage was added to the main test suites.

Generated changes were accepted only after evidence appropriate to their risk: unit and integration tests, lint and type checks, production builds, migration and Compose checks, browser interaction tests, visual inspection, or public artifact verification. Representative public milestones are [production hardening in PR #1](https://github.com/vano04/Mono/pull/1), [release 0.1.4 in PR #2](https://github.com/vano04/Mono/pull/2), and [the 0.1.5 repository-hygiene release in PR #3](https://github.com/vano04/Mono/pull/3).

## Quick start

Requirements: Docker with Compose support. From the repository root:

```bash
./scripts/install.sh
```

The install script builds the API and web images from the cloned source, starts PostgreSQL and the application services, and waits for their health checks. The equivalent Compose command is `docker compose up -d --build --wait`.

Open <http://localhost:3000>. On a fresh database, the first browser creates the instance owner and a password. Data is stored in named PostgreSQL and artifact volumes and survives `docker compose down`.

To update an existing checkout, fast-forward it to the latest 

[README truncated for size]

## Detected evidence (automated analysis)

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

## Codebase structure (from repository index)

### Files (120 of 207)

```
.agents/plugins/marketplace.json
.claude-plugin/marketplace.json
.dockerignore
.env.example
.github/workflows/ci.yml
.github/workflows/containers.yml
.github/workflows/publish-pypi.yml
.github/workflows/release.yml
.gitignore
alembic.ini
apps/api/migrations/env.py
apps/api/migrations/script.py.mako
apps/api/migrations/versions/0001_initial.py
apps/api/migrations/versions/0002_project_progress_metric.py
apps/api/migrations/versions/0003_vector_search.py
apps/api/migrations/versions/0004_tag_registry.py
apps/api/migrations/versions/0005_instance_auth.py
apps/api/migrations/versions/0006_api_tokens.py
apps/api/migrations/versions/0007_password_auth.py
apps/api/migrations/versions/0008_identity_usernames.py
apps/api/migrations/versions/0009_project_access.py
apps/api/migrations/versions/0010_visualizations.py
apps/api/migrations/versions/0011_result_visualization_types.py
apps/api/migrations/versions/0012_identity_onboarding.py
apps/api/migrations/versions/0013_identity_locale.py
apps/api/migrations/versions/0014_search_embedding_hnsw.py
apps/api/migrations/versions/0015_identity_appearance.py
apps/api/mono_api/__init__.py
apps/api/mono_api/auth.py
apps/api/mono_api/config.py
apps/api/mono_api/database.py
apps/api/mono_api/embeddings.py
apps/api/mono_api/main.py
apps/api/mono_api/models.py
apps/api/mono_api/rtvis.py
apps/api/mono_api/schemas.py
apps/api/mono_api/seed.py
apps/mcp/mono_mcp/__init__.py
apps/mcp/mono_mcp/server.py
apps/web/.dockerignore
apps/web/.gitignore
apps/web/AGENTS.md
apps/web/CLAUDE.md
apps/web/components.json
apps/web/Dockerfile
apps/web/eslint.config.mjs
apps/web/next.config.ts
apps/web/package.json
apps/web/postcss.config.mjs
apps/web/public/.gitkeep
apps/web/README.md
apps/web/src/app/access/page.tsx
apps/web/src/app/account/page.tsx
apps/web/src/app/api/v1/runs/[identifier]/stream/route.ts
apps/web/src/app/docs/page.tsx
apps/web/src/app/globals.css
apps/web/src/app/layout.tsx
apps/web/src/app/page.tsx
apps/web/src/app/projects/[slug]/archive/page.tsx
apps/web/src/app/projects/[slug]/page.tsx
apps/web/src/app/projects/[slug]/search/page.tsx
apps/web/src/app/projects/[slug]/settings/page.tsx
apps/web/src/components/access-admin.tsx
apps/web/src/components/account-menu.tsx
apps/web/src/components/account-settings.tsx
apps/web/src/components/app-settings-link.tsx
apps/web/src/components/appearance-provider.tsx
apps/web/src/components/appearance-settings.tsx
apps/web/src/components/artifact-files.tsx
apps/web/src/components/auth-provider.tsx
apps/web/src/components/create-experiment-dialog.tsx
apps/web/src/components/create-project-dialog.tsx
apps/web/src/components/edit-experiment-dialog.tsx
apps/web/src/components/i18n-provider.tsx
apps/web/src/components/mono-logo.tsx
apps/web/src/components/onboarding-tour.tsx
apps/web/src/components/progress-chart.tsx
apps/web/src/components/project-access-card.tsx
apps/web/src/components/project-shell.tsx
apps/web/src/components/project-visualizations.tsx
apps/web/src/components/project-workspace.tsx
apps/web/src/components/projects-screen.tsx
apps/web/src/components/record-actions.tsx
apps/web/src/components/record-detail-dialog.tsx
apps/web/src/components/run-curve-chart.tsx
apps/web/src/components/status-badge.tsx
apps/web/src/components/tag-filter.tsx
apps/web/src/components/ui/alert-dialog.tsx
apps/web/src/components/ui/badge.tsx
apps/web/src/components/ui/button.tsx
apps/web/src/components/ui/card.tsx
apps/web/src/components/ui/dialog.tsx
apps/web/src/components/ui/dropdown-menu.tsx
apps/web/src/components/ui/empty.tsx
apps/web/src/components/ui/field.tsx
apps/web/src/components/ui/input-group.tsx
apps/web/src/components/ui/input.tsx
apps/web/src/components/ui/label.tsx
apps/web/src/components/ui/scroll-area.tsx
apps/web/src/components/ui/select.tsx
apps/web/src/components/ui/separator.tsx
apps/web/src/components/ui/sheet.tsx
apps/web/src/components/ui/skeleton.tsx
apps/web/src/components/ui/sonner.tsx
apps/web/src/components/ui/switch.tsx
apps/web/src/components/ui/table.tsx
apps/web/src/components/ui/textarea.tsx
apps/web/src/components/ui/toggle-group.tsx
apps/web/src/components/ui/toggle.tsx
apps/web/src/components/ui/tooltip.tsx
apps/web/src/components/visualization-renderer.tsx
apps/web/src/i18n/config.ts
apps/web/src/i18n/messages/de.ts
apps/web/src/i18n/messages/en.ts
apps/web/src/i18n/messages/es.ts
apps/web/src/i18n/messages/fr.ts
apps/web/src/i18n/messages/hi.ts
apps/web/src/i18n/messages/ja.ts
apps/web/src/i18n/messages/ko.ts
apps/web/src/i18n/messages/pt-BR.ts
[87 more files omitted for size]
```

### Dependencies

- apps/web/package.json: @base-ui/react@^1.6.0, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^9, eslint-config-next@16.2.10, lucide-react@^1.24.0, next@16.2.10, next-themes@^0.4.6, react@19.2.4, react-dom@19.2.4, shadcn@^4.13.0, sonner@^2.0.7, tailwind-merge@^3.6.0, tailwindcss@^4, tw-animate-css@^1.4.0, typescript@^5
- pyproject.toml: alembic@>=1.16,<2, fastapi@>=0.116,<1, fastembed@>=0.7,<1, httpx@>=0.28,<1, mcp@>=1.12,<2, pgvector@>=0.4,<1, psutil@>=7,<8, psycopg[binary]@>=3.2,<4, pydantic-settings@>=2.10,<3, pytest@>=8.4,<9, pytest-cov@>=6.2,<7, python-multipart@>=0.0.20,<1, sqlalchemy@>=2.0,<3, typer@>=0.16,<1, uvicorn[standard]@>=0.35,<1

### Recent commits (newest first)

- Use mono-research as the PyPI package name (#5)
- Rebrand RunTrace as Mono and prepare 0.1.6 (#4)
- Document Codex usage and development workflow
- Persist development mode identity for auth settings and tokens
- Add read-only demo mode
- Persist user settings across browsers
- Require explicit token submit intent
- Prevent duplicate API token creation
- Improve README product tour and development account menu
- Merge pull request #3 from vano04/codex/public-repository-hygiene
- Remove internal QA artifacts and release 0.1.5
- Merge pull request #2 from vano04/codex/release-0.1.4
- Prepare release 0.1.4
- Merge pull request #1 from vano04/codex/production-feature-hardening
- Harden RunTrace and document verified features
- Generate a dynamic favicon from the selected accent color
- Move appearance settings into account settings
- Merge branch 'codex/translate-all-locale-catalogs'
- Add localized identity preferences and translate docs
- Add localized documentation and identity preferences

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

### program.md

```markdown
# autoresearch

This is an experiment to have the LLM do its own research on
Manifold-Based-Muon.

The project trains a small GPT on cached FineWeb10B shards and compares
optimizer variants, especially Dense and Muon. The main research goal is to
improve the final validation loss without making the optimizer unnecessarily
complex.

Autoresearch commands run from the repository root. Configs under
`Data/configs` are execution inputs and local files under `Data/logs` are only
temporary spool files while a process is running. Mono is the durable
source of truth for experiment proposals, claims, resolved configuration,
parameters, live metrics and events, complete logs, generated artifacts,
outcomes, and conclusions. Do not treat the local log directory as the
experiment archive.

## Mono identity and connection

The Mono project must already exist. Use the project slug supplied by the
human or present in the active Mono context; never invent a slug. Use a
stable worker ID derived from the agreed run tag, for example
`autoresearch/may28-dense`.

Before doing research:

1. Call `get_project_context` for the project. This retrieves the current
   `program.md`, exclusions, baseline, metric definitions, claimable proposals,
   active work, and recent evidence.
2. Call `search_experiments` for the optimizer or mechanism being considered.
   Search again whenever a new idea may overlap prior work.
3. Treat Mono exclusions as hard constraints in addition to the constraints
   in this file.
4. If Mono is unavailable, report the connection failure clearly. Do not
   start an untracked experiment or claim that anything was recorded.
5. Never write API tokens, credentials, private data, or secrets into a
   hypothesis, configuration, event, artifact, result, or conclusion.

## Mono interfaces and ownership

Use the Mono MCP tools for project context, evidence search, shared queue
proposals and claims, and lightweight metric/event writes. Use the Mono
Python SDK or HTTP API for the complete run lifecycle when configuration,
parameters, log files, or other artifacts must be stored. The interface is an
implementation detail: all records belong to the same Mono project and run.

Create exactly one Mono run for each execution attempt. Do not create one
run through MCP and a second run through the SDK for the same process. Prefer a
Python SDK `Run` as the execution tracker because it captures Git/environment
metadata and supports:

- `log_config` for queryable parameters plus a versioned JSON config artifact;
- `log_params` for resolved or derived parameters not present in the config;
- `log_metric`/`log_metrics` for primary and diagnostic time series;
- `log_event` for checkpoints, decisions, warnings, cancellations, and errors;
- `log_text` for previewable stdout/stderr and reports;
- `log_artifact` for arbitrary generated files;
- `finish` for completed runs and `abort` for actual process crashes.

If an execution was created through `create_run`, attach all later
[truncated — 16334 more characters]
```

### docs/plugin-terms.md

```markdown
# Plugin terms of use

The Mono plugin is a client for a Mono instance selected and operated by the user. You are responsible for having permission to access that instance, protecting its bearer token, reviewing tool calls, and complying with the policies that apply to the data you record.

Do not use the plugin to access systems or data without authorization, disclose credentials, violate applicable law, or interfere with other users' experiments. Tool calls can create and modify shared records; inspect the target project and retrieved evidence before making writes.

The software is provided without a hosted service, service-level commitment, or guarantee of availability. Rights to copy, modify, or redistribute the source are governed by the GNU Affero General Public License v3.0 only (`AGPL-3.0-only`) included in the repository. These terms do not grant additional intellectual-property rights.

```

### pyproject.toml

```
[project]
name = "mono-research"
version = "0.1.6"
description = "Persistent experiment memory for autonomous research agents"
readme = "README.md"
license = "AGPL-3.0-only"
requires-python = ">=3.11"
keywords = ["agents", "experiments", "mcp", "research", "tracking"]
classifiers = [
  "Development Status :: 3 - Alpha",
  "Environment :: Console",
  "Framework :: FastAPI",
  "License :: OSI Approved :: GNU Affero General Public License v3",
  "Programming Language :: Python :: 3",
  "Programming Language :: Python :: 3.11",
  "Programming Language :: Python :: 3.12",
  "Programming Language :: Python :: 3.13",
  "Topic :: Scientific/Engineering",
]
dependencies = [
  "httpx>=0.28,<1",
  "psutil>=7,<8",
  "typer>=0.16,<1",
]

[project.optional-dependencies]
mcp = ["mcp>=1.12,<2"]
server = [
  "alembic>=1.16,<2",
  "fastapi>=0.116,<1",
  "pgvector>=0.4,<1",
  "psycopg[binary]>=3.2,<4",
  "pydantic-settings>=2.10,<3",
  "python-multipart>=0.0.20,<1",
  "sqlalchemy>=2.0,<3",
  "uvicorn[standard]>=0.35,<1",
]
embeddings = ["fastembed>=0.7,<1"]
dev = [
  "pytest>=8.4,<9",
  "pytest-cov>=6.2,<7",
]

[project.scripts]
mono = "mono.cli:app"
mono-mcp = "mono_mcp.server:main"

[project.urls]
Homepage = "https://github.com/vano04/Mono"
Documentation = "https://github.com/vano04/Mono#readme"
Issues = "https://github.com/vano04/Mono/issues"
Repository = "https://github.com/vano04/Mono.git"

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

[tool.hatch.build.targets.wheel]
packages = [
  "apps/api/mono_api",
  "apps/mcp/mono_mcp",
  "packages/python_sdk/mono",
]

[tool.hatch.build.targets.sdist]
exclude = [
  "/.agents",
  "/.claude-plugin",
  "/.github",
  "/.codex",
  "/MonoDemo",
  "/artifacts",
  "/design-qa.md",
  "/docs/verification-report.md",
]

[tool.pytest.ini_options]
pythonpath = ["apps/api", "apps/mcp", "packages/python_sdk"]
testpaths = ["tests"]
addopts = "-q"

```

### docker-compose.yml

```yaml
services:
  postgres:
    image: pgvector/pgvector:pg17
    restart: unless-stopped
    environment:
      POSTGRES_DB: mono
      POSTGRES_USER: mono
      POSTGRES_PASSWORD: mono
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U mono"]
      interval: 3s
      timeout: 3s
      retries: 20
    volumes:
      - mono-postgres:/var/lib/postgresql/data

  api:
    build:
      context: .
      dockerfile: Dockerfile.api
    environment:
      MONO_DATABASE_URL: postgresql+psycopg://mono:mono@postgres:5432/mono
      MONO_ARTIFACT_PATH: /data/artifacts
      MONO_CORS_ORIGINS: "${MONO_CORS_ORIGINS:-http://localhost:3000}"
      MONO_SEED_DEMO: "${MONO_DEV:-false}"
      MONO_DEV: "${MONO_DEV:-false}"
      MONO_DEMO: "${MONO_DEMO:-false}"
      MONO_AUTO_MIGRATE: "false"
      MONO_EMBEDDINGS_ENABLED: "${MONO_EMBEDDINGS_ENABLED:-false}"
      MONO_EMBEDDING_CACHE_PATH: /data/models
      MONO_SECURE_SESSION_COOKIE: "${MONO_SECURE_SESSION_COOKIE:-false}"
      MONO_OWNER_RECOVERY_PASSWORD: "${MONO_OWNER_RECOVERY_PASSWORD:-}"
      MONO_SESSION_TTL_HOURS: "${MONO_SESSION_TTL_HOURS:-168}"
      MONO_SETUP_LINK_TTL_HOURS: "${MONO_SETUP_LINK_TTL_HOURS:-24}"
      MONO_CLAIM_TIMEOUT_SECONDS: "${MONO_CLAIM_TIMEOUT_SECONDS:-300}"
      MONO_MAX_ARTIFACT_SIZE: "${MONO_MAX_ARTIFACT_SIZE:-10485760}"
    depends_on:
      postgres:
        condition: service_healthy
    ports:
      - "8000:8000"
    volumes:
      - mono-artifacts:/data/artifacts
      - mono-models:/data/models
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=2)"]
      interval: 10s
      timeout: 3s
      retries: 10
      start_period: 10s
    restart: unless-stopped
    init: true

  mcp:
    build:
      context: .
      dockerfile: Dockerfile.api
    command: ["mono-mcp"]
    environment:
      MONO_BASE_URL: http://api:8000
      MONO_API_TOKEN: "${MONO_API_TOKEN:-}"
    depends_on:
      api:
        condition: service_healthy
    profiles: ["mcp"]

  web:
    build:
      context: apps/web
      args:
        INTERNAL_API_URL: http://api:8000
    environment:
      INTERNAL_API_URL: http://api:8000
    depends_on:
      api:
        condition: service_healthy
    ports:
      - "3000:3000"
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:3000/"]
      interval: 10s
      timeout: 3s
      retries: 10
      start_period: 10s
    restart: unless-stopped
    init: true

volumes:
  mono-postgres:
  mono-artifacts:
  mono-models:

```

### apps/web/Dockerfile

```
FROM node:22-alpine AS dependencies
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:22-alpine AS builder
WORKDIR /app
ARG INTERNAL_API_URL=http://api:8000
ENV INTERNAL_API_URL=$INTERNAL_API_URL
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:22-alpine AS runner
WORKDIR /app
LABEL org.opencontainers.image.source="https://github.com/vano04/Mono"
LABEL org.opencontainers.image.description="Mono web dashboard"
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
RUN mkdir -p .next/cache && chown -R node:node .next/cache
EXPOSE 3000
USER node
CMD ["node", "server.js"]

```

### apps/web/package.json

```
{
  "name": "web",
  "version": "0.1.6",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint",
    "test": "node --no-warnings --test tests/*.test.mjs",
    "typecheck": "tsc --noEmit --incremental false"
  },
  "dependencies": {
    "@base-ui/react": "^1.6.0",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "lucide-react": "^1.24.0",
    "next": "16.2.10",
    "next-themes": "^0.4.6",
    "react": "19.2.4",
    "react-dom": "19.2.4",
    "shadcn": "^4.13.0",
    "sonner": "^2.0.7",
    "tailwind-merge": "^3.6.0",
    "tw-animate-css": "^1.4.0"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.2.10",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### packages/python_sdk/mono/cli.py

```python
from __future__ import annotations

import json
import re
import shutil
import subprocess
import sys
from importlib.metadata import version as package_version
from typing import Annotated

import typer

from .client import Mono
from .credentials import resolve_connection, save_credentials


app = typer.Typer(no_args_is_help=True, help="Track experiments and query Mono memory.")
integrations_app = typer.Typer(no_args_is_help=True, help="Install Mono in supported agent CLIs.")
app.add_typer(integrations_app, name="integrations")


def version_callback(value: bool) -> None:
    if value:
        typer.echo(f"mono {package_version('mono-research')}")
        raise typer.Exit()


@app.callback()
def main(
    version: Annotated[
        bool,
        typer.Option(
            "--version",
            "-V",
            callback=version_callback,
            is_eager=True,
            help="Show the Mono version and exit.",
        ),
    ] = False,
) -> None:
    """Track experiments and query Mono memory."""


def client(base_url: str | None, api_token: str | None) -> Mono:
    resolved_base_url, resolved_api_token = resolve_connection(base_url, api_token)
    return Mono(base_url=resolved_base_url, api_token=resolved_api_token, strict=True)


@app.command()
def auth(
    api_key: Annotated[str, typer.Argument(help="Agent API key, or '-' to read it from stdin.")],
    base_url: str | None = typer.Option(None, envvar="MONO_BASE_URL", help="Mono API URL."),
) -> None:
    """Authenticate the CLI and MCP tool with an agent API key."""
    if api_key == "-":
        api_key = sys.stdin.readline().strip()
    if not api_key:
        raise typer.BadParameter("API key cannot be empty")

    resolved_base_url, _ = resolve_connection(base_url, api_key)
    status = Mono(
        base_url=resolved_base_url,
        api_token=api_key,
        strict=True,
    ).request("GET", "/api/v1/auth/status")
    if not status.get("authenticated"):
        raise typer.BadParameter("Mono rejected this API key")

    path = save_credentials(resolved_base_url, api_key)
    typer.echo(f"Authenticated with {resolved_base_url}. Credentials saved to {path}; installed MCP plugins will use them automatically.")


@app.command()
def search(
    project: str,
    query: str,
    base_url: str | None = typer.Option(None, envvar="MONO_BASE_URL"),
    api_token: str | None = typer.Option(None, envvar="MONO_API_TOKEN", hidden=True),
    limit: int = 10,
) -> None:
    result = client(base_url, api_token).search(project, query, limit)
    typer.echo(json.dumps(result, indent=2, default=str))


@app.command("context")
def context_command(
    project: str,
    base_url: str | None = typer.Option(None, envvar="MONO_BASE_URL"),
    api_token: str | None = typer.Option(None, envvar="MONO_API_TOKEN", hidden=True),
) -> None:
    result = client(base_url, api_token).request("GET", f"/api/v1/projects/{project}/context")
    typer.echo(json.dumps(result, indent=2, default=str))


@app.command(
    context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
    help="Run a command and track structured MONO_METRIC/MONO_EVENT output.",
)
def exec(
    ctx: typer.Context,
    project: str = typer.Option(...),
    name: str = typer.Option(...),
    hypothesis: str = typer.Option(...),
    reasoning: str = typer.Option(""),
    base_url: str | None = typer.Option(None, envvar="MONO_BASE_URL"),
    api_token: str | None = typer.Option(None, envvar="MONO_API_TOKEN", hidden=True),
) -> None:
    command = list(ctx.args)
    if command and command[0] == "--":
        command = command[1:]
    if not command:
        raise typer.BadParameter("Provide a command after --")
    rt = client(base_url, api_token)
    with rt.run(project, name, hypothesis, reasoning, command=" ".join(command)) as tracked:
        process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
        assert process.stdout
        metric_pattern = re.compile(r"^MONO_METRIC\s+(\S+)=([+-]?[\d.]+)(?:\s+step=(\d+))?")
        event_pattern = re.compile(r'^MONO_EVENT(?:\s+level=(\w+))?\s+message=["\']?(.*?)["\']?$')
        for line in process.stdout:
            sys.stdout.write(line)
            if match := metric_pattern.match(line.strip()):
                tracked.log_metric(match.group(1), float(match.group(2)), int(match.group(3)) if match.group(3) else None)
            elif match := event_pattern.match(line.strip()):
                tracked.log_event(match.group(2), match.group(1) or "info")
        code = process.wait()
        if code == 0:
            tracked.finish("undecided", result_summary="Command completed successfully")
        else:
            tracked.abort(f"Command exited {code}")
            raise typer.Exit(code)


@integrations_app.command("install")
def install_integration(
    host: Annotated[str, typer.Argument(help="Agent CLI to configure: codex or claude")],
    ref: str = typer.Option("master", help="Mono Git ref to install."),
    dry_run: bool = typer.Option(False, help="Print commands without changing host configuration."),
) -> None:
    """Install the Mono plugin from its public repository marketplace."""
    host = host.lower()
    if host not in {"codex", "claude"}:
        raise typer.BadParameter("Host must be 'codex' or 'claude'")
    if not shutil.which(host):
        raise typer.BadParameter(f"{host} is not installed or is not on PATH")
    if host == "codex":
        commands = [
            ["codex", "plugin", "marketplace", "add", "vano04/Mono", "--ref", ref],
            ["codex", "plugin", "add", "mono@mono"],
        ]
    else:
        commands = [
            ["claude", "plugin", "marketplace", "add", "vano04/Mono"],
            ["claude", "plugin", "install", "mono@mono", "--scope", "user"],
        ]
    for command in commands:
        typer.echo("$ " + " ".join(command))
        if not dry_run:
            subprocess.run(command, check=True)
    typer.echo("Mono p
[truncated — 102 more characters]
```

### MonoDemo/agent-tests/sdk-cli-integration/server.py

```python
from __future__ import annotations

import json
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from threading import Lock
from urllib.parse import urlsplit


TOKEN = "synthetic-qa-token"
PROJECT_ID = "project-synthetic-qa"

state = {
    "runs": {},
    "next_run": 1,
    "context_requests": 0,
    "search_requests": 0,
    "auth_requests": 0,
}
lock = Lock()


def json_bytes(payload: object) -> bytes:
    return json.dumps(payload, sort_keys=True).encode("utf-8")


class Handler(BaseHTTPRequestHandler):
    # Keep the single-threaded fixture from waiting on an idle keep-alive
    # connection between sequential CLI/SDK requests.
    protocol_version = "HTTP/1.0"

    def log_message(self, _format: str, *_args: object) -> None:
        return

    def send_json(self, status: int, payload: object) -> None:
        body = json_bytes(payload)
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def read_json(self) -> dict:
        length = int(self.headers.get("Content-Length", "0"))
        if not length:
            return {}
        return json.loads(self.rfile.read(length))

    def authorized(self) -> bool:
        return self.headers.get("Authorization") == f"Bearer {TOKEN}"

    def do_GET(self) -> None:
        path = urlsplit(self.path).path
        if path == "/api/v1/auth/status":
            with lock:
                state["auth_requests"] += 1
            self.send_json(200, {"authenticated": self.authorized()})
            return
        if path == "/__state":
            with lock:
                runs = list(state["runs"].values())
                payload = {
                    "auth_requests": state["auth_requests"],
                    "context_requests": state["context_requests"],
                    "search_requests": state["search_requests"],
                    "runs": runs,
                }
            self.send_json(200, payload)
            return
        if not self.authorized():
            self.send_json(401, {"detail": "unauthorized"})
            return
        if path == "/api/v1/projects/qa-project/context":
            with lock:
                state["context_requests"] += 1
            self.send_json(
                200,
                {
                    "project": {"id": PROJECT_ID, "slug": "qa-project", "name": "Synthetic QA"},
                    "program": "Exercise CLI and SDK lifecycle paths",
                    "exclusions": ["No production data"],
                    "metric": {"name": "synthetic_loss", "direction": "minimize"},
                    "baseline": None,
                    "claimable_experiments": [],
                },
            )
            return
        if path == "/api/v1/projects/qa-project":
            self.send_json(200, {"id": PROJECT_ID, "slug": "qa-project", "name": "Synthetic QA"})
            return
        if path.startswith("/api/v1/runs/"):
            run_id = path.rsplit("/", 1)[-1]
            with lock:
                run = state["runs"].get(run_id)
            if run is None:
                self.send_json(404, {"detail": "run not found"})
            else:
                self.send_json(200, run)
            return
        self.send_json(404, {"detail": "not found"})

    def do_POST(self) -> None:
        path = urlsplit(self.path).path
        if path == "/__shutdown":
            self.send_json(200, {"ok": True})
            self.server.shutdown()
            return
        if not self.authorized():
            self.send_json(401, {"detail": "unauthorized"})
            return
        payload = self.read_json()
        if path == "/api/v1/search":
            with lock:
                state["search_requests"] += 1
            self.send_json(
                200,
                {
                    "project": payload.get("project"),
                    "query": payload.get("query"),
                    "count": 1,
                    "results": [{"id": "synthetic-evidence-1", "display_id": "EXP-QA-001", "name": "CLI/SDK smoke evidence"}],
                },
            )
            return
        if path == "/api/v1/projects/qa-project/runs":
            with lock:
                number = state["next_run"]
                state["next_run"] += 1
                run_id = f"run_synthetic_{number:03d}"
                run = {
                    "id": run_id,
                    "display_id": f"RUN-QA-{number:03d}",
                    "project_id": PROJECT_ID,
                    "project": "qa-project",
                    "name": payload.get("name", ""),
                    "hypothesis": payload.get("hypothesis", ""),
                    "lifecycle": "running",
                    "disposition": None,
                    "result_summary": "",
                    "conclusion": "",
                    "error_summary": "",
                    "metrics": [],
                    "events": [],
                }
                state["runs"][run_id] = run
            self.send_json(201, run)
            return
        parts = path.split("/")
        if len(parts) == 6 and parts[:4] == ["", "api", "v1", "runs"]:
            run_id, action = parts[4], parts[5]
            with lock:
                run = state["runs"].get(run_id)
                if run is None:
                    self.send_json(404, {"detail": "run not found"})
                    return
                if action == "metrics":
                    run["metrics"].extend(payload.get("metrics", []))
                elif action == "events":
                    run["events"].append(payload)
                elif action == "finish":
                    run["lifecycle"] = "completed"
                    run["disposition"] = payload.get("disposition")
                    run["result_summary"] = payload.get("result_summary", "")
                    run["conclusi
[truncated — 782 more characters]
```

### apps/mcp/mono_mcp/server.py

```python
from __future__ import annotations

from typing import Any
from urllib.parse import quote

import httpx
from mcp.server.fastmcp import FastMCP
from mono.credentials import resolve_connection


mcp = FastMCP("Mono")


def request(method: str, path: str, payload: dict[str, Any] | None = None) -> Any:
    base_url, api_token = resolve_connection()
    headers = {"Authorization": f"Bearer {api_token}"} if api_token else {}
    with httpx.Client(base_url=base_url, timeout=15, headers=headers) as client:
        response = client.request(method, path, json=payload)
        response.raise_for_status()
        return response.json() if response.content else None


@mcp.tool()
def list_projects() -> list[dict[str, Any]]:
    """List projects available to the current credential so callers can retrieve canonical slugs instead of guessing."""
    return request("GET", "/api/v1/projects")


@mcp.tool()
def get_project_context(project: str) -> dict[str, Any]:
    """Retrieve context for a project slug or ID. Use list_projects when the canonical slug is unknown."""
    return request("GET", f"/api/v1/projects/{project}/context")


@mcp.tool()
def start_experimenting(project: str, worker_id: str, loop_mode: str = "continuous") -> dict[str, Any]:
    """Fetch live context and return a compact one-claim-at-a-time execution contract."""
    if loop_mode not in {"single", "continuous"}:
        raise ValueError("loop_mode must be 'single' or 'continuous'")
    context = request("GET", f"/api/v1/projects/{project}/context")
    return {
        "context": context,
        "worker_id": worker_id,
        "loop": {
            "mode": loop_mode,
            "repeat": loop_mode == "continuous",
            "claim_limit": 1,
            "tracking_owner_required": True,
            "cycle": ["search", "claim", "create_or_attach_one_run", "execute", "finish_or_crash", "refresh"],
            "stop_conditions": [
                "The user stops or pauses the loop.",
                "No relevant claimable experiment remains.",
                "Required authority, credentials, hardware, or project context is unavailable.",
                "A project exclusion or safety constraint prevents further work.",
            ],
        },
        "next_action": f"Choose exactly one tracking owner, search evidence, then claim one proposal as '{worker_id}'.",
    }


@mcp.tool()
def search_experiments(project: str, query: str, include_archived: bool = False, limit: int = 10, include_tags: list[str] | None = None, exclude_tags: list[str] | None = None) -> dict[str, Any]:
    """Search hypotheses, reasoning, changes, outcomes, and conclusions within a project."""
    payload: dict[str, Any] = {"project": project, "query": query, "include_archived": include_archived, "limit": limit}
    if include_tags:
        payload["include_tags"] = include_tags
    if exclude_tags:
        payload["exclude_tags"] = exclude_tags
    return request("POST", "/api/v1/search", payload)


@mcp.tool()
def list_tags(project: str) -> list[dict[str, Any]]:
    """List the available project tags, including rule-backed tags."""
    return request("GET", f"/api/v1/projects/{project}/tags")


@mcp.tool()
def create_tag(project: str, name: str) -> dict[str, Any]:
    """Register a new project tag."""
    return request("POST", f"/api/v1/projects/{project}/tags", {"name": name})


@mcp.tool()
def update_tag(project: str, tag_id: str, name: str) -> dict[str, Any]:
    """Rename a registered tag and update its explicit uses."""
    return request("PATCH", f"/api/v1/projects/{project}/tags/{tag_id}", {"name": name})


@mcp.tool()
def delete_tag(project: str, tag_id: str) -> None:
    """Delete a registered tag and remove its explicit uses."""
    return request("DELETE", f"/api/v1/projects/{project}/tags/{tag_id}")


@mcp.tool()
def get_visualization_guide(project: str) -> dict[str, Any]:
    """Return the project-dashboard visualization guide, including existing built-ins and widgets to avoid duplicating."""
    return request("GET", f"/api/v1/projects/{project}/visualizations/guide")


@mcp.tool()
def get_result_visualization_guide(project: str) -> dict[str, Any]:
    """Return the separate guide and registered types for reusable experiment-run result displays."""
    return request("GET", f"/api/v1/projects/{project}/result-visualizations/guide")


@mcp.tool()
def list_result_visualization_types(project: str) -> list[dict[str, Any]]:
    """List built-in and custom experiment result display types selectable instead of curve, timings, or scalar."""
    return request("GET", f"/api/v1/projects/{project}/result-visualizations")


@mcp.tool()
def create_result_visualization_type(project: str, key: str, name: str, spec: dict[str, Any], description: str = "") -> dict[str, Any]:
    """Create a reusable experiment result display backed by the current run's run_metrics dataset."""
    return request("POST", f"/api/v1/projects/{project}/result-visualizations", {"key": key, "name": name, "description": description, "spec": spec, "created_by": "agent"})


@mcp.tool()
def delete_result_visualization_type(project: str, key: str) -> None:
    """Delete an unused custom experiment result display type; built-ins and in-use types cannot be deleted."""
    return request("DELETE", f"/api/v1/projects/{project}/result-visualizations/{key}")


@mcp.tool()
def list_visualizations(project: str) -> list[dict[str, Any]]:
    """List custom visualizations saved to a project."""
    return request("GET", f"/api/v1/projects/{project}/visualizations")


@mcp.tool()
def get_visualization(project: str, visualization_id: str) -> dict[str, Any]:
    """Retrieve one saved visualization and its resolved project data."""
    return request("GET", f"/api/v1/projects/{project}/visualizations/{visualization_id}")


@mcp.tool()
def preview_visualization(project: str, spec: dict[str, Any], source_run_id: str | None = None) -> dict[str, Any]:
    """Validate and resolve an RTVis specification withou
[truncated — 7021 more characters]
```

### apps/web/src/app/page.tsx

```typescript
import { ProjectsScreen } from "@/components/projects-screen"

export default function Home() {
  return <ProjectsScreen />
}

```

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